diff --git a/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp b/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp index 362d76d0252a95..34f43c0c08e9a0 100644 --- a/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp +++ b/packages/react-native/ReactCommon/jsc/JSCRuntime.cpp @@ -256,6 +256,8 @@ class JSCRuntime : public jsi::Runtime { jsi::Runtime::PointerValue* makeStringValue(JSStringRef str) const; jsi::Runtime::PointerValue* makeObjectValue(JSObjectRef obj) const; + JSValueRef getNativeStateSymbol(); + void checkException(JSValueRef exc); void checkException(JSValueRef res, JSValueRef exc); void checkException(JSValueRef exc, const char* msg); @@ -264,6 +266,7 @@ class JSCRuntime : public jsi::Runtime { JSGlobalContextRef ctx_; std::atomic ctxInvalid_; std::string desc_; + JSValueRef nativeStateSymbol_ = nullptr; #ifndef NDEBUG mutable std::atomic objectCounter_; mutable std::atomic symbolCounter_; @@ -384,6 +387,8 @@ JSCRuntime::~JSCRuntime() { // atomic to avoid unsafe unprotects happening after shutdown // has started. ctxInvalid_ = true; + // No need to unprotect nativeStateSymbol_ since the heap is getting torn down + // anyway JSGlobalContextRelease(ctx_); #ifndef NDEBUG assert( @@ -450,25 +455,6 @@ bool JSCRuntime::isInspectable() { return false; } -namespace { - -bool smellsLikeES6Symbol(JSGlobalContextRef ctx, JSValueRef ref) { - // Since iOS 13, JSValueGetType will return kJSTypeSymbol - // Before: Empirically, an es6 Symbol is not an object, but its type is - // object. This makes no sense, but we'll run with it. - // https://github.com/WebKit/webkit/blob/master/Source/JavaScriptCore/API/JSValueRef.cpp#L79-L82 - - JSType type = JSValueGetType(ctx, ref); - - if (type == /* kJSTypeSymbol */ 6) { - return true; - } - - return (!JSValueIsObject(ctx, ref) && type == kJSTypeObject); -} - -} // namespace - JSCRuntime::JSCSymbolValue::JSCSymbolValue( JSGlobalContextRef ctx, const std::atomic& ctxInvalid, @@ -486,7 +472,7 @@ JSCRuntime::JSCSymbolValue::JSCSymbolValue( counter_(counter) #endif { - assert(smellsLikeES6Symbol(ctx_, sym_)); + assert(JSValueIsSymbol(ctx_, sym_)); JSValueProtect(ctx_, sym_); #ifndef NDEBUG counter_ += 1; @@ -723,7 +709,7 @@ jsi::Object JSCRuntime::createObject() { } // HostObject details -namespace detail { +namespace { struct HostObjectProxyBase { HostObjectProxyBase( JSCRuntime& rt, @@ -733,15 +719,13 @@ struct HostObjectProxyBase { JSCRuntime& runtime; std::shared_ptr hostObject; }; -} // namespace detail -namespace { std::once_flag hostObjectClassOnceFlag; JSClassRef hostObjectClass{}; } // namespace jsi::Object JSCRuntime::createObject(std::shared_ptr ho) { - struct HostObjectProxy : public detail::HostObjectProxyBase { + struct HostObjectProxy : public HostObjectProxyBase { static JSValueRef getProperty( JSContextRef ctx, JSObjectRef object, @@ -873,25 +857,107 @@ std::shared_ptr JSCRuntime::getHostObject( // We are guaranteed at this point to have isHostObject(obj) == true // so the private data should be HostObjectMetadata JSObjectRef object = objectRef(obj); - auto metadata = - static_cast(JSObjectGetPrivate(object)); + auto metadata = static_cast(JSObjectGetPrivate(object)); assert(metadata); return metadata->hostObject; } -bool JSCRuntime::hasNativeState(const jsi::Object&) { - throw std::logic_error("Not implemented"); +// NativeState details +namespace { +struct NativeStateContainer { + NativeStateContainer(std::shared_ptr state) + : nativeState(std::move(state)) {} + + std::shared_ptr nativeState; + + static void finalize(JSObjectRef obj) { + auto container = + static_cast(JSObjectGetPrivate(obj)); + delete container; + } +}; + +JSClassRef getNativeStateClass() { + static JSClassRef nativeStateClass = [] { + JSClassDefinition nativeStateClassDef = kJSClassDefinitionEmpty; + nativeStateClassDef.version = 0; + nativeStateClassDef.attributes = kJSClassAttributeNoAutomaticPrototype; + nativeStateClassDef.finalize = NativeStateContainer::finalize; + return JSClassCreate(&nativeStateClassDef); + }(); + return nativeStateClass; +} +} // namespace + +JSValueRef JSCRuntime::getNativeStateSymbol() { + if (!nativeStateSymbol_) { + JSStringRef symbolName = + JSStringCreateWithUTF8CString("__internal_nativeState"); + JSValueRef symbol = JSValueMakeSymbol(ctx_, symbolName); + JSValueProtect(ctx_, symbol); + nativeStateSymbol_ = symbol; + JSStringRelease(symbolName); + } + return nativeStateSymbol_; +} + +bool JSCRuntime::hasNativeState(const jsi::Object& obj) { + JSValueRef exc = nullptr; + JSValueRef state = JSObjectGetPropertyForKey( + ctx_, objectRef(obj), getNativeStateSymbol(), &exc); + checkException(exc); + + return JSValueIsObjectOfClass(ctx_, state, getNativeStateClass()); } std::shared_ptr JSCRuntime::getNativeState( - const jsi::Object&) { - throw std::logic_error("Not implemented"); + const jsi::Object& obj) { + JSValueRef exc = nullptr; + JSValueRef state = JSObjectGetPropertyForKey( + ctx_, objectRef(obj), getNativeStateSymbol(), &exc); + checkException(exc); + + JSObjectRef stateObj = JSValueToObject(ctx_, state, &exc); + checkException(exc); + + auto container = + static_cast(JSObjectGetPrivate(stateObj)); + assert(container); + return container->nativeState; } void JSCRuntime::setNativeState( - const jsi::Object&, - std::shared_ptr) { - throw std::logic_error("Not implemented"); + const jsi::Object& obj, + std::shared_ptr nativeState) { + JSValueRef nativeStateSymbol = getNativeStateSymbol(); + + JSValueRef exc = nullptr; + JSValueRef state = + JSObjectGetPropertyForKey(ctx_, objectRef(obj), nativeStateSymbol, &exc); + checkException(exc); + if (JSValueIsUndefined(ctx_, state)) { + JSObjectRef stateObj = JSObjectMake( + ctx_, + getNativeStateClass(), + new NativeStateContainer(std::move(nativeState))); + JSObjectSetPropertyForKey( + ctx_, + objectRef(obj), + nativeStateSymbol, + stateObj, + kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontEnum | + kJSPropertyAttributeDontDelete, + &exc); + checkException(exc); + } else { + JSObjectRef stateObj = JSValueToObject(ctx_, state, &exc); + checkException(exc); + + auto container = + static_cast(JSObjectGetPrivate(stateObj)); + assert(container); + container->nativeState = std::move(nativeState); + } } jsi::Value JSCRuntime::getProperty( @@ -1415,16 +1481,11 @@ jsi::Value JSCRuntime::createValue(JSValueRef value) const { JSObjectRef objRef = JSValueToObject(ctx_, value, nullptr); return jsi::Value(createObject(objRef)); } - // TODO: Uncomment this when all supported JSC versions have this symbol - // case kJSTypeSymbol: - default: { - if (smellsLikeES6Symbol(ctx_, value)) { - return jsi::Value(createSymbol(value)); - } else { - // WHAT ARE YOU - abort(); - } - } + case kJSTypeSymbol: + return jsi::Value(createSymbol(value)); + default: + // WHAT ARE YOU + abort(); } } diff --git a/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp b/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp index bbfe8f2c06b148..e24284112beb89 100644 --- a/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp +++ b/packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp @@ -1452,6 +1452,74 @@ TEST_P(JSITest, ArrayBufferSizeTest) { 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 + holder.setNativeState(rt, nullptr); + EXPECT_TRUE(holder.hasNativeState(rt)); + EXPECT_TRUE(holder.getNativeState(rt) == nullptr); +} + +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); +} + INSTANTIATE_TEST_CASE_P( Runtimes, JSITest,