mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
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
110 lines
3.5 KiB
JavaScript
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,
|
|
};
|