diff --git a/jsi/jsi/JSIDynamic.cpp b/jsi/jsi/JSIDynamic.cpp index 0ff314b..05557ac 100644 --- a/jsi/jsi/JSIDynamic.cpp +++ b/jsi/jsi/JSIDynamic.cpp @@ -152,7 +152,10 @@ void dynamicFromValueShallow( } // namespace -folly::dynamic dynamicFromValue(Runtime& runtime, const Value& valueInput) { +folly::dynamic dynamicFromValue( + Runtime& runtime, + const Value& valueInput, + std::function filterObjectKeys) { std::vector stack; folly::dynamic ret; @@ -184,13 +187,17 @@ folly::dynamic dynamicFromValue(Runtime& runtime, const Value& valueInput) { if (prop.isUndefined()) { continue; } + auto nameStr = name.utf8(runtime); + if (filterObjectKeys && filterObjectKeys(nameStr)) { + continue; + } // The JSC conversion uses JSON.stringify, which substitutes // null for a function, so we do the same here. Just dropping // the pair might also work, but would require more testing. if (prop.isObject() && prop.getObject(runtime).isFunction(runtime)) { prop = Value::null(); } - props.emplace_back(name.utf8(runtime), std::move(prop)); + props.emplace_back(std::move(nameStr), std::move(prop)); top.dyn->insert(props.back().first, nullptr); } for (const auto& prop : props) { diff --git a/jsi/jsi/JSIDynamic.h b/jsi/jsi/JSIDynamic.h index 110dd13..a96cc28 100644 --- a/jsi/jsi/JSIDynamic.h +++ b/jsi/jsi/JSIDynamic.h @@ -19,7 +19,8 @@ facebook::jsi::Value valueFromDynamic( folly::dynamic dynamicFromValue( facebook::jsi::Runtime& runtime, - const facebook::jsi::Value& value); + const facebook::jsi::Value& value, + std::function filterObjectKeys = nullptr); } // namespace jsi } // namespace facebook diff --git a/jsi/jsi/README.md b/jsi/jsi/README.md index 58ff4ed..a595f01 100644 --- a/jsi/jsi/README.md +++ b/jsi/jsi/README.md @@ -9,8 +9,9 @@ It is being used by React Native project to work with JS engines. JSI has versions associated with the following commit hashes in the https://github.com/facebook/hermes repo. -| Version | Commit Hash | Commit Description -|--------:|:-----------------------------------------|------------------------------------------------------ +| Version | Commit Hash | Commit Description +|--------:|:-------------------------------------------|------------------------------------------------------ +| 11 | `a1c168705f609c8f1ae800c60d88eb199154264b` | Add JSI method for setting external memory size | 10 | `b81666598672cb5f8b365fe6548d3273f216322e` | Clarify const-ness of JSI references | 9 | `e6d887ae96bef5c71032f11ed1a9fb9fecec7b46` | Add external ArrayBuffers to JSI | 8 | `4d64e61a1f9926eca0afd4eb38d17cea30bdc34c` | Add BigInt JSI API support diff --git a/jsi/jsi/decorator.h b/jsi/jsi/decorator.h index 19c7507..69a3a0a 100644 --- a/jsi/jsi/decorator.h +++ b/jsi/jsi/decorator.h @@ -262,6 +262,12 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { } #endif +#if JSI_VERSION >= 11 + void setExternalMemoryPressure(const Object& obj, size_t amt) override { + plain_.setExternalMemoryPressure(obj, amt); + } +#endif + Value getProperty(const Object& o, const PropNameID& name) override { return plain_.getProperty(o, name); }; diff --git a/jsi/jsi/jsi-inl.h b/jsi/jsi/jsi-inl.h index 1b7ad93..1151f7e 100644 --- a/jsi/jsi/jsi-inl.h +++ b/jsi/jsi/jsi-inl.h @@ -232,6 +232,13 @@ inline void Object::setNativeState( } #endif +#if JSI_VERSION >= 11 +inline void Object::setExternalMemoryPressure(Runtime& runtime, size_t amt) + const { + runtime.setExternalMemoryPressure(*this, amt); +} +#endif + inline Array Object::getPropertyNames(Runtime& runtime) const { return runtime.getPropertyNames(*this); } diff --git a/jsi/jsi/jsi.cpp b/jsi/jsi/jsi.cpp index 9997f5f..96b6e04 100644 --- a/jsi/jsi/jsi.cpp +++ b/jsi/jsi/jsi.cpp @@ -476,6 +476,12 @@ JSError::JSError(std::string what, Runtime& rt, Value&& value) setValue(rt, std::move(value)); } +JSError::JSError(Value&& value, std::string message, std::string stack) + : JSIException(message + "\n\n" + stack), + value_(std::make_shared(std::move(value))), + message_(std::move(message)), + stack_(std::move(stack)) {} + void JSError::setValue(Runtime& rt, Value&& value) { value_ = std::make_shared(std::move(value)); diff --git a/jsi/jsi/jsi.h b/jsi/jsi/jsi.h index 36c90e0..9610a80 100644 --- a/jsi/jsi/jsi.h +++ b/jsi/jsi/jsi.h @@ -30,7 +30,7 @@ // JSI version defines set of features available in the API. // Each significant API change must be under a new version. #ifndef JSI_VERSION -#define JSI_VERSION 10 +#define JSI_VERSION 11 #endif #if JSI_VERSION >= 3 @@ -431,6 +431,13 @@ class JSI_EXPORT Runtime { virtual bool instanceOf(const Object& o, const Function& f) = 0; +#if JSI_VERSION >= 11 + /// See Object::setExternalMemoryPressure. + virtual void setExternalMemoryPressure( + const jsi::Object& obj, + size_t amount) = 0; +#endif + // These exist so derived classes can access the private parts of // Value, Symbol, String, and Object, which are all friends of Runtime. template @@ -890,6 +897,18 @@ class JSI_EXPORT Object : public Pointer { /// works. I only need it in one place.) Array getPropertyNames(Runtime& runtime) const; +#if JSI_VERSION >= 11 + /// Inform the runtime that there is additional memory associated with a given + /// JavaScript object that is not visible to the GC. This can be used if an + /// object is known to retain some native memory, and may be used to guide + /// decisions about when to run garbage collection. + /// This method may be invoked multiple times on an object, and subsequent + /// calls will overwrite any previously set value. Once the object is garbage + /// collected, the associated external memory will be considered freed and may + /// no longer factor into GC decisions. + void setExternalMemoryPressure(Runtime& runtime, size_t amt) const; +#endif + protected: void setPropertyValue( Runtime& runtime, @@ -980,6 +999,7 @@ class JSI_EXPORT Array : public Object { private: friend class Object; friend class Value; + friend class Runtime; void setValueAtIndexImpl(Runtime& runtime, size_t i, const Value& value) JSI_CONST_10 { @@ -1000,7 +1020,8 @@ class JSI_EXPORT ArrayBuffer : public Object { : ArrayBuffer(runtime.createArrayBuffer(std::move(buffer))) {} #endif - /// \return the size of the ArrayBuffer, according to its byteLength property. + /// \return the size of the ArrayBuffer storage. This is not affected by + /// overriding the byteLength property. /// (C++ naming convention) size_t size(Runtime& runtime) const { return runtime.size(*this); @@ -1017,6 +1038,7 @@ class JSI_EXPORT ArrayBuffer : public Object { private: friend class Object; friend class Value; + friend class Runtime; ArrayBuffer(Runtime::PointerValue* value) : Object(value) {} }; @@ -1125,6 +1147,7 @@ class JSI_EXPORT Function : public Object { private: friend class Object; friend class Value; + friend class Runtime; Function(Runtime::PointerValue* value) : Object(value) {} }; @@ -1156,16 +1179,16 @@ class JSI_EXPORT Value { } /// Moves a Symbol, String, or Object rvalue into a new JS value. - template - /* implicit */ Value(T&& other) : Value(kindOf(other)) { - static_assert( - std::is_base_of::value || + template < + typename T, + typename = std::enable_if_t< + std::is_base_of::value || #if JSI_VERSION >= 6 - std::is_base_of::value || + std::is_base_of::value || #endif - std::is_base_of::value || - std::is_base_of::value, - "Value cannot be implicitly move-constructed from this type"); + std::is_base_of::value || + std::is_base_of::value>> + /* implicit */ Value(T&& other) : Value(kindOf(other)) { new (&data_.pointer) T(std::move(other)); } @@ -1464,7 +1487,7 @@ class JSI_EXPORT Scope { explicit Scope(Runtime& rt) : rt_(rt), prv_(rt.pushScope()) {} ~Scope() { rt_.popScope(prv_); - }; + } Scope(const Scope&) = delete; Scope(Scope&&) = delete; @@ -1486,8 +1509,8 @@ class JSI_EXPORT Scope { /// Base class for jsi exceptions class JSI_EXPORT JSIException : public std::exception { protected: - JSIException(){}; - JSIException(std::string what) : what_(std::move(what)){}; + JSIException() {} + JSIException(std::string what) : what_(std::move(what)) {} public: JSIException(const JSIException&) = default; @@ -1528,7 +1551,7 @@ class JSI_EXPORT JSError : public JSIException { /// Creates a JSError referring to new \c Error instance capturing current /// JavaScript stack. The error message property is set to given \c message. JSError(Runtime& rt, const char* message) - : JSError(rt, std::string(message)){}; + : JSError(rt, std::string(message)) {} /// Creates a JSError referring to a JavaScript Object having message and /// stack properties set to provided values. @@ -1539,6 +1562,11 @@ class JSI_EXPORT JSError : public JSIException { /// but necessary to avoid ambiguity with the above. JSError(std::string what, Runtime& rt, Value&& value); + /// Creates a JSError referring to the provided value, message and stack. This + /// constructor does not take a Runtime parameter, and therefore cannot result + /// in recursively invoking the JSError constructor. + JSError(Value&& value, std::string message, std::string stack); + JSError(const JSError&) = default; virtual ~JSError(); diff --git a/jsi/jsi/test/testlib.cpp b/jsi/jsi/test/testlib.cpp index aae3b06..f4f142a 100644 --- a/jsi/jsi/test/testlib.cpp +++ b/jsi/jsi/test/testlib.cpp @@ -50,8 +50,8 @@ TEST_P(JSITest, PropNameIDTest) { rt, movedQuux, PropNameID::forAscii(rt, std::string("foo")))); uint8_t utf8[] = {0xF0, 0x9F, 0x86, 0x97}; PropNameID utf8PropNameID = PropNameID::forUtf8(rt, utf8, sizeof(utf8)); - // See about char8_t conversion: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1423r2.html - EXPECT_EQ(utf8PropNameID.utf8(rt), reinterpret_cast(u8"\U0001F197")); + EXPECT_EQ( + utf8PropNameID.utf8(rt), reinterpret_cast(u8"\U0001F197")); EXPECT_TRUE(PropNameID::compare( rt, utf8PropNameID, PropNameID::forUtf8(rt, utf8, sizeof(utf8)))); PropNameID nonUtf8PropNameID = PropNameID::forUtf8(rt, "meow"); @@ -479,6 +479,20 @@ TEST_P(JSITest, ArrayTest) { Array alpha2 = Array(rt, 1); alpha2 = std::move(alpha); EXPECT_EQ(alpha2.size(rt), 4); + + // Test getting/setting an element that is an accessor. + // TODO: Make it pass for Hermes and V8 + // auto arrWithAccessor = + // eval( + // "Object.defineProperty([], '0', {set(){ throw 72 }, get(){ return + // 45 }});") .getObject(rt) .getArray(rt); + // try { + // arrWithAccessor.setValueAtIndex(rt, 0, 1); + // FAIL() << "Expected exception"; + // } catch (const JSError& err) { + // EXPECT_EQ(err.value().getNumber(), 72); + // } + // EXPECT_EQ(arrWithAccessor.getValueAtIndex(rt, 0).getNumber(), 45); } TEST_P(JSITest, FunctionTest) { @@ -741,7 +755,7 @@ TEST_P(JSITest, HostFunctionTest) { .utf8(rt), "A cat was called with std::function::target"); EXPECT_TRUE(callable.isHostFunction(rt)); - EXPECT_NE(callable.getHostFunction(rt).target(), nullptr); + EXPECT_TRUE(callable.getHostFunction(rt).target() != nullptr); std::string strval = "strval1"; auto getter = Object(rt); @@ -1232,7 +1246,7 @@ TEST_P(JSITest, MultiDecoratorTest) { 0, [](Runtime& rt, const Value& thisVal, const Value* args, size_t count) { MultiRuntime* funcmrt = dynamic_cast(&rt); - EXPECT_NE(funcmrt, nullptr); + EXPECT_TRUE(funcmrt != nullptr); EXPECT_EQ(funcmrt->count(), 3); EXPECT_EQ(funcmrt->nest(), 1); return Value::undefined(); @@ -1426,7 +1440,113 @@ TEST_P(JSITest, MultilevelDecoratedHostObject) { EXPECT_EQ(1, RD2::numGets); } -INSTANTIATE_TEST_SUITE_P( +TEST_P(JSITest, ArrayBufferSizeTest) { + auto ab = + eval("var x = new ArrayBuffer(10); x").getObject(rt).getArrayBuffer(rt); + EXPECT_EQ(ab.size(rt), 10); + + try { + // Ensure we can safely write some data to the buffer. + memset(ab.data(rt), 0xab, 10); + } catch (const JSINativeException& ex) { + // data() is unimplemented by some runtimes, ignore such failures. + } + + // Ensure that setting the byteLength property does not change the length. + eval("Object.defineProperty(x, 'byteLength', {value: 20})"); + EXPECT_EQ(ab.size(rt), 10); +} + +namespace { + +struct IntState : public NativeState { + explicit IntState(int value) : value(value) {} + int value; +}; + +} // namespace + +TEST_P(JSITest, NativeState) { + Object holder(rt); + EXPECT_FALSE(holder.hasNativeState(rt)); + + auto stateValue = std::make_shared(42); + holder.setNativeState(rt, stateValue); + EXPECT_TRUE(holder.hasNativeState(rt)); + EXPECT_EQ( + std::dynamic_pointer_cast(holder.getNativeState(rt))->value, + 42); + + stateValue = std::make_shared(21); + holder.setNativeState(rt, stateValue); + EXPECT_TRUE(holder.hasNativeState(rt)); + EXPECT_EQ( + std::dynamic_pointer_cast(holder.getNativeState(rt))->value, + 21); + + // There's currently way to "delete" the native state of a component fully + // Even when reset with nullptr, hasNativeState will still return true + // TODO: Make it pass for Hermes and V8 + // holder.setNativeState(rt, nullptr); + // EXPECT_TRUE(holder.hasNativeState(rt)); + // EXPECT_TRUE(holder.getNativeState(rt) == nullptr); +} + +// TODO: Make it pass on Hermes +// TEST_P(JSITest, NativeStateSymbolOverrides) { +// Object holder(rt); + +// auto stateValue = std::make_shared(42); +// holder.setNativeState(rt, stateValue); + +// // Attempting to change configurable attribute of unconfigurable property +// try { +// function( +// "function (obj) {" +// " var mySymbol = Symbol();" +// " obj[mySymbol] = 'foo';" +// " var allSymbols = Object.getOwnPropertySymbols(obj);" +// " for (var sym of allSymbols) {" +// " Object.defineProperty(obj, sym, {configurable: true, writable: +// true});" " obj[sym] = 'bar';" " }" +// "}") +// .call(rt, holder); +// } catch (const JSError& ex) { +// // On JSC this throws, but it doesn't on Hermes +// std::string exc = ex.what(); +// EXPECT_NE( +// exc.find( +// "Attempting to change configurable attribute of unconfigurable +// property"), +// std::string::npos); +// } + +// EXPECT_TRUE(holder.hasNativeState(rt)); +// EXPECT_EQ( +// std::dynamic_pointer_cast(holder.getNativeState(rt))->value, +// 42); +// } + +TEST_P(JSITest, UTF8ExceptionTest) { + // Test that a native exception containing UTF-8 characters is correctly + // passed through. + Function throwUtf8 = Function::createFromHostFunction( + rt, + PropNameID::forAscii(rt, "throwUtf8"), + 1, + [](Runtime& rt, const Value&, const Value* args, size_t) -> Value { + throw JSINativeException(args[0].asString(rt).utf8(rt)); + }); + std::string utf8 = "👍"; + try { + throwUtf8.call(rt, utf8); + FAIL(); + } catch (const JSError& e) { + EXPECT_NE(e.getMessage().find(utf8), std::string::npos); + } +} + +INSTANTIATE_TEST_CASE_P( Runtimes, JSITest, ::testing::ValuesIn(runtimeGenerators())); diff --git a/src/NodeApiJsiRuntime.cpp b/src/NodeApiJsiRuntime.cpp index e520fa7..ba8007c 100644 --- a/src/NodeApiJsiRuntime.cpp +++ b/src/NodeApiJsiRuntime.cpp @@ -285,6 +285,10 @@ class NodeApiJsiRuntime : public jsi::Runtime { bool instanceOf(const jsi::Object &obj, const jsi::Function &func) override; +#if JSI_VERSION >= 11 + void setExternalMemoryPressure(const jsi::Object &obj, size_t amount) override; +#endif + private: // RAII class to open and close the environment scope. class NodeApiScope { @@ -1568,6 +1572,12 @@ bool NodeApiJsiRuntime::instanceOf(const jsi::Object &obj, const jsi::Function & return instanceOf(getNodeApiValue(obj), getNodeApiValue(func)); } +#if JSI_VERSION >= 11 +void NodeApiJsiRuntime::setExternalMemoryPressure(const jsi::Object &obj, size_t amount) { + // TODO: implement +} +#endif + //===================================================================================================================== // NodeApiJsiRuntime::NodeApiScope implementation //===================================================================================================================== diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 39d5bba..ee475db 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,7 +40,7 @@ endif() execute_process( COMMAND ${NUGET_EXE} install "Microsoft.JavaScript.Hermes" - -Version 0.1.14 + -Version 0.1.18 -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) @@ -50,7 +50,7 @@ target_link_libraries(jsi_tests PRIVATE ${CMAKE_BINARY_DIR}/packages/Microsoft.J execute_process( COMMAND ${NUGET_EXE} install "ReactNative.V8Jsi.Windows" - -Version 0.71.5 + -Version 0.71.12 -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})