diff --git a/NativeScript/ffi/objc/hermes/NativeApiJsi.mm b/NativeScript/ffi/objc/hermes/NativeApiJsi.mm index 9b3e2f93d..61dca798e 100644 --- a/NativeScript/ffi/objc/hermes/NativeApiJsi.mm +++ b/NativeScript/ffi/objc/hermes/NativeApiJsi.mm @@ -257,6 +257,42 @@ throw JSError(runtime, }); } +#define NATIVESCRIPT_NATIVE_API_HAS_ENGINE_RUNTIME_TEARDOWN 1 + +// Runtime data is destroyed with the runtime while its Values can still be +// released: Hermes frees it before tearing down its heap, and jsi's default +// implementation frees it from a finalizer of that runtime's own global. +class NativeApiRuntimeBridges final { + public: + ~NativeApiRuntimeBridges() { + for (auto& bridge : bridges_) { + bridge->releaseRuntimeValues(); + } + } + + void add(std::shared_ptr bridge) { + bridges_.push_back(std::move(bridge)); + } + + private: + std::vector> bridges_; +}; + +constexpr facebook::jsi::UUID kNativeApiRuntimeBridgesUUID{ + 0x5f0e2c71, 0x8b3d, 0x4a6e, 0x9c41, 0x2d7a6b13e8f4}; + +void BindNativeApiBridgeToRuntime(Runtime& runtime, + std::shared_ptr bridge) { + bridge->setOwnerRuntime(runtime); + auto bridges = std::static_pointer_cast( + runtime.getRuntimeData(kNativeApiRuntimeBridgesUUID)); + if (bridges == nullptr) { + bridges = std::make_shared(); + runtime.setRuntimeData(kNativeApiRuntimeBridgesUUID, bridges); + } + bridges->add(std::move(bridge)); +} + } // namespace #include "../shared/bridge/Install.mm" diff --git a/NativeScript/ffi/objc/shared/bridge/Callbacks.mm b/NativeScript/ffi/objc/shared/bridge/Callbacks.mm index 0188e6e78..25548f656 100644 --- a/NativeScript/ffi/objc/shared/bridge/Callbacks.mm +++ b/NativeScript/ffi/objc/shared/bridge/Callbacks.mm @@ -455,6 +455,9 @@ throw JSError(runtime, } ~NativeApiCallback() { + if (bridge_ != nullptr) { + bridge_->releaseOrAbandonValue(function_); + } if (closure_ != nullptr) { ffi_closure_free(closure_); closure_ = nullptr; @@ -483,7 +486,7 @@ void retainBlockCopy(const void* blockPointer) { } auto self = shared_from_this(); if (bridge_ != nullptr && runtime_ != nullptr && function_ != nullptr && - blockPointer != nullptr) { + blockPointer != nullptr && !bridge_->runtimeValuesReleased()) { bridge_->rememberRoundTripValue(*runtime_, blockPointer, Value(*runtime_, *function_), false, roundTripValidationKey_); @@ -553,7 +556,7 @@ bool releaseBlockCopy(const void* blockPointer) { } if (bridge != nullptr && runtime != nullptr && - pointerToForget != nullptr) { + pointerToForget != nullptr && !bridge->runtimeValuesReleased()) { NativeApiRuntimeScope runtimeScope(*runtime); bridge->forgetRoundTripValue(*runtime, pointerToForget); } @@ -577,6 +580,9 @@ bool releaseBlockCopy(const void* blockPointer) { } void invoke(void* ret, void* args[]) { + if (runtimeDestroyed(ret)) { + return; + } if (runtime_ == nullptr || function_ == nullptr || signature_ == nullptr) { throwNativeApiCallbackException("Invalid callback."); } @@ -735,7 +741,27 @@ void invoke(void* ret, void* args[]) { } private: + // A native caller can keep a block or method implementation past its + // runtime (e.g. across a JavaScript reload); it gets a zero result instead. + bool runtimeDestroyed(void* ret) { + if (bridge_ == nullptr || !bridge_->runtimeValuesReleased()) { + return false; + } + std::string name = signature_ != nullptr ? signature_->selectorName : ""; + if (name.empty()) { + name = block_ ? "block" : "native callback"; + } + NSLog(@"NativeScript: %s called after its JavaScript runtime was " + @"destroyed; returning zero/nil.", + name.c_str()); + zeroReturnValue(ret); + return true; + } + void invokeOnCurrentThread(void* ret, void* args[], std::string* error) { + if (runtimeDestroyed(ret)) { + return; + } try { NativeApiRuntimeScope runtimeScope(*runtime_); size_t nativeArgOffset = signature_->implicitArgumentCount; diff --git a/NativeScript/ffi/objc/shared/bridge/HostObject.mm b/NativeScript/ffi/objc/shared/bridge/HostObject.mm index d16a91f96..857d6fdbe 100644 --- a/NativeScript/ffi/objc/shared/bridge/HostObject.mm +++ b/NativeScript/ffi/objc/shared/bridge/HostObject.mm @@ -24,6 +24,7 @@ explicit NativeApiHostObject(std::shared_ptr bridge) : bridge_(std::move(bridge)) {} Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "runtime") { return makeString(runtime, NATIVESCRIPT_NATIVE_API_RUNTIME_NAME); @@ -507,6 +508,7 @@ throw JSError(runtime, } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; names.reserve(11); addPropertyName(runtime, names, "runtime"); diff --git a/NativeScript/ffi/objc/shared/bridge/Install.mm b/NativeScript/ffi/objc/shared/bridge/Install.mm index 611747cf8..253cf32cc 100644 --- a/NativeScript/ffi/objc/shared/bridge/Install.mm +++ b/NativeScript/ffi/objc/shared/bridge/Install.mm @@ -1,5 +1,11 @@ +#ifndef NATIVESCRIPT_NATIVE_API_HAS_ENGINE_RUNTIME_TEARDOWN +inline void BindNativeApiBridgeToRuntime(Runtime&, + std::shared_ptr) {} +#endif + Object CreateNativeApi(Runtime& runtime, const NativeApiConfig& config) { auto bridge = std::make_shared(config); + BindNativeApiBridgeToRuntime(runtime, bridge); return Object::createFromHostObject(runtime, std::make_shared(std::move(bridge))); } diff --git a/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm b/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm index 57acf87fa..7629af304 100644 --- a/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm +++ b/NativeScript/ffi/objc/shared/bridge/ObjCBridge.mm @@ -545,6 +545,66 @@ explicit NativeApiBridge(const NativeApiConfig& config) } } + // Called while the bridge's runtime is being destroyed, when its Values can + // still be released. Host objects can be shared into another runtime and + // outlive this one, so from here on the bridge holds no Values. + void releaseRuntimeValues() { + decltype(roundTripValues_) roundTripValues; + decltype(roundTripCacheFramesByThread_) roundTripFrames; + decltype(recentRoundTripValues_) recentRoundTripValues; +#ifdef TARGET_ENGINE_HERMES + std::shared_ptr roundTripRoot; +#endif + { + std::lock_guard lock(roundTripValuesMutex_); + runtimeValuesReleased_.store(true, std::memory_order_release); + roundTripValues.swap(roundTripValues_); + roundTripFrames.swap(roundTripCacheFramesByThread_); + recentRoundTripValues.swap(recentRoundTripValues_); + recentRoundTripValueOrder_.clear(); +#ifdef TARGET_ENGINE_HERMES + rootedRoundTripValues_.clear(); + roundTripRoot.swap(roundTripRootCache_); +#endif + roundTripValuesGeneration_.fetch_add(1, std::memory_order_release); + } + classValues_.clear(); + classPrototypes_.clear(); + pointerValues_.clear(); + objectExpandos_.clear(); + objectExpandoOwnerCounts_.clear(); + objectExpandosGeneration_.fetch_add(1, std::memory_order_release); + } + + bool runtimeValuesReleased() const { + return runtimeValuesReleased_.load(std::memory_order_acquire); + } + + // A destroyed runtime freed its Values with its heap; releasing one would + // touch freed memory. + template + void releaseOrAbandonValue(std::shared_ptr& value) { + if (value != nullptr && runtimeValuesReleased()) { + new std::shared_ptr(std::move(value)); + } + value.reset(); + } + + void setOwnerRuntime(const Runtime& runtime) { ownerRuntime_ = &runtime; } + + // Worklets shares host objects between runtimes by reference. Using one in + // a runtime other than its bridge's would mix the two heaps. + void requireOwnerRuntime(Runtime& runtime) const { + if (ownerRuntime_ != nullptr && ownerRuntime_ != &runtime) { + throw JSError( + runtime, + "This NativeScript object belongs to another JavaScript runtime, " + "such as the Worklets UI runtime. Pass a handle instead: " + "interop.handleof(object).toHexString() in the runtime that owns " + "it, then interop.object(handle) in this one."); + } + } + MDMetadataReader* metadata() const { return metadata_.get(); } void* selfDl() const { return selfDl_; } @@ -621,7 +681,7 @@ void rememberRoundTripValue(Runtime& runtime, const void* native, const Value& value, bool stringLikeNative = false, uintptr_t validationKey = 0) { - if (native == nullptr) { + if (native == nullptr || runtimeValuesReleased()) { return; } uintptr_t key = @@ -666,7 +726,7 @@ void rememberScopedRoundTripValueWithValidationKey(Runtime& runtime, bool stringLikeNative, bool persistBeyondFrame, uintptr_t validationKey) { - if (native == nullptr) { + if (native == nullptr || runtimeValuesReleased()) { return; } uintptr_t key = @@ -890,7 +950,7 @@ void endRoundTripCacheFrame() { } void rememberClassValue(Runtime& runtime, Class cls, const Value& value) { - if (cls == Nil) { + if (cls == Nil || runtimeValuesReleased()) { return; } classValues_[normalizeRuntimePointer(reinterpret_cast(cls))] = @@ -910,7 +970,7 @@ Value findClassValue(Runtime& runtime, Class cls) const { } void rememberClassPrototype(Runtime& runtime, Class cls, const Value& value) { - if (cls == Nil) { + if (cls == Nil || runtimeValuesReleased()) { return; } classPrototypes_[normalizeRuntimePointer(reinterpret_cast(cls))] = @@ -931,7 +991,7 @@ Value findClassPrototype(Runtime& runtime, Class cls) const { void setObjectExpando(Runtime& runtime, const void* native, const std::string& property, const Value& value) { - if (native == nullptr || property.empty()) { + if (native == nullptr || property.empty() || runtimeValuesReleased()) { return; } objectExpandos_[normalizeRuntimePointer(reinterpret_cast(native))] @@ -1095,6 +1155,9 @@ void cachePropertyGetter(Class cls, const std::string& property, void rememberPointerValue(Runtime& runtime, const void* native, const Value& value) { + if (runtimeValuesReleased()) { + return; + } pointerValues_[reinterpret_cast(native)] = std::make_shared(runtime, value); } @@ -2173,6 +2236,8 @@ static void appendSurfaceMember( std::thread::id jsThreadId_ = std::this_thread::get_id(); std::mutex retainedLifetimesMutex_; std::vector> retainedLifetimes_; + std::atomic runtimeValuesReleased_{false}; + const Runtime* ownerRuntime_ = nullptr; }; class NativeApiRoundTripCacheFrameGuard final { diff --git a/NativeScript/ffi/objc/shared/bridge/TypeConv.mm b/NativeScript/ffi/objc/shared/bridge/TypeConv.mm index 908c144c6..5e00602bb 100644 --- a/NativeScript/ffi/objc/shared/bridge/TypeConv.mm +++ b/NativeScript/ffi/objc/shared/bridge/TypeConv.mm @@ -1217,6 +1217,7 @@ throw JSError(runtime, "This native return type is not supported by " } Value NativeApiReferenceHostObject::get(Runtime& runtime, const PropNameID& name) { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, "reference"); @@ -1258,6 +1259,7 @@ throw JSError(runtime, "This native return type is not supported by " NativeApiHostSetResult NativeApiReferenceHostObject::set(Runtime& runtime, const PropNameID& name, const Value& value) { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); auto index = parseArrayIndexProperty(property); if (property != "value" && !index) { @@ -1283,6 +1285,7 @@ throw JSError(runtime, "This native return type is not supported by " } Value NativeApiStructObjectHostObject::get(Runtime& runtime, const PropNameID& name) { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, info_ != nullptr && info_->isUnion ? "union" : "struct"); @@ -1328,6 +1331,7 @@ throw JSError(runtime, "This native return type is not supported by " NativeApiHostSetResult NativeApiStructObjectHostObject::set(Runtime& runtime, const PropNameID& name, const Value& value) { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (info_ == nullptr || data_ == nullptr) { throw JSError(runtime, "Struct is not initialized."); @@ -1345,6 +1349,7 @@ throw JSError(runtime, "This native return type is not supported by " } std::vector NativeApiStructObjectHostObject::getPropertyNames(Runtime& runtime) { + bridge_->requireOwnerRuntime(runtime); std::vector names; addPropertyName(runtime, names, "kind"); addPropertyName(runtime, names, "name"); diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm index e74c4032b..04124c280 100644 --- a/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Class.mm @@ -19,6 +19,7 @@ static Class classRespondingToClassSelector(Class cls, SEL selector) { } Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, "class"); @@ -249,6 +250,7 @@ throw JSError( } NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); Class cls = objc_lookUpClass(symbol_.runtimeName.c_str()); if (cls == nil) { @@ -284,6 +286,7 @@ throw JSError(runtime, } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; names.reserve(8); addPropertyName(runtime, names, "kind"); diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm index 5913da68d..94a08e8e8 100644 --- a/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Interop.mm @@ -13,6 +13,9 @@ backingValue_(std::move(backingValue)) {} ~NativeApiPointerHostObject() override { + if (bridge_ != nullptr) { + bridge_->releaseOrAbandonValue(backingValue_); + } if (adopted_ && pointer_ != nullptr) { if (bridge_ != nullptr) { bridge_->forgetPointerValue(pointer_); @@ -39,6 +42,7 @@ void clearWithoutFree() { } Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, kind_); @@ -144,6 +148,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; names.reserve(3); addPropertyName(runtime, names, "kind"); @@ -186,6 +191,10 @@ Value get(Runtime& runtime, const PropNameID& name) override { backingValue_(std::move(backingValue)) {} ~NativeApiReferenceHostObject() override { + if (bridge_ != nullptr) { + bridge_->releaseOrAbandonValue(pendingValue_); + bridge_->releaseOrAbandonValue(backingValue_); + } for (id object : retainedObjects_) { [object release]; } @@ -205,6 +214,7 @@ void ensureStorage(Runtime& runtime, NativeApiType type, Value get(Runtime& runtime, const PropNameID& name) override; NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override; std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; addPropertyName(runtime, names, "kind"); addPropertyName(runtime, names, "value"); diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm index 2fd5b80be..67ac12843 100644 --- a/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Object.mm @@ -12,6 +12,7 @@ } Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "next") { return Function::createFromHostFunction( @@ -24,6 +25,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; addPropertyName(runtime, names, "next"); return names; @@ -178,6 +180,7 @@ NativeApiSymbol nativeApiSymbolForRuntimeClass( } Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, "super"); @@ -240,6 +243,7 @@ throw JSError( } NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (receiver_ == nil || dispatchClass_ == Nil) { throw JSError(runtime, "Cannot set property on nil super."); @@ -281,6 +285,7 @@ throw JSError(runtime, } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; addPropertyName(runtime, names, "kind"); addPropertyName(runtime, names, "toString"); @@ -840,6 +845,7 @@ bool invokeEnginePrototypeSetter(Runtime& runtime, const std::string& property, } Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); // Fast path: check expando cache first (hot path for method calls). @@ -1297,6 +1303,7 @@ throw JSError(runtime, } NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (object_ == nil) { throw JSError(runtime, "Cannot set property on nil object."); @@ -1354,6 +1361,7 @@ throw JSError( } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; names.reserve(6); addPropertyName(runtime, names, "kind"); diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm index 5c689f627..c30912d0c 100644 --- a/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Protocol.mm @@ -15,6 +15,7 @@ const NativeApiSymbol& symbol() const { return symbol_; } Value get(Runtime& runtime, const PropNameID& name) override { + bridge_->requireOwnerRuntime(runtime); std::string property = name.utf8(runtime); if (property == "kind") { return makeString(runtime, "protocol"); @@ -91,6 +92,7 @@ Value get(Runtime& runtime, const PropNameID& name) override { } std::vector getPropertyNames(Runtime& runtime) override { + bridge_->requireOwnerRuntime(runtime); std::vector names; addPropertyName(runtime, names, "kind"); addPropertyName(runtime, names, "name"); diff --git a/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm b/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm index a71cea169..7d6b58495 100644 --- a/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm +++ b/NativeScript/ffi/objc/shared/bridge/host_objects/Struct.mm @@ -26,6 +26,12 @@ } } + ~NativeApiStructObjectHostObject() override { + if (bridge_ != nullptr) { + bridge_->releaseOrAbandonValue(backingValue_); + } + } + void* data() const { return data_; } std::shared_ptr info() const { return info_; } std::shared_ptr> storageOwner() const { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index b15d01120..fc385ad86 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1337,6 +1337,75 @@ export function installWorklets( return true; } +const nativeObjectHandleKey = "__nativeScriptObjectHandle"; + +// Worklets hands a returned native object to this runtime by reference, still +// bound to the UI runtime. Only its handle crosses; the +1 retained here keeps +// it alive until nativeObjectsFromHandles adopts it. +function nativeObjectsToHandles(value: unknown): unknown { + "worklet"; + + if (value === null || typeof value !== "object") { + return value; + } + if (Array.isArray(value)) { + return value.map(nativeObjectsToHandles); + } + const prototype = Object.getPrototypeOf(value); + if (prototype === Object.prototype || prototype === null) { + const result: Record = {}; + for (const key of Object.keys(value)) { + result[key] = nativeObjectsToHandles( + (value as Record)[key], + ); + } + return result; + } + const { NSObject, CFRetain } = globalThis as Record; + if (typeof NSObject !== "function" || !(value instanceof NSObject)) { + return value; + } + const handle = nativeHandleForNSObject(value); + if (handle === undefined) { + return value; + } + CFRetain(value); + return { [nativeObjectHandleKey]: handle }; +} + +function nativeObjectsFromHandles(value: unknown): unknown { + if (value === null || typeof value !== "object") { + return value; + } + if (Array.isArray(value)) { + return value.map(nativeObjectsFromHandles); + } + const handle = (value as Record)[nativeObjectHandleKey]; + if (typeof handle === "string") { + const interop = (globalThis as Record).interop; + return interop.Pointer(Number(handle)).takeRetainedValue(); + } + if (Object.getPrototypeOf(value) !== Object.prototype) { + return value; + } + const result: Record = {}; + for (const key of Object.keys(value)) { + result[key] = nativeObjectsFromHandles( + (value as Record)[key], + ); + } + return result; +} + +function runWorkletReturningHandles( + callback: (...args: Args) => ReturnValue, + args: Args, +): unknown { + "worklet"; + + return nativeObjectsToHandles(callback(...args)); +} + export function runOnUI( callback: (...args: Args) => ReturnValue | Promise, ...args: Args @@ -1352,7 +1421,9 @@ export function runOnUI( "NativeScript.runOnUI requires a worklet callback", ); } - return worklets.runOnUIAsync(callback, ...args); + return worklets + .runOnUIAsync(runWorkletReturningHandles, callback, args) + .then(nativeObjectsFromHandles) as Promise; } function callbackInvoker(