From fb953424b10bb9bcc5f876120f48572160c5ce9b Mon Sep 17 00:00:00 2001 From: Neil Dhar Date: Wed, 10 Aug 2022 15:48:33 -0700 Subject: [PATCH] Add external ArrayBuffers to JSI (#793) Summary: Pull Request resolved: https://github.com/facebook/hermes/pull/793 Add a JSI API for constructing `ArrayBuffer`s from a user provided `jsi::MutableBuffer`. Changelog: [General][Added] Added ability to construct ArrayBuffers from existing memory buffers. Reviewed By: jpporto Differential Revision: D37744467 fbshipit-source-id: c5071641617af6b3a654dbc5ade1093dc54db960 --- API/hermes/TracingRuntime.cpp | 5 +++ API/hermes/TracingRuntime.h | 2 ++ API/hermes/hermes.cpp | 20 +++++++++++ API/jsi/jsi/decorator.h | 8 +++++ API/jsi/jsi/jsi.cpp | 2 ++ API/jsi/jsi/jsi.h | 21 ++++++++++++ unittests/API/APITest.cpp | 63 +++++++++++++++++++++++++++++++++++ 7 files changed, 121 insertions(+) diff --git a/API/hermes/TracingRuntime.cpp b/API/hermes/TracingRuntime.cpp index ae75fc13dbe..5505e9fe3a9 100644 --- a/API/hermes/TracingRuntime.cpp +++ b/API/hermes/TracingRuntime.cpp @@ -583,6 +583,11 @@ jsi::Array TracingRuntime::createArray(size_t length) { return arr; } +jsi::ArrayBuffer TracingRuntime::createArrayBuffer( + std::shared_ptr buffer) { + throw std::logic_error("Cannot create external ArrayBuffers in trace mode."); +} + size_t TracingRuntime::size(const jsi::Array &arr) { // Array size inquiries read from the length property, which is // non-configurable and thus cannot have side effects. diff --git a/API/hermes/TracingRuntime.h b/API/hermes/TracingRuntime.h index 86097d2586c..86f0a416af9 100644 --- a/API/hermes/TracingRuntime.h +++ b/API/hermes/TracingRuntime.h @@ -90,6 +90,8 @@ class TracingRuntime : public jsi::RuntimeDecorator { jsi::Value lockWeakObject(jsi::WeakObject &wo) override; jsi::Array createArray(size_t length) override; + jsi::ArrayBuffer createArrayBuffer( + std::shared_ptr buffer) override; size_t size(const jsi::Array &arr) override; size_t size(const jsi::ArrayBuffer &buf) override; diff --git a/API/hermes/hermes.cpp b/API/hermes/hermes.cpp index 78572e62a0a..3fbb6f227a0 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -737,6 +737,8 @@ class HermesRuntimeImpl final : public HermesRuntime, jsi::Value lockWeakObject(jsi::WeakObject &) override; jsi::Array createArray(size_t length) override; + jsi::ArrayBuffer createArrayBuffer( + std::shared_ptr buffer) override; size_t size(const jsi::Array &) override; size_t size(const jsi::ArrayBuffer &) override; uint8_t *data(const jsi::ArrayBuffer &) override; @@ -1987,6 +1989,24 @@ jsi::Array HermesRuntimeImpl::createArray(size_t length) { return add(result->getHermesValue()).getArray(*this); } +jsi::ArrayBuffer HermesRuntimeImpl::createArrayBuffer( + std::shared_ptr buffer) { + vm::GCScope gcScope(runtime_); + auto buf = runtime_.makeHandle(vm::JSArrayBuffer::create( + runtime_, + vm::Handle::vmcast(&runtime_.arrayBufferPrototype))); + auto size = buffer->size(); + auto *data = buffer->data(); + auto *ctx = new std::shared_ptr(std::move(buffer)); + auto finalize = [](void *ctx) { + delete static_cast *>(ctx); + }; + auto res = vm::JSArrayBuffer::setExternalDataBlock( + runtime_, buf, data, size, ctx, finalize); + checkStatus(res); + return add(buf.getHermesValue()).getArrayBuffer(*this); +} + size_t HermesRuntimeImpl::size(const jsi::Array &arr) { vm::GCScope gcScope(runtime_); return getLength(arrayHandle(arr)); diff --git a/API/jsi/jsi/decorator.h b/API/jsi/jsi/decorator.h index 075cb8aa666..9df3a2bc8f5 100644 --- a/API/jsi/jsi/decorator.h +++ b/API/jsi/jsi/decorator.h @@ -302,6 +302,10 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { Array createArray(size_t length) override { return plain_.createArray(length); }; + ArrayBuffer createArrayBuffer( + std::shared_ptr buffer) override { + return plain_.createArrayBuffer(std::move(buffer)); + }; size_t size(const Array& a) override { return plain_.size(a); }; @@ -701,6 +705,10 @@ class WithRuntimeDecorator : public RuntimeDecorator { Around around{with_}; return RD::createArray(length); }; + ArrayBuffer createArrayBuffer( + std::shared_ptr buffer) override { + return RD::createArrayBuffer(std::move(buffer)); + }; size_t size(const Array& a) override { Around around{with_}; return RD::size(a); diff --git a/API/jsi/jsi/jsi.cpp b/API/jsi/jsi/jsi.cpp index 815189d2443..2831571537d 100644 --- a/API/jsi/jsi/jsi.cpp +++ b/API/jsi/jsi/jsi.cpp @@ -66,6 +66,8 @@ Value callGlobalFunction(Runtime& runtime, const char* name, const Value& arg) { Buffer::~Buffer() = default; +MutableBuffer::~MutableBuffer() = default; + PreparedJavaScript::~PreparedJavaScript() = default; Value HostObject::get(Runtime&, const PropNameID&) { diff --git a/API/jsi/jsi/jsi.h b/API/jsi/jsi/jsi.h index 85c0626905c..b50780964ad 100644 --- a/API/jsi/jsi/jsi.h +++ b/API/jsi/jsi/jsi.h @@ -31,6 +31,10 @@ class FBJSRuntime; namespace facebook { namespace jsi { +/// Base class for buffers of data or bytecode that need to be passed to the +/// runtime. The buffer is expected to be fully immutable, so the result of +/// size(), data(), and the contents of the pointer returned by data() must not +/// change after construction. class JSI_EXPORT Buffer { public: virtual ~Buffer(); @@ -52,6 +56,18 @@ class JSI_EXPORT StringBuffer : public Buffer { std::string s_; }; +/// Base class for buffers of data that need to be passed to the runtime. The +/// result of size() and data() must not change after construction. However, the +/// region pointed to by data() may be modified by the user or the runtime. The +/// user must ensure that access to the contents of the buffer is properly +/// synchronised. +class JSI_EXPORT MutableBuffer { + public: + virtual ~MutableBuffer(); + virtual size_t size() const = 0; + virtual uint8_t* data() = 0; +}; + /// PreparedJavaScript is a base class representing JavaScript which is in a /// form optimized for execution, in a runtime-specific way. Construct one via /// jsi::Runtime::prepareJavaScript(). @@ -336,6 +352,8 @@ class JSI_EXPORT Runtime { virtual Value lockWeakObject(WeakObject&) = 0; virtual Array createArray(size_t length) = 0; + virtual ArrayBuffer createArrayBuffer( + std::shared_ptr buffer) = 0; virtual size_t size(const Array&) = 0; virtual size_t size(const ArrayBuffer&) = 0; virtual uint8_t* data(const ArrayBuffer&) = 0; @@ -915,6 +933,9 @@ class JSI_EXPORT ArrayBuffer : public Object { ArrayBuffer(ArrayBuffer&&) = default; ArrayBuffer& operator=(ArrayBuffer&&) = default; + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : ArrayBuffer(runtime.createArrayBuffer(std::move(buffer))) {} + /// \return the size of the ArrayBuffer, according to its byteLength property. /// (C++ naming convention) size_t size(Runtime& runtime) const { diff --git a/unittests/API/APITest.cpp b/unittests/API/APITest.cpp index 7250dff64ab..8db32c97b35 100644 --- a/unittests/API/APITest.cpp +++ b/unittests/API/APITest.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -105,6 +106,68 @@ TEST_F(HermesRuntimeTest, ArrayBufferTest) { EXPECT_EQ(buffer[1], 5678); } +class HermesRuntimeTestMethodsTest : public HermesRuntimeTestBase { + public: + HermesRuntimeTestMethodsTest() + : HermesRuntimeTestBase(::hermes::vm::RuntimeConfig::Builder() + .withEnableHermesInternalTestMethods(true) + .build()) {} +}; + +TEST_F(HermesRuntimeTestMethodsTest, ExternalArrayBufferTest) { + struct FixedBuffer : MutableBuffer { + size_t size() const override { + return sizeof(arr); + } + uint8_t *data() override { + return reinterpret_cast(arr.data()); + } + + std::array arr; + }; + + { + auto buf = std::make_shared(); + for (uint32_t i = 0; i < buf->arr.size(); i++) + buf->arr[i] = i; + auto arrayBuffer = ArrayBuffer(*rt, buf); + auto square = eval( + R"#( +(function (buf) { + var view = new Uint32Array(buf); + for(var i = 0; i < view.length; i++) view[i] = view[i] * view[i]; +}) +)#"); + square.asObject(*rt).asFunction(*rt).call(*rt, arrayBuffer); + for (uint32_t i = 0; i < 256; i++) + EXPECT_EQ(buf->arr[i], i * i); + } + + { + auto buf = std::make_shared(); + std::weak_ptr weakBuf(buf); + auto arrayBuffer = ArrayBuffer(*rt, std::move(buf)); + auto detach = eval( + R"#( +(function (buf) { + var view = new Uint32Array(buf); + HermesInternal.detachArrayBuffer(buf); + view[0] = 5; +}) +)#"); + try { + detach.asObject(*rt).asFunction(*rt).call(*rt, arrayBuffer); + FAIL() << "Expected JSIException"; + } catch (const JSError &ex) { + EXPECT_NE( + strstr(ex.what(), "Cannot set a value into a detached ArrayBuffer"), + nullptr); + } + rt->instrumentation().collectGarbage(""); + EXPECT_TRUE(weakBuf.expired()); + } +} + TEST_F(HermesRuntimeTest, BytecodeTest) { const uint8_t shortBytes[] = {1, 2, 3}; EXPECT_FALSE(HermesRuntime::isHermesBytecode(shortBytes, 0));