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

Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57893

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

Reviewed By: javache

Differential Revision: D115130767

fbshipit-source-id: 7d7b9a706b8983968e3e1207b22c66322f04a223
This commit is contained in:
Christoph Purrer
2026-08-17 10:41:02 -07:00
committed by meta-codesync[bot]
parent c467843ed0
commit 4bf5575490
7 changed files with 174 additions and 39 deletions
@@ -82,16 +82,25 @@ function EventEmitterTemplate(
eventEmitter: NativeModuleEventEmitterShape,
imports: Set<string>,
): string {
imports.add('com.facebook.react.bridge.CxxCallbackImpl');
// mEventEmitterCallback is set from JNI by configureEventEmitterCallback(),
// which the generated SpecJSI constructor calls when JS first looks the module
// up. Emitting before that would hit a null field, so the emitted code no-ops
// instead. The local is for readability: reference reads are already atomic,
// and the field is never reset to null.
return ` protected final void emit${toPascalCase(eventEmitter.name)}(${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? `${translateEventEmitterTypeToJavaType(eventEmitter, imports)} value`
: ''
}) {
mEventEmitterCallback.invoke("${eventEmitter.name}"${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? ', value'
: ''
});
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke("${eventEmitter.name}"${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? ', value'
: ''
});
}
}`;
}
@@ -78,20 +78,28 @@ function EventEmitterHeaderTemplate(
function EventEmitterImplementationTemplate(
eventEmitter: NativeModuleEventEmitterShape,
): string {
// _eventEmitterCallback is installed by the generated SpecJSI constructor,
// which runs when JS first looks the module up. Emitting before that would
// call an empty std::function, so the emitted code no-ops instead. The local
// copy is for readability; it does not synchronize against a concurrent
// install.
return `- (void)emit${toPascalCase(eventEmitter.name)}${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? `:(${getEventEmitterTypeObjCType(eventEmitter)})value`
: ''
}
{
_eventEmitterCallback("${eventEmitter.name}", ${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? eventEmitter.typeAnnotation.typeAnnotation.type !==
'BooleanTypeAnnotation'
? 'value'
: '[NSNumber numberWithBool:value]'
: 'nil'
});
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback("${eventEmitter.name}", ${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? eventEmitter.typeAnnotation.typeAnnotation.type !==
'BooleanTypeAnnotation'
? 'value'
: '[NSNumber numberWithBool:value]'
: 'nil'
});
}
}`;
}
@@ -83,10 +83,17 @@ namespace facebook::react {
.join('')
: ''
}${
// The callback outlives this module, so it captures a copy of the map
// instead of referencing eventEmitterMap_. Every emitter is registered
// directly above, so the copy is complete; an emitter registered after
// construction would not be reachable from the callback.
eventEmitters.length > 0
? `
setEventEmitterCallback([&](const std::string &name, id value) {
static_cast<AsyncEventEmitter<id> &>(*eventEmitterMap_[name]).emit(value);
setEventEmitterCallback([eventEmitterMap = eventEmitterMap_](const std::string &name, id value) {
auto it = eventEmitterMap.find(name);
if (it != eventEmitterMap.end() && it->second) {
static_cast<AsyncEventEmitter<id> &>(*it->second).emit(value);
}
});`
: ''
}
@@ -229,6 +229,7 @@ Map {
package com.facebook.fbreact.specs;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.CxxCallbackImpl;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
@@ -250,27 +251,45 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo
}
protected final void emitOnEvent1() {
mEventEmitterCallback.invoke(\\"onEvent1\\");
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"onEvent1\\");
}
}
protected final void emitOnEvent2(String value) {
mEventEmitterCallback.invoke(\\"onEvent2\\", value);
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"onEvent2\\", value);
}
}
protected final void emitOnEvent3(double value) {
mEventEmitterCallback.invoke(\\"onEvent3\\", value);
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"onEvent3\\", value);
}
}
protected final void emitOnEvent4(boolean value) {
mEventEmitterCallback.invoke(\\"onEvent4\\", value);
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"onEvent4\\", value);
}
}
protected final void emitOnEvent5(ReadableMap value) {
mEventEmitterCallback.invoke(\\"onEvent5\\", value);
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"onEvent5\\", value);
}
}
protected final void emitOnEvent6(ReadableArray value) {
mEventEmitterCallback.invoke(\\"onEvent6\\", value);
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"onEvent6\\", value);
}
}
@ReactMethod
@@ -583,6 +602,7 @@ Map {
package com.facebook.fbreact.specs;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.CxxCallbackImpl;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
@@ -602,7 +622,10 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo
}
protected final void emitLiteralEvent(String value) {
mEventEmitterCallback.invoke(\\"literalEvent\\", value);
CxxCallbackImpl eventEmitterCallback = mEventEmitterCallback;
if (eventEmitterCallback != null) {
eventEmitterCallback.invoke(\\"literalEvent\\", value);
}
}
@ReactMethod(isBlockingSynchronousMethod = true)
@@ -326,27 +326,45 @@ Map {
@implementation NativeSampleTurboModuleSpecBase
- (void)emitOnEvent1
{
_eventEmitterCallback(\\"onEvent1\\", nil);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"onEvent1\\", nil);
}
}
- (void)emitOnEvent2:(NSString *_Nonnull)value
{
_eventEmitterCallback(\\"onEvent2\\", value);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"onEvent2\\", value);
}
}
- (void)emitOnEvent3:(NSNumber *_Nonnull)value
{
_eventEmitterCallback(\\"onEvent3\\", value);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"onEvent3\\", value);
}
}
- (void)emitOnEvent4:(BOOL)value
{
_eventEmitterCallback(\\"onEvent4\\", [NSNumber numberWithBool:value]);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"onEvent4\\", [NSNumber numberWithBool:value]);
}
}
- (void)emitOnEvent5:(NSDictionary *)value
{
_eventEmitterCallback(\\"onEvent5\\", value);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"onEvent5\\", value);
}
}
- (void)emitOnEvent6:(NSArray<id<NSObject>> *)value
{
_eventEmitterCallback(\\"onEvent6\\", value);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"onEvent6\\", value);
}
}
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper
@@ -373,8 +391,11 @@ namespace facebook::react {
eventEmitterMap_[\\"onEvent4\\"] = std::make_shared<AsyncEventEmitter<id>>();
eventEmitterMap_[\\"onEvent5\\"] = std::make_shared<AsyncEventEmitter<id>>();
eventEmitterMap_[\\"onEvent6\\"] = std::make_shared<AsyncEventEmitter<id>>();
setEventEmitterCallback([&](const std::string &name, id value) {
static_cast<AsyncEventEmitter<id> &>(*eventEmitterMap_[name]).emit(value);
setEventEmitterCallback([eventEmitterMap = eventEmitterMap_](const std::string &name, id value) {
auto it = eventEmitterMap.find(name);
if (it != eventEmitterMap.end() && it->second) {
static_cast<AsyncEventEmitter<id> &>(*it->second).emit(value);
}
});
}
} // namespace facebook::react
@@ -734,7 +755,10 @@ Map {
@implementation NativeSampleTurboModuleSpecBase
- (void)emitLiteralEvent:(NSString *_Nonnull)value
{
_eventEmitterCallback(\\"literalEvent\\", value);
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback(\\"literalEvent\\", value);
}
}
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper
@@ -756,8 +780,11 @@ namespace facebook::react {
methodMap_[\\"getStringLiteral\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getStringLiteral};
eventEmitterMap_[\\"literalEvent\\"] = std::make_shared<AsyncEventEmitter<id>>();
setEventEmitterCallback([&](const std::string &name, id value) {
static_cast<AsyncEventEmitter<id> &>(*eventEmitterMap_[name]).emit(value);
setEventEmitterCallback([eventEmitterMap = eventEmitterMap_](const std::string &name, id value) {
auto it = eventEmitterMap.find(name);
if (it != eventEmitterMap.end() && it->second) {
static_cast<AsyncEventEmitter<id> &>(*it->second).emit(value);
}
});
}
} // namespace facebook::react