Skip to content

Make codegen'd TurboModule event emitters no-op when the emitter callback is absent - #57893

Open
christophpurrer wants to merge 1 commit into
react:mainfrom
christophpurrer:export-D115130767
Open

Make codegen'd TurboModule event emitters no-op when the emitter callback is absent#57893
christophpurrer wants to merge 1 commit into
react:mainfrom
christophpurrer:export-D115130767

Conversation

@christophpurrer

Copy link
Copy Markdown
Contributor

Summary:
The codegen'd TurboModule emitOn<Event> methods invoked their EventEmitterCallback without checking it was set. That callback is installed by the generated *SpecJSI constructor, which runs when JS first looks the module up — so a native module that emits before that point invoked an empty std::function on iOS (std::bad_function_call) or a null field on Android (NPE). Native code commonly holds the module instance and pushes events well before any JS surface mounts, so call sites had to wrap every emit in a try/catch to stay crash-free.

Both generators now read the callback into a local and no-op when it is absent:

  • ObjC++ (serializeEventEmitter.js): copies _eventEmitterCallback, calls it only if non-empty.
  • Java (GenerateModuleJavaSpec.js): copies the Nullable CxxCallbackImpl field and null-checks it.

The local is for readability, not synchronization: the callback is installed once and never cleared, and Java reference reads are already atomic.

The setEventEmitterCallback lambdas had a separate lifetime bug: they captured eventEmitterMap_ by reference and looked events up with operator[]. The Java/ObjC module owns the callback and can outlive the C++ *SpecJSI that installed it, so a stale callback dereferenced a dangling map; and operator[] silently default-inserted a null shared_ptr for an unknown event name, which the next line dereferenced. They now capture a copy of the map and use a checked find (serializeModule.js, JavaTurboModule.cpp). Every emitter is registered before the callback is installed, so the copy is complete.

Note the scope of the guarantee: it covers the ObjC++ and Java generators, which route through an EventEmitterCallback. A C++-only TurboModule (<Module>CxxSpec, from GenerateModuleH.js) has no callback to check — it emits through eventEmitterMap_ entries its own constructor registers — so the "emit unconditionally" guidance is about the callback, not about emitting before construction finishes.

Changelog:
[General][Fixed] - TurboModule event emitters no longer throw when an event is emitted before the emitter callback is installed

Differential Revision: D115130767

…back is absent

Summary:
The codegen'd TurboModule `emitOn<Event>` methods invoked their `EventEmitterCallback` without checking it was set. That callback is installed by the generated `*SpecJSI` constructor, which runs when JS first looks the module up — so a native module that emits before that point invoked an empty `std::function` on iOS (`std::bad_function_call`) or a null field on Android (NPE). Native code commonly holds the module instance and pushes events well before any JS surface mounts, so call sites had to wrap every emit in a try/catch to stay crash-free.

Both generators now read the callback into a local and no-op when it is absent:

- ObjC++ (`serializeEventEmitter.js`): copies `_eventEmitterCallback`, calls it only if non-empty.
- Java (`GenerateModuleJavaSpec.js`): copies the `Nullable CxxCallbackImpl` field and null-checks it.

The local is for readability, not synchronization: the callback is installed once and never cleared, and Java reference reads are already atomic.

The `setEventEmitterCallback` lambdas had a separate lifetime bug: they captured `eventEmitterMap_` by reference and looked events up with `operator[]`. The Java/ObjC module owns the callback and can outlive the C++ `*SpecJSI` that installed it, so a stale callback dereferenced a dangling map; and `operator[]` silently default-inserted a null `shared_ptr` for an unknown event name, which the next line dereferenced. They now capture a copy of the map and use a checked `find` (`serializeModule.js`, `JavaTurboModule.cpp`). Every emitter is registered before the callback is installed, so the copy is complete.

Note the scope of the guarantee: it covers the ObjC++ and Java generators, which route through an `EventEmitterCallback`. A C++-only TurboModule (`<Module>CxxSpec`, from `GenerateModuleH.js`) has no callback to check — it emits through `eventEmitterMap_` entries its own constructor registers — so the "emit unconditionally" guidance is about the callback, not about emitting before construction finishes.

Changelog:
[General][Fixed] - TurboModule event emitters no longer throw when an event is emitted before the emitter callback is installed

Differential Revision: D115130767
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 11, 2026
@meta-codesync

meta-codesync Bot commented Aug 11, 2026

Copy link
Copy Markdown

@christophpurrer has exported this pull request. If you are a Meta employee, you can view the originating Diff in D115130767.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant