Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add external ArrayBuffers to JSI #793

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions API/hermes/TracingRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,11 @@ jsi::Array TracingRuntime::createArray(size_t length) {
return arr;
}

jsi::ArrayBuffer TracingRuntime::createArrayBuffer(
std::shared_ptr<jsi::MutableBuffer> 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.
Expand Down
2 changes: 2 additions & 0 deletions API/hermes/TracingRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class TracingRuntime : public jsi::RuntimeDecorator<jsi::Runtime> {
jsi::Value lockWeakObject(jsi::WeakObject &wo) override;

jsi::Array createArray(size_t length) override;
jsi::ArrayBuffer createArrayBuffer(
std::shared_ptr<jsi::MutableBuffer> buffer) override;

size_t size(const jsi::Array &arr) override;
size_t size(const jsi::ArrayBuffer &buf) override;
Expand Down
20 changes: 20 additions & 0 deletions API/hermes/hermes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<jsi::MutableBuffer> buffer) override;
size_t size(const jsi::Array &) override;
size_t size(const jsi::ArrayBuffer &) override;
uint8_t *data(const jsi::ArrayBuffer &) override;
Expand Down Expand Up @@ -1987,6 +1989,24 @@ jsi::Array HermesRuntimeImpl::createArray(size_t length) {
return add<jsi::Object>(result->getHermesValue()).getArray(*this);
}

jsi::ArrayBuffer HermesRuntimeImpl::createArrayBuffer(
std::shared_ptr<jsi::MutableBuffer> buffer) {
vm::GCScope gcScope(runtime_);
auto buf = runtime_.makeHandle(vm::JSArrayBuffer::create(
runtime_,
vm::Handle<vm::JSObject>::vmcast(&runtime_.arrayBufferPrototype)));
auto size = buffer->size();
auto *data = buffer->data();
auto *ctx = new std::shared_ptr<jsi::MutableBuffer>(std::move(buffer));
auto finalize = [](void *ctx) {
delete static_cast<std::shared_ptr<jsi::MutableBuffer> *>(ctx);
};
auto res = vm::JSArrayBuffer::setExternalDataBlock(
runtime_, buf, data, size, ctx, finalize);
checkStatus(res);
return add<jsi::Object>(buf.getHermesValue()).getArrayBuffer(*this);
}

size_t HermesRuntimeImpl::size(const jsi::Array &arr) {
vm::GCScope gcScope(runtime_);
return getLength(arrayHandle(arr));
Expand Down
8 changes: 8 additions & 0 deletions API/jsi/jsi/decorator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<MutableBuffer> buffer) override {
return plain_.createArrayBuffer(std::move(buffer));
};
size_t size(const Array& a) override {
return plain_.size(a);
};
Expand Down Expand Up @@ -701,6 +705,10 @@ class WithRuntimeDecorator : public RuntimeDecorator<Plain, Base> {
Around around{with_};
return RD::createArray(length);
};
ArrayBuffer createArrayBuffer(
std::shared_ptr<MutableBuffer> buffer) override {
return RD::createArrayBuffer(std::move(buffer));
};
size_t size(const Array& a) override {
Around around{with_};
return RD::size(a);
Expand Down
2 changes: 2 additions & 0 deletions API/jsi/jsi/jsi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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&) {
Expand Down
21 changes: 21 additions & 0 deletions API/jsi/jsi/jsi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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().
Expand Down Expand Up @@ -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<MutableBuffer> buffer) = 0;
virtual size_t size(const Array&) = 0;
virtual size_t size(const ArrayBuffer&) = 0;
virtual uint8_t* data(const ArrayBuffer&) = 0;
Expand Down Expand Up @@ -915,6 +933,9 @@ class JSI_EXPORT ArrayBuffer : public Object {
ArrayBuffer(ArrayBuffer&&) = default;
ArrayBuffer& operator=(ArrayBuffer&&) = default;

ArrayBuffer(Runtime& runtime, std::shared_ptr<MutableBuffer> 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 {
Expand Down
63 changes: 63 additions & 0 deletions unittests/API/APITest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <hermes/CompileJS.h>
#include <hermes/Public/JSOutOfMemoryError.h>
#include <hermes/hermes.h>
#include <jsi/instrumentation.h>

#include <tuple>

Expand Down Expand Up @@ -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<uint8_t *>(arr.data());
}

std::array<uint32_t, 256> arr;
};

{
auto buf = std::make_shared<FixedBuffer>();
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<FixedBuffer>();
std::weak_ptr<FixedBuffer> 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));
Expand Down