Files
react-native/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeEventEmitter.js
T
Christoph Purrer 4bf5575490 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
2026-08-17 10:41:02 -07:00

110 lines
3.5 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {NativeModuleEventEmitterShape} from '../../../CodegenSchema';
const {parseValidUnionType, toPascalCase} = require('../../Utils');
function getEventEmitterTypeObjCType(
eventEmitter: NativeModuleEventEmitterShape,
): string {
const typeAnnotation = eventEmitter.typeAnnotation.typeAnnotation;
switch (typeAnnotation.type) {
case 'StringTypeAnnotation':
return 'NSString *_Nonnull';
case 'StringLiteralTypeAnnotation':
return 'NSString *_Nonnull';
case 'UnionTypeAnnotation':
const validUnionType = parseValidUnionType(typeAnnotation);
switch (validUnionType) {
case 'boolean':
return 'BOOL';
case 'number':
return 'NSNumber *_Nonnull';
case 'object':
return 'NSDictionary *';
case 'string':
return 'NSString *_Nonnull';
default:
validUnionType as empty;
throw new Error(`Unsupported union member type`);
}
case 'NumberTypeAnnotation':
case 'NumberLiteralTypeAnnotation':
return 'NSNumber *_Nonnull';
case 'BooleanTypeAnnotation':
case 'BooleanLiteralTypeAnnotation':
return 'BOOL';
case 'GenericObjectTypeAnnotation':
case 'ObjectTypeAnnotation':
case 'TypeAliasTypeAnnotation':
return 'NSDictionary *';
case 'ArrayTypeAnnotation':
return 'NSArray<id<NSObject>> *';
case 'DoubleTypeAnnotation':
case 'FloatTypeAnnotation':
case 'Int32TypeAnnotation':
case 'VoidTypeAnnotation':
// TODO: Add support for these types
throw new Error(
`Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`,
);
default:
typeAnnotation.type as empty;
throw new Error(
`Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`,
);
}
}
function EventEmitterHeaderTemplate(
eventEmitter: NativeModuleEventEmitterShape,
): string {
return `- (void)emit${toPascalCase(eventEmitter.name)}${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? `:(${getEventEmitterTypeObjCType(eventEmitter)})value`
: ''
};`;
}
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`
: ''
}
{
auto eventEmitterCallback = _eventEmitterCallback;
if (eventEmitterCallback) {
eventEmitterCallback("${eventEmitter.name}", ${
eventEmitter.typeAnnotation.typeAnnotation.type !== 'VoidTypeAnnotation'
? eventEmitter.typeAnnotation.typeAnnotation.type !==
'BooleanTypeAnnotation'
? 'value'
: '[NSNumber numberWithBool:value]'
: 'nil'
});
}
}`;
}
module.exports = {
EventEmitterHeaderTemplate,
EventEmitterImplementationTemplate,
};