Skip to content

fix(metagen): keep typedef'd CF types for block parameters - #116

Open
NathanWalker wants to merge 484 commits into
android-react-nativefrom
fix/metagen-block-param-typedefs
Open

NathanWalker wants to merge 484 commits into
android-react-nativefrom
fix/metagen-block-param-typedefs

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Split out of #103 (part 3).

Summary

The metadata generator read block parameters from the block's canonical type, which has lost its parameters' typedef names (CMSampleBufferRef becomes struct opaqueCMSampleBuffer *) and with them the CF bridging. Block parameters are now read from the declared (sugared) block type, the same way function, method and delegate parameters already are.

 metadata generator: block parameters
-  read from the canonical type  (CMSampleBufferRef -> struct opaqueCMSampleBuffer *, raw pointer)
+  read from the declared type   (CMSampleBufferRef stays a CF object)

It falls back to the canonical type if the sugared type isn't a block pointer or its arity differs. Neither fallback fires on the current iOS or macOS SDK. C function-pointer callbacks (e.g. CFRunLoopObserverCallBack) are unchanged and still read the canonical type.

Also raises the iOS deployment target from 13.0 to 15.0, in two separate commits. Xcode 27 can't build below 15.0, so without this the iOS run above can't build at all. The change covers the device and simulator targets in NativeScript/CMakeLists.txt, the React Native podspec (and its copy in scripts/test_react_native_ffi_compat.sh), the iOS app template, the libffi build flags and the test runner's TKLiveSync stub. The checked-in libffi archives were built for 13.0 and still link into a 15.0 target.

⚠️ Breaking change (iOS and macOS, once metadata is regenerated)

The generator is shared, so NativeScript iOS apps change too on the next regen:

  • Block parameters typed with CoreFoundation typedefs (CMSampleBufferRef, CGImageRef, CVImageBufferRef, CFErrorRef, SecTrustRef, CFArrayRef, …) arrive in JS as bridged objects instead of interop.Pointer/Reference. A CFStringRef now arrives as a JS string.
  • Typings: about 50 declarations per platform go from interop.PointerConvertible to interop.Object (AVFoundation, CoreMedia, VideoToolbox, Security, CoreText, ImageIO, ReplayKit…). Generic collection blocks (NSArray.enumerateObjectsUsingBlock, NSDictionary.enumerateKeysAndObjectsUsingBlock, diffable data sources…) are now typed with the class's type parameter, a TypeScript-only change that defaults to interop.Object.
  • Migration: pass these values straight to the native APIs that take them; drop pointer-only handling such as .add(), .toNumber(), new interop.Reference(type, ptr), or manual CFRetain/CFRelease of the callback argument.
Regenerated typings iOS (SDK 27.1) macOS
Changed declarations 84 in 17 files 82 in 18 files
CF block params, PointerConvertible → Object 47 decls (53 params) 45 decls (56 params)
Generic block params, Object → type parameter 37 decls (43 params) 33 decls (39 params)
Other none 3: two const T[] params typed interop.Pointer (binary unchanged), and SecTransformSetTransformAction's block now returns interop.Object

Examples: AVCaptureStillImageOutput.captureStillImageAsynchronouslyFromConnectionCompletionHandler (CMSampleBufferRef), AVAssetImageGenerator.generateCGImagesAsynchronouslyForTimesCompletionHandler (CGImageRef), RPScreenRecorder.startCaptureWithHandlerCompletionHandler (CMSampleBufferRef), SecTrustEvaluateAsyncWithError (SecTrustRef, CFErrorRef).

Base and new typings type-check with the same tsc errors (5.6.3), so nothing new.

Relation to #115: the VideoToolbox output-handler case from #103 only queues once metadata is regenerated with this change. With current metadata its CMSampleBufferRef parameter is a raw pointer, so the callback keeps waiting for JS.

Evidence

Both generators went through the same clean pipeline: metadata, GeneratedSignatureDispatch.inc and the TestRunner app metadata were all regenerated, with no checked-in metadata involved.

npm run test:macos npm run test:ios (simulator)
Base generator 716 tests, 0 failures 716 specs, 8 failures
This PR 716 tests, 0 failures 716 specs, 8 failures (the same 8)

The 8 iOS failures are all double/Float64 cases (FunctionWithDouble1, StaticMethodWithDouble1, InstanceMethodWithDouble1, DerivedMethodWithDouble1, functionWithDoublePtr, functionWithFloat64ArrayBuffer, functionWithFloat64ArrayBufferView, NSInvocation_methodWithDouble1), present on the base generator too. The iOS simulator build needs the deployment-target bump below to build under Xcode 27.1.

New regression test in Marshalling/ObjCTypesTests.js ("Block parameters with CF typedefs are marshalled as objects", via CGImageMetadataEnumerateTagsUsingBlock): passes with this generator on both the napi bridge (MACOS_TEST_FFI_BACKEND=napi, 717 tests, 0 failures) and the default V8 bridge (717 tests, 0 failures). It fails with the base generator, where path arrives as an interop.Reference. iOS was not re-run after adding the test.

Merge Danger

Door: one-way for apps (typings and runtime values change on regen)

Blast Radius: every iOS/macOS app that regenerates metadata and uses CF-typed block parameters.

ammarahm-ed and others added 30 commits August 14, 2026 06:16
When a java object is finally freed in Java, we notify the JS side to free the JS counterpart. At this time, the Java object is held weakly in Java too using a WeakReference. While the Java object is weakly held, the WeakReference is strongly kept in a HashMap never getting removed. In this PR we release the WeakReferences too on GC event ensuring that all memory is reclaimed.
We need to ensure the errors created via public API JS_NewError include .stack property.
ammarahm-ed and others added 23 commits September 19, 2026 11:58
Expo installs global.expo as a non-writable property, so exposing the
expo.* Java package under that name threw from CreateTopLevelNamespaces
on Hermes. Fall back to the $-prefixed name already used for packages
that collide with JS keywords.
The react-native target builds the Android tools jars, which compile
against jars stored in Git LFS under platforms/android/test-app/**/libs.
Without lfs on the Apple job's checkout, those resolve to pointer files
and compileKotlin fails with "zip END header not found".
…when it matches its last one

