Skip to content

refactor: organize direct FFI backends#43

Open
DjDeveloperr wants to merge 25 commits into
mainfrom
refactor
Open

refactor: organize direct FFI backends#43
DjDeveloperr wants to merge 25 commits into
mainfrom
refactor

Conversation

@DjDeveloperr
Copy link
Copy Markdown
Collaborator

Summary

  • Reorganize NativeScript/ffi so Hermes owns the public JSI entrypoint, direct-engine bridge internals live under ffi/direct, and shared code is named for what it actually shares.
  • Move direct adapters for V8, JSC, and QuickJS off facebook::jsi naming and onto nativescript::direct while keeping Hermes as the only real JSI backend.
  • Centralize signature-dispatch hashing/lookup support, add direct prepared dispatch for V8/JSC/QuickJS, and wire Hermes/RN TurboModule staging to generated signature dispatch.
  • Update CMake, build scripts, RN podspec packaging, and FFI boundary checks to enforce the new architecture.

Validation

  • ./scripts/check_ffi_boundaries.sh
  • git diff --check
  • ./scripts/build_metadata_generator.sh
  • BUILD_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-jsc
  • BUILD_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-quickjs
  • BUILD_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-v8
  • MACOS_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 skipped
  • MACOS_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-pack

The metadata generation steps still print existing SDK/private-header diagnostics, but the commands exit successfully.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dbca52fe-2538-4ac8-9003-e79cba703f30

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant