Make codegen'd TurboModule event emitters no-op when the emitter callback is absent (#57893) - #57893
Open
christophpurrer wants to merge 1 commit into
Open
Make codegen'd TurboModule event emitters no-op when the emitter callback is absent (#57893)#57893christophpurrer wants to merge 1 commit into
christophpurrer wants to merge 1 commit into
Conversation
|
@christophpurrer has exported this pull request. If you are a Meta employee, you can view the originating Diff in D115130767. |
…back is absent (react#57893) 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
christophpurrer
force-pushed
the
export-D115130767
branch
from
August 11, 2026 15:27
a12fb4b to
6d06919
Compare
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:
The codegen'd TurboModule
emitOn<Event>methods invoked theirEventEmitterCallbackwithout checking it was set. That callback is installed by the generated*SpecJSIconstructor, which runs when JS first looks the module up — so a native module that emits before that point invoked an emptystd::functionon 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:
serializeEventEmitter.js): copies_eventEmitterCallback, calls it only if non-empty.GenerateModuleJavaSpec.js): copies theNullable CxxCallbackImplfield 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
setEventEmitterCallbacklambdas had a separate lifetime bug: they capturedeventEmitterMap_by reference and looked events up withoperator[]. The Java/ObjC module owns the callback and can outlive the C++*SpecJSIthat installed it, so a stale callback dereferenced a dangling map; andoperator[]silently default-inserted a nullshared_ptrfor an unknown event name, which the next line dereferenced. They now capture a copy of the map and use a checkedfind(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, fromGenerateModuleH.js) has no callback to check — it emits througheventEmitterMap_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