Fabric keeps _props across prepareForRecycle while the container is
cleared, so diffing against _props skipped setNativeViewHandle when a
recycled host was mounted with the handle it last showed, leaving it
empty. Diff against the oldProps argument instead, which is null on a
fresh mount.
fix(react-native/ios): attach a recycled NativeScriptUIView's handle when it matches its last one
The property-getter, round-trip and expando lookup caches identified a
bridge by its address. After a JS reload a new bridge can be allocated
where a destroyed one lived and match its entries: the getter cache then
returned a raw pointer into the freed bridge (EXC_BAD_ACCESS in
preparedObjCInvocationIsInit on the next launch), and cached misses
could hide values that exist.
…act-native.json (#85)

The config plugin and nativescript-rn configure replaced the whole
reactNative.metadata block, so every prebuild or configure run deleted
a hand-written metadata.android.include. That list is what keeps
classes, including autolinked module packages, in filtered Android
release metadata.

Merge into the existing metadata instead, accept metadata.android.include
in the plugin options, and write the file on Android-only prebuilds too.
* fix(react-native): explain a missing native module at import

Keep failing at import when NativeScriptNativeApi is not in the app
binary, but with an error that says the package cannot run in Expo Go
and that a build made before the package was added must be rebuilt,
instead of React Native's generic TurboModuleRegistry invariant.

Drop isAvailable() and the Android-specific init() path.
* fix(android): use $name when a Java package's global name is taken

Check whether the global already has the package's name and, if it
does, expose the package as `$name`, logging a warning. Both the jsi
and napi trees now do the same check, so an existing global is never
overwritten and no engine-specific read-only handling is needed.
With globals enabled, every key of every enum object became a global,
including short member names such as Center, Fill and CloudKit's Zone.
Libraries that feature-detect a global by name were misled: Angular's
zoneless scheduler checks typeof Zone and crashed at bootstrap.

The metadata only stored the shortened member names, so the bridge could
not tell which alias was the real C constant. Metadata format v2 records,
per enum, the prefix and suffix the generator stripped from its members,
and flags members whose stored name is already the full C name. The
reader rebuilds every member's exact C name from that; v1 metadata still
loads but yields no C names.

A shared alias builder (ffi/objc/shared/EnumMemberAliases.h) tags the C
name among each member's aliases and is used by both the JSI bridge and
the Node-API tree. The bridge exposes getEnumGlobalNames(enumName), and
the shared global installer and installGlobals() in
@nativescript/react-native define only those names. The Node-API tree
defines only C names as globals. Short names and other conveniences stay
on the enum object, which also gains the C name as a key, so constants
like MKMapTypeStandard and CKErrorZoneBusy now resolve.
* refactor(android): detach through the worker teardown path

Narrows the reload fix to what React Native needs: the module tracks its
own install, invalidate() detaches, and detaching reuses the teardown a
worker already goes through.

- Runtime.detach() is detachRuntime() (formerly detachWorkerRuntime) plus
  Runtime::DisposeRuntime (formerly DisposeWorkerRuntime), now built in
  the host-runtime flavour too. The separate Runtime::Detach is gone.
- ~Runtime still resets the main-runtime statics, and BuildMetadata still
  builds the tree once.

Dropped, to land separately or not at all: throwing on a failed post to
the JS thread, the GcListener delivery lock and the notifyGc change, the
orphaned-object map, the LRU cache clear, the global-ref delete in
~Runtime, MethodCallbackData ownership, and their napi mirrors.

* refactor(android): name napi's runtime teardown DisposeRuntime too
A GC batch could reach a worker runtime while it was being disposed:
the GC thread took the runtime from the subscriber map, the worker
thread unsubscribed and disposed its native Runtime, and the GC thread
then entered notifyGc for it and crashed (SIGSEGV on the GC thread).

Each subscriber now carries its own lock. A batch is delivered while
holding that lock, and only if the subscriber is still subscribed;
unsubscribe() removes the subscriber, then takes its lock to mark it
unsubscribed. Once unsubscribe() returns, no GC thread is inside that
runtime's notifyGc and none will enter it.

The memory monitor's notifyGc() takes the same per-runtime lock instead
of the listener-wide one, and unsubscribe() no longer takes the
listener-wide lock, so subscribing or unsubscribing one runtime never
waits on a GC delivery into another.
… dispose (#92)

Disposing a runtime (a worker, or a React Native guest runtime) left three
things behind:

- ObjectManager's LRU cache of Java objects: ~LRUCache drops its map without
  calling the evict callback, so every cached weak global ref stayed in the
  process-wide weak reference table. Add LRUCache::clear(), which evicts each
  entry, and call it from ObjectManager's dispose.
- The JNI global ref ~Runtime holds on the Java com.tns.Runtime.
- MethodCallbackData for static methods, extension-only methods and methods
  added from a skipped base class. CtorFuncCache only owns the instance
  method data, so these were never deleted (about 180 per worker that touches
  a handful of classes). MetadataNodeCache now owns them and deletes them in
  onDisposeEnv/onDisposeRuntime.

Applied to both the napi and jsi trees. Split out of #89.
…destroying (#95)

DestroyRuntime set is_destroying before flushing the finalizer queue.
HostObjectProxyPostFinalizer treats is_destroying as "leave the target
handle", a rule carried over from napi, where js_free_napi_env frees
leftover refs. engine:: handles have no such owner, so every queued proxy
cleanup leaked its JS wrapper, and on QuickJS JS_FreeRuntime aborted on
list_empty(&rt->gc_obj_list).

The queue is normally drained on the next looper tick. A worker that calls
close() from onmessage quits its looper first, which drops that tick, so any
proxy collected during the handler was still queued at dispose. terminate()
from the parent arrives after the tick has run.

Flush the queue first, while the runtime is not yet marked destroying.

Fixes #94
s_type_long_operations_cache is process-wide, but each runtime inserts
its entry on init and erases it on dispose from its own thread. Make it a
tns::ConcurrentMap, as MetadataNode's per-runtime cache already is, in
both the napi and jsi trees.

Fixes #93
…bal (#83)

Without strictGlobal the Worklets plugin captures every name a worklet
does not declare into its closure, read on the JS thread when the module
is evaluated, where NativeScript's classes do not exist: a module-level
worklet naming UILabel failed at startup with
"ReferenceError: Property 'UILabel' doesn't exist".

The config plugin and configure CLI now write
['react-native-worklets/plugin', { strictGlobal: true }] and
['babel-preset-expo', { worklets: false }] (so the preset adds no second
copy), upgrade bare entries in an existing config, and warn when an
entry with options lacks strictGlobal. The Babel setup also runs on
Android-only prebuilds.
Block parameter types were read from the canonical function type, which
drops typedef sugar: CMSampleBufferRef became struct opaqueCMSampleBuffer *
and lost its CF bridging, so CF block parameters were emitted as raw
pointers. Read them from the declared block type instead, falling back to
the canonical type if the arity differs.
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

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

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a14f857a-7929-44cb-906e-7f88aded18aa

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
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Xcode 27 no longer builds for iOS deployment targets below 15.0, so the
device and simulator builds of NativeScript.xcframework fail at 13.0.
Match the 15.0 deployment target in NativeScript/CMakeLists.txt: the
React Native podspec, the iOS app template, the libffi build and the
test runner's TKLiveSync stub.
@ammarahm-ed
ammarahm-ed force-pushed the android-react-native branch 2 times, most recently from 4f87f05 to 9633aba Compare November 8, 2026 12:59

This branch has not been deployed

No deployments
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.

4 participants