Runtime/FFI fixes extracted from the abandoned RN architecture branch - #70
Open
DjDeveloperr wants to merge 8 commits into
Open
Runtime/FFI fixes extracted from the abandoned RN architecture branch#70DjDeveloperr wants to merge 8 commits into
DjDeveloperr wants to merge 8 commits into
Conversation
Multi-candidate Resources resolution for resolveMainPath() (bundle resourcePath, executable-relative Contents/Resources, argv[0], _NSGetExecutablePath, cwd) so the CLI/test-runner processes that don't run from a standard .app bundle can still find app/index.js or a package.json "main", gated behind NS_BUNDLE_LOADER_DEBUG logging. NativeScript.mm: runMainApplication now tries resolveMainPath() before falling back to "./app/index.js". Switch runtime_ from unique_ptr to a raw pointer with an explicit resetRuntime() teardown point: at process exit, static-destruction order relative to the ObjC runtime is unspecified, so an implicit unique_ptr destructor can run after dependencies it needs are already gone; restartWithConfig: also needs the old runtime to outlive the new one's Init(). ThreadSafeFunction.mm: turn the global cleanup-hook mutex/condvar/map into leaked-singleton accessors (heap-allocated, never destructed) for the same static-destruction-order reason. ci.yml: enable IOS_TEST_VERBOSE_SPECS for per-spec start/done logging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Object expandos (setObjectExpando/findObjectExpando/forgetObjectExpandos) gain a per-runtime key: a worklet spins up an additional Runtime on its own thread against the same shared bridge, so a Value created in one Runtime must never leak into another. Storage becomes native-pointer -> property -> owning-runtime, all under one objectExpandosMutex_ (also now guarding the existing objectExpandoOwnerCounts_ refcounts, since a host-object dtor can release its owner count from either thread relative to a get/set). runtimeObjectExpandoKey() derives the per-runtime identity: the JSI-facing engines (V8/JSC/QuickJS) key on runtime.state().get(), Hermes keys on the Runtime& address directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… no aggregate globals NativeApiBackendConfig gains callbackInvocationAllowed (teardown-safety gate for RN) and indexRuntimePointers (default true; RN sets false). NativeApiBridge::addSymbol() only eagerly resolves objc_lookUpClass / protocol pointers when indexRuntimePointers_ is set — RN launch cost: don't realize every class/protocol at symbol-index time when RN never touches most of them at startup. Callbacks.mm invoke() now checks bridge_->callbackInvocationAllowed() before running the callback and zero-returns instead when the host is tearing down or reloading. NativeApiJsiReactNative.h: RN config sets installGlobalSymbols=false (unchanged behavior) and now also indexRuntimePointers=false. Install.mm's else-branch drops the InstallAggregateGlobals call for RN — unused, and building it eagerly cost launch time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ointer guards interop.setAssociatedObject/getAssociatedObject: the sanctioned way to persist state on a native UIKit-backed object across engine calls. JS expandos on a host-object wrapper do not round-trip (a fresh wrapper can be handed back for the same native receiver on the next call); a real objc_setAssociatedObject does, because it lives on the native object itself. Target accepts a live wrapped object/pointer or the decimal text of a raw address. convertNativeReturnValue: an id-typed return that is actually a Class now resolves through the class-symbol path (by runtime pointer, then runtime class, then bare class_getName) instead of falling into makeNativeObjectValue. nativeObjectPointerMayBeObject (`raw > 0x1000`) guards every id-typed return path (nativeObjectIsStringLike, findCachedNativeObjectReturn, convertNativeReturnValue) against dereferencing a misread register value — without it, a non-object primitive read back as `id` can crash on object_getClass/isKindOfClass:. Primitive type-alias table: long/ulong/NSInteger/NSUInteger (mdTypeSLong/ mdTypeULong), BOOL/CGFloat (platform width)/NSTimeInterval/CFTimeInterval, so signatures can use the platform typedef names instead of only the fixed-width primitives. packages/objc-node-api/index.d.ts: types for the associated-object API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ClassBuilder ("extend()"/native-subclass) surface's identity and
dispatch primitives:
- Object.mm: NativeApiObjectHostObject gains a superDispatchClass_ (set at
construction or via setSuperDispatchClass), used to answer `this.super`
correctly after a wrapper has been re-seated (see below) instead of always
recomputing the receiver's immediate runtime superclass.
detachObjectPreservingBridgeState() disowns a wrapper WITHOUT forgetting
its round-trip value or dropping its expandos — used when an initializer
returns the same receiver a second, divergent wrapper had already claimed.
get()'s engine-extended branch now falls through, for inherited METHODS
only (accessors stay deferred to avoid re-entrant shadowing), to metadata
method resolution via the nearest metadata ancestor, so a first access to
an inherited (non-overridden) selector on a JS subclass resolves instead of
hard-returning undefined. set() hoists the JS-accessor-setter attempt
above the metadata/runtime setter paths (an accessor override must win)
and, in the no-JS-setter fallback, stores the expando unconditionally
(dropped enginePrototypeHasSetter — reaching that branch already proves no
JS setter fired, so re-probing for one was redundant).
- classPrototypeForObject gains a symbol-name fallback (classes only known
by symbol, not yet indexed by runtime pointer with indexRuntimePointers
off).
- Class.mm: makeNativeObjectValue takes an optional superDispatchClass,
threaded onto both the fresh-wrapper and cached-wrapper paths.
- Callbacks.mm: a per-callback NativeApiMethodCallbackPolicy (trimmed to the
subset with a live consumer: callSuperBeforeCallback +
skipCallbackIfAssociatedObjectTruthy, read off a JS function's
`__nativeScriptMethodPolicy` expando via NativeScriptRuntime.nativeMethodPolicy).
invokeMethodSuper() calls the ObjC super implementation via
objc_msgSendSuper before the JS override runs when the policy asks for it.
shouldSkipConstructingMethodCallback suppresses a non-init method callback
reaching a receiver still marked under construction. bindThis_ callbacks'
`this` now carries the override's superDispatchClass too.
- ClassBuilder.mm: preservedNativeApiInitializerSelfReturn detects an
initializer returning the same receiver a wrapper was already created for
and keeps that one wrapper live (detaching the divergent duplicate) rather
than letting two wrappers fight over the same native receiver's bridge
state. callNativeApiBaseObjectSelector wraps $base/super dispatch with
this handling. nativeAccessorCallbackPolicy auto-applies a re-entrancy
guard key to every native accessor (getter/setter) override.
- HostObject.mm: __setObjectConstructionState / __setObjectAccessorCallbackState
native entry points backing the above (associated objects, not JS
expandos — expandos don't round-trip across proxy instances for the same
native receiver).
- Install.mm (JS bootstrap): alloc/init construction marks/unmarks
construction state around JS-subclass instantiation;
installInstanceClassIdentity gives extended prototypes a `class`/
`superclass` identity that resolves to the actual (possibly further
subclassed) constructor; indexed-collection method aliases
(objectAtIndexedSubscript/setObjectAtIndexedSubscript/Symbol.iterator) for
extend()ed NSFastEnumeration-like classes, with accessor callback-state
wrapping folded into the same helper.
- V8HostObjects.mm: the masking (kNone) host-object interceptor's get/set
now check the real V8 prototype chain first (findPrototypeDescriptor/
tryResolvePrototypeGet/tryInvokePrototypeSetter) so a JS-defined prototype
accessor is honored ahead of the interceptor.
- Per-engine (hermes/jsc/quickjs/v8) selector-group call sites: after a
prepared instance-initializer selector call, apply
preservedNativeApiInitializerSelfReturn to the result.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
[SomeView appearance] (and appearanceWhenContainedIn: etc.) hands back an opaque _UIAppearance proxy: UIKit forwards recognized selectors to an internal invocation-recording store instead of actually running them, and there's no public way to ask "what class are you a proxy for" besides parsing `-description`'s `<Customizable class: ClassName>` format. New host_objects/Appearance.mm holds the primitives built on that: parse the description once, tag the recovered class onto the proxy as an expando, then read/write a class-keyed (not proxy-instance-keyed — UIAppearance state is effectively global per class/containment chain) property cache so get() sees what a prior set() wrote instead of round-tripping through UIKit's opaque recording. Setters cache too, since an appearance proxy setter doesn't reliably support read-your-write. Wired in everywhere a UIAppearance proxy's properties can be read or written: - host_objects/Object.mm get()/set(): consult/populate the appearance cache before falling through to metadata/runtime property resolution. tagStaticAppearanceSelectorResult (needs the complete NativeApiObjectHostObject type) stays here and tags+installs accessors on the result of any `[SomeClass appearance...]`-family call. - host_objects/Class.mm: intercepts the `appearance` static method itself so its result gets tagged/accessor-installed rather than staying a plain callable selector-group function. - host_objects/Protocol.mm: the same cache read/write for protocol-declared properties. - Invocation.mm: callPreparedObjCSelector/callObjCSelector tag every fast-path and generic-tail result, and cache every property-setter call (NativeApiPreparedObjCInvocation gains propertySetterName so a successful setter call can cache without re-deriving the property name). callObjCSelector also allows a forwarded property selector through when the receiver is a tagged appearance proxy (class_getInstanceMethod/ respondsToSelector: can both say no for a selector UIKit will still forward). - SelectorGroupCall.h: the shared resolveNativeApiSelectorGroupCall() short-circuits a property-getter call through the appearance cache before ever touching ObjC, and gains a gsdAllowed field so appearance static selectors are excluded from every engine's raw-GSD fast path (which bypasses proxy tagging). - Per-engine (hermes/jsc/quickjs/v8) GSD/fast-path tails: cache a successful setter call's value and tag/re-tag the result, mirroring the generic path. Also brings in the runtimeReadablePropertyGetter cache (simplified to a single mutex-guarded (Class, property) -> selector map, no thread-local front cache) and objectGetPathCanReadRuntimeProperty, both prerequisites for the appearance-adjacent set() success-path expando write (fixes a set-then-get asymmetry for write-only/asymmetrically-named runtime properties) and reused by get()'s inherited-method resolution added in the previous commit. classPrototypeForObject's symbol-name fallback (needed when a class isn't yet runtime-pointer-indexed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ssing A bound selector-group method function (e.g. `view.viewWithTag`) is cached as a native-object expando keyed by the underlying ObjC pointer (Object.mm's `bridge_->setObjectExpando(..., methodFunction)`), so the cache survives independently of the `NativeApiObjectHostObject` wrapper it was bound to. Once that original wrapper is torn down (its owning JS proxy collected) and the SAME native pointer is later re-wrapped by a fresh `NativeApiObjectHostObject` on another crossing, the stale cached function still resolves its receiver via the dead wrapper's weak/lifetime state -- `data.boundReceiverState->object()` (SelectorGroupCall.h) and `state.boundReceiver.lock()` (NativeApiJsi.mm) both silently return nil -- so every call through it threw "Objective-C selector requires a native receiver" even though the method is being invoked on a live object. Reproduced 100% of the time on cold launch of every itest scenario (including plain `nav-stack`, previously 12/12 clean), isolated away from the react-native-screens adapter and the simulator via: (1) fresh never-booted simulator device still crashed, (2) causally disabling the adapter's only recent change did not stop it, (3) an attached lldb session showed `state.boundReceiver` / `data.boundReceiverState` resolving a dead weak_ptr (strong=0) at the exact throw site. Fix: when the bound receiver has died, fall back to resolving from the call's actual `thisValue` (the live receiver `.method(...)` was invoked on) instead of throwing -- exactly what the unbound path already does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
taggedAppearanceProxyClass's untagged fallback (appearanceProxyCustomizable- ClassFromExactDescription) sends a real Objective-C `-description` message to ANY object on its first property read, as a heuristic to detect UIAppearance proxies. That runs unconditionally inside NativeApiObjectHostObject::get(), so it fires for every property access on every native object, including objects handed to JS reentrantly as callback arguments while native code's own machinery is still on the stack. Root-caused via os_log breadcrumbs bracketing the sheet-detents-custom spike end to end (both in-app and inside the interop bridge itself): the customDetentWithIdentifierResolver resolver block was invoked correctly, and returning a bare CGFloat constant from it always worked -- the block's own return-value marshalling was never broken. The hang was reading ctx.maximumDetentValue: the first property access on the live, UIKit-owned UISheetPresentationControllerDetentResolutionContext object triggers this generic appearance-proxy check, which calls -description on it -- and that deadlocks inside UIKit's own detent-resolution machinery, which is still running on the same call stack. Fix: skip the -description fallback when gNativeCallerThreadEngineCallback- Depth > 0 (already used elsewhere in this file's own call chain to detect exactly this situation). A real UIAppearance proxy is only ever obtained by JS calling an `+appearance`-family method itself -- an outbound call this engine makes, never something delivered inbound as a callback argument -- so the guard never regresses genuine appearance-proxy detection; it only disables an unsafe heuristic for objects that were never appearance proxies to begin with. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extracted from an abandoned React Native architecture branch (PR #46, now being rewritten). These 8 commits are genuine runtime/FFI fixes, nothing RN-specific — any embedder can hit the underlying bugs. No RN code included.
CLI bundle resolution + shutdown hardening — a CLI/test process not launched from a
.appbundle couldn't findapp/index.js; now tries several candidate paths. Also fixes a process-exit crash from unspecified static-destruction order vs. the ObjC runtime.Thread-safe, per-runtime object expandos — a worklet's second
Runtime(own thread, shared bridge) used the same unguarded expando storage as the main runtime, so a value could leak across runtimes. Now keyed per-runtime and mutex-protected.Backend config: callback gate + lazy symbol indexing — new opt-in flags to refuse callbacks once a host is tearing down, and to index class/protocol runtime pointers lazily instead of eagerly.
Associated objects, primitive aliases, class returns, pointer guards —
interop.set/getAssociatedObjectfor state that must survive wrapper churn;NSInteger/BOOL-style aliases;Class-typed returns now resolve correctly; guards against misread-register-as-object derefs.JS-subclass identity & dispatch — fixes
this.superafter wrapper re-seating, inherited-method resolution, and accessor/setter precedence forextend()-built classes.UIAppearance proxy primitives —
[View appearance]returns an opaque proxy that silently no-ops property access; adds real get/set support.Stale bound-receiver crash — a cached bound method (e.g.
view.viewWithTag) tied to a torn-down wrapper crashed every call once its pointer was re-wrapped, even on a live object. Falls back to the live receiver.Stop calling
-descriptionon live callback-argument objects — an appearance-proxy heuristic called-descriptionon every property read, deadlocking inside UIKit's own call stack for objects handed to JS from a callback. Skipped now while inside a callback.