internal import ExpoModulesJSI_Cxx internal import jsi /// Base class for JS object's native state. open class JavaScriptNativeState { /// Public-facing factory: builds a heap-allocated `expo::NativeStateShared` /// wrapping a `jsi::NativeState` subclass that derives from `expo::NativeState`. /// The factory is invoked lazily by `acquireShared` — i.e. only when the wrapper /// is about to be attached to a JS object. Lazy invocation matters because the /// factory's pointee may carry side-effecting destructors (e.g. SharedObject's /// releaser) that must not fire until JSI has taken ownership. /// /// Each invocation gets a fresh Swift context (a retained `Unmanaged` pointer to /// the wrapper) plus a deallocator that releases that pointer when the C++ /// pointee dies. Both must be forwarded to the produced pointee. public typealias Factory = ( _ context: UnsafeMutableRawPointer, _ deallocator: @convention(c) (UnsafeMutableRawPointer?) -> Void ) -> UnsafeMutableRawPointer /// Internal factory shape used by `acquireShared`. Returns the shared_ptr by value /// so the default `init()` can use a built-in materialization without going through /// a heap allocation; `init(factory:)` adapts the user-supplied `Factory` to this /// shape by consuming the heap pointer. private typealias InternalFactory = ( _ context: UnsafeMutableRawPointer, _ deallocator: @convention(c) (UnsafeMutableRawPointer?) -> Void ) -> expo.NativeStateShared /// Non-owning reference to the underlying C++ `expo::NativeState`. `nil` until /// the first `setNativeState` call materializes a pointee, and stays populated /// afterward (though its contents may expire when the last JSI slot drops the /// strong ref). `acquireShared()` transparently re-allocates if expired. private var weakPointee: WeakPointer? = nil private let factory: InternalFactory public typealias Deallocator = (_ nativeState: JavaScriptNativeState) -> Void private var deallocator: Deallocator? = nil public init() { self.factory = { context, contextDeallocator in return expo.makeExpoNativeStateShared(context, contextDeallocator) } } /// Builds the underlying native state via a custom factory. See `Factory` for /// the lazy-invocation contract. The factory must be safe to invoke more than /// once: `acquireShared()` re-runs it when reattaching after the previous /// pointee was released, so closures capturing one-shot identity (e.g. a /// freshly-pulled registry id) should keep the wrapper alive for exactly one /// attachment and let it deallocate afterward. public init(factory: @escaping Factory) { self.factory = { context, contextDeallocator in let pointer = factory(context, contextDeallocator) let typed = pointer.assumingMemoryBound(to: expo.NativeStateShared.self) return expo.consumeNativeStateSharedPtr(typed) } } /// Returns a `shared_ptr` to the underlying `expo::NativeState`, materializing /// one lazily if there's no live pointee. Called by `JavaScriptObject.setNativeState` /// to obtain the value to pass to JSI. /// /// Reads then writes `weakPointee` without synchronization — safe only because /// every call site today runs on `JavaScriptActor`. A cross-runtime sharing /// path (e.g. worklets attaching the same native state from a different /// runtime/thread) must add locking before reusing this entry point. internal func acquireShared() -> expo.NativeStateShared { if let alive = weakPointee?.lock() { return alive } let context = Unmanaged.passRetained(self).toOpaque() let shared = factory(context, JavaScriptNativeState.contextDeallocator) self.weakPointee = WeakPointer(shared) return shared } private static let contextDeallocator: @convention(c) (UnsafeMutableRawPointer?) -> Void = { context in guard let context else { return } let unmanagedContext = Unmanaged.fromOpaque(context) let nativeState = unmanagedContext.takeUnretainedValue() nativeState.deallocator?(nativeState) unmanagedContext.release() } /// Sets a deallocator, a closure that is invoked when the underlying C++ pointee /// dies, i.e. when this native state is no longer attached to any JS object. /// Replaces any previously set deallocator. The same closure is used across /// generations if the wrapper is reattached after a release. public func setDeallocator(_ deallocator: @escaping Deallocator) { self.deallocator = deallocator } /// Turns the C++ `expo.NativeState` pointer into its Swift counterpart, or /// returns `nil` if the context is null or the recovered instance's type /// doesn't match `Self`. Precondition: a non-null context must be a retained /// `JavaScriptNativeState` opaque pointer (see `Public/NativeState.h::getContext`); /// any other layout will be reinterpreted and crash. internal static func from(cxx nativeState: UnsafeMutablePointer) -> Self? { guard let context = nativeState.pointee.getContext() else { return nil } let value = Unmanaged.fromOpaque(context).takeUnretainedValue() return value as? Self } // MARK: - WeakPointer /// Non-owning wrapper around `expo::NativeStateWeak`. `lock()` returns a strong /// `shared_ptr` if the pointee is still alive, or `nil` if the last JSI slot has /// already dropped it. private struct WeakPointer { let inner: expo.NativeStateWeak init(_ shared: borrowing expo.NativeStateShared) { self.inner = expo.makeNativeStateWeak(shared) } func lock() -> expo.NativeStateShared? { let strong = inner.lock() return strong.__convertToBool() ? strong : nil } } }