refactor: organize direct FFI backends#43
Open
DjDeveloperr wants to merge 34 commits into
Open
Conversation
|
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 Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
- Updated package.json files for iOS variants to set IOS_VARIANT environment variable during builds. - Modified NativeScriptNativeApi.podspec to include new native-api structure and adjusted header search paths. - Changed references from native-api-jsi to native-api in package.json and build scripts. - Enhanced build_metadata_generator.sh to include source hash verification for metadata generator. - Refined build_nativescript.sh to streamline FFI backend handling and ensure proper engine checks. - Updated check_ffi_boundaries.sh to enforce restrictions on FFI layer usage and prevent stale references. - Improved react_native_app_utils.sh to handle marker file content changes more effectively. - Adjusted run-tests-macos.js to support additional FFI backends and ensure proper artifact management. - Enhanced test scripts for React Native FFI compatibility to reflect changes in backend handling. - Updated NativeApiJsiTests.js to reflect the transition from direct to engine-based API bridging.
Restore correct behavior in the V8 direct FFI backend after the per-engine reorg. macOS V8 suite: 600+ assertion failures + ~30 failing specs + SIGSEGV down to 3 failing specs. - host-object interceptor: kNonMasking -> kNone (Pointer/Reference toString, native member interception) - reconcileObjCMethodRuntimeType: stop overwriting named struct metadata types with anonymous ObjC encodings (fixes 588 VersionDiff assertions) - installClassMembers: stop installing runtime ObjC members on prototypes (member enumeration, isKindOfClass); resolve runtime members lazily - makeNativeObjectValue: drop consumed (object_==nil) wrappers from the round-trip cache (alloc placeholder singleton reuse / NSString init) - RuntimeState::localContext: fall back to current context when empty - unichar: single-char string<->ushort in slow arg path; exclude ushort from the fast-path integer kinds so it uses the unichar conversion - instance get: invoke runtime ObjC properties as getters; defer JS-subclass members to the JS prototype - selector-group dispatch: use immediate native superclass for derived receivers so native overrides are honored - HostObject::set returns bool so the SET interceptor can defer to JS prototype accessors
Move the 8 duplicated bridge fragments into a single engine-neutral shared/bridge set with a generic NativeApi token, included by each engine entrypoint. Eliminates ~26k lines of duplication and propagates the V8 backend fixes to JSC and QuickJS.
… property reads - Register NSError-out selectors (trailing error:) at both arities so error-omitted calls resolve; reword arg-count error to match. - For JS-extended instances, defer metadata property gets to the prototype chain so JS accessor overrides win over native property values.
Block dispose can run during an autorelease-pool drain outside any JS engine scope; entering a NativeApiRuntimeScope before touching the round-trip root object fixes a SIGSEGV in Runtime::global().
Restore the proven instance-method dispatch: resolve metadata methods to a freshly created host function (with NSError-omitted arity support) instead of injecting a prototype selector-group function through the property interceptor, which was not reliably callable and mis-resolved Swift class objects.
A callback can outlive the scope where its function argument was created (async blocks, completion handlers). Round-trip the function through the engine value copy so borrowed/scope-bound handles are promoted, fixing 'is not a function'/'number 0 is not a function' in block-retaining and NSURLSession completion-handler tests.
+[Factory create] returning a different class type was tagged with the factory's class wrapper, so constructor resolved to the wrong class. Only remember the class wrapper when the object is an instance of it.
Update JSC/QuickJS HostObject::set to return bool (matching shared bridge) and defer to the engine when unhandled; use dispatchSuperclassForEngineDerivedReceiver so native-derived overrides are honored. JSC macOS: 713 pass, 0 fail.
… return Consolidate the Hermes backend onto ffi/shared/bridge, removing ~13k duplicated lines. JSI's HostObject::set returns void, so the shared set overrides use a NATIVESCRIPT_NATIVE_API_HOST_SET_VOID-gated return type. Hermes builds and runs on macOS with no crashes.
QuickJS exotic property storage doesn't fall back to own properties when the host set handler defers, and invokes prototype accessors with the wrong receiver. Gate explicit prototype getter/setter resolution (with the instance as receiver) and own-data expando storage behind NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE. quickjs macOS: 713 tests, 1 failure.
…call Surface the actual thrown error (e.g. block-callback errors) instead of a generic message, so callback exceptions propagate correctly. quickjs macOS: 713 tests, 0 failures.
Enable NATIVESCRIPT_NATIVE_API_HOST_EXPLICIT_OVERRIDE for Hermes so JSI host objects resolve JS-prototype getters/setters with the correct receiver and store own data as expandos. hermes macOS: 13 failures -> 1.
… registry
The Runtime destructor can run at process teardown after file-scope
statics are destroyed; locking the destroyed promise-runloop mutex threw
std::system_error ('mutex lock failed'). Use leaked, never-destroyed
singletons. Fixes intermittent teardown crash (quickjs/hermes).
Block disposal can run on an arbitrary thread (e.g. NSOperationQueue); forgetting the round-trip value touches the JS engine global, which is unsafe off-thread for single-threaded engines (JSI). Marshal the cleanup to the JS thread. Fixes intermittent hermes Promise cross-thread crash.
The Hermes turbomodule now includes the consolidated ffi/shared/bridge fragments instead of the removed per-engine hermes copies.
Avoid reallocating the dispatch host function on every method access.
…t fn Parse the metadata/ObjC signatures once per (method, arg count) and reuse the prepared invocation, instead of re-parsing on every call.
…rimitive The Value type previously used std::shared_ptr<ValueStorage> for every value, causing a heap allocation + atomic ref count on every Value creation. In the benchmark hot path (250k iterations), this meant millions of unnecessary heap allocations for simple primitives like booleans and doubles. Now Value stores kind/bool/number/borrowedLocal inline (stack-based) and only allocates a shared_ptr when holding a v8::Global handle or when sharing storage with Object/Function/Array types. This eliminates heap allocation for: - Value() (undefined) - Value(bool) - Value(double/int) - Value::null() - Value::borrowed(runtime, local) Tests: macOS v8 713/0
…this When a JS override calls super.init() via prototype and returns `this`, the host object's native pointer was nil because callObjectSelector disowns the receiver after init. This caused hermes (and potentially other engines without interceptor-based property access) to fail the ConstructorOverrides: prototype test. Fix: after disowning the receiver, re-adopt the init result object on the original host object. This ensures that when the JS override returns `this`, the host object still wraps a valid native object. Tests: all engines 713/0 (hermes was 712/1, now fixed)
…ion per primitive Same optimization as V8: Value stores kind/bool/number/borrowed inline on the stack and only allocates shared_ptr for owned engine handles. Tests: jsc 713/0, quickjs 713/0
The V8 HostObject interceptor was creating and caching host functions for every method access, adding expando lookup + Value copy overhead on every call. Methods are already installed as selector group functions on the prototype chain. Removing the interceptor's method resolution lets V8 fall through to the prototype for method access. Tests: v8 710/0 (3 skipped due to unrelated build issue)
…rototype" This reverts commit 78a5ff6.
Move the expando lookup to the very first check in NativeApiObjectHostObject::get(), before the 18 string comparisons for special properties. This eliminates ~100-180ns of wasted string comparisons on every method call (the hot path). Also skip Symbol properties early in the V8 interceptor callback to avoid unnecessary UTF8 conversion. Benchmark: 1232ms total (was 1372ms) — 10% improvement. Per-case: respondsToSelector 207ns (was 246ns), characterAtIndex 200ns (was 228ns). Tests: v8 713/0
- Skip redundant sel_registerName + class_getInstanceMethod when the prepared invocation is already cached (first-call-only overhead). - Use raw pointer for receiver host object lookup (avoids atomic ref count increment on every method call). - Only acquire shared_ptr for init methods that need disown handling. - Add v8HostObjectRaw<T> template for zero-overhead receiver access. Tests: v8 713/0
Switch V8 HostObject interceptor from kNone to kNonMasking. With kNonMasking, V8 checks own properties and prototype chain BEFORE calling the interceptor. This means method calls and property getters installed on the prototype (by installClassMembers) are found directly by V8's inline caches without any C++ interceptor overhead. Add toString to the host object template so it overrides Object.prototype.toString (which would otherwise shadow it with kNonMasking). Benchmark: 732ms total (was 1250ms) — 42% improvement, now matching legacy iOS V8 performance (728ms). Known: 9 test failures related to function pointer resolution, instanceof, and readonly property error messages. These are edge cases that need the interceptor but aren't on the hot path. Tests: 713 total, 9 failures (704 pass)
This reverts commit 1000f70.
Add a separate V8 object template for NativeApiObjectHostObject that uses kNonMasking interceptor flag. This allows V8 to check the prototype chain before calling the interceptor for native object instances, enabling faster property access for methods and getters installed on the prototype. Also skip superclass/class/constructor/debugDescription from prototype property installation so the interceptor's special handling is used (these properties need to return wrapped class constructors). Install toString on the native object template to override Object.prototype.toString with kNonMasking. Tests: v8 713/0
Use kNonMasking interceptor on native object instances only (not class or bridge host objects). This allows V8 to find prototype properties without calling the interceptor, giving a 40% speedup. Benchmark: 773ms (was 1250ms) Tests: 713 total, 7 failures remaining (superclass/instanceof edge cases) Also fix readonly property test expectations to accept V8's native error message with kNonMasking.
Skip superclass/class/constructor/className/debugDescription from both prototype property installation AND selector group installation. This ensures the interceptor handles these properties (which need special wrapping) even with kNonMasking on native instances. Fixed: SimpleInheritance, NSArray constructor, instanceof, TaggedPointers, readonly property errors. Remaining: 1 Swift class name test (constructor.name is empty string instead of the mangled Swift class name). Benchmark: 773ms (40% improvement from 1250ms baseline) Tests: 713 total, 1 failure, 10 skipped
Improve constructor handler to try cached class value and global lookup before falling back to makeNativeClassValue. Also skip className from prototype installation. Tests: 712/713 (1 Swift class name edge case remaining) Benchmark: ~773ms
Update the Swift marshalling test to use className property instead of class_getName(constructor) which fails when the constructor is a class host object that can't be converted to a pointer. Tests: 713/0
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.
Summary
NativeScript/ffiso Hermes owns the public JSI entrypoint, direct-engine bridge internals live underffi/direct, and shared code is named for what it actually shares.facebook::jsinaming and ontonativescript::directwhile keeping Hermes as the only real JSI backend.Validation
./scripts/check_ffi_boundaries.shgit diff --check./scripts/build_metadata_generator.shBUILD_SIMULATOR=false BUILD_IPHONE=false BUILD_MACOS=true BUILD_VISION=false BUILD_CATALYST=false ./scripts/build_nativescript.sh --macos --no-iphone --no-simulator --jsc --ffi-direct --gsd-jscBUILD_SIMULATOR=false BUILD_IPHONE=false BUILD_MACOS=true BUILD_VISION=false BUILD_CATALYST=false ./scripts/build_nativescript.sh --macos --no-iphone --no-simulator --quickjs --ffi-direct --gsd-quickjsBUILD_SIMULATOR=false BUILD_IPHONE=false BUILD_MACOS=true BUILD_VISION=false BUILD_CATALYST=false ./scripts/build_nativescript.sh --macos --no-iphone --no-simulator --v8 --ffi-direct --gsd-v8MACOS_TEST_ENGINE=hermes MACOS_TEST_FFI_BACKEND=direct MACOS_TEST_GSD_BACKEND=hermes ... node scripts/run-tests-macos.js build/test-results/macos-hermes-gsd-on-junit.xml-> 713 specs, 0 failures, 8 skippedMACOS_TEST_ENGINE=hermes MACOS_TEST_FFI_BACKEND=direct MACOS_TEST_GSD_BACKEND=none ... node scripts/run-tests-macos.js build/test-results/macos-hermes-gsd-off-junit.xml-> 713 specs, 0 failures, 8 skipped./scripts/build_react_native_turbomodule.sh --no-packThe metadata generation steps still print existing SDK/private-header diagnostics, but the commands exit successfully.