Skip to content

Commit ab2ea64

Browse files
christophpurrermeta-codesync[bot]
authored andcommitted
Align TurboModule EventEmitter payload types across platforms (#58063)
Summary: Pull Request resolved: #58063 Codegen's `EventEmitter<T>` support for TurboModules diverged between platforms in two ways: - The explicit number types `Double`, `Float` and `Int32` worked as emitter payloads on Android and C++, but iOS rejected them at codegen time (plain `number` worked). iOS now maps all of them to `NSNumber *_Nonnull`, matching how plain `number` is already emitted. - `ArrayBuffer` was rejected on Android and iOS, but the C++ generator silently accepted it and produced a `jsi::ArrayBuffer` emitter. `ArrayBuffer` is not emittable on any platform — Android emitters always carry a `folly::dynamic` payload, which cannot hold raw bytes — and the schema type `NativeModuleEventEmitterBaseTypeAnnotation` already excluded it. It is now rejected everywhere. `ArrayBuffer` payloads are rejected in the shared parser, so Flow and TypeScript specs produce the same error, and the three generators keep an equivalent guard so schemas that are constructed without going through the parser fail the same way. `ArrayBuffer` remains supported as a method argument and as a synchronous return value. Changelog: [iOS][Added] - Support `Double`, `Float` and `Int32` payloads for TurboModule `EventEmitter`s [General][Breaking] - Reject `ArrayBuffer` as a TurboModule `EventEmitter` payload on all platforms Reviewed By: GijsWeterings Differential Revision: D116952150 fbshipit-source-id: e148a394c4916b3ebfbd3f559b190a5910b29de1
1 parent 2c4278d commit ab2ea64

23 files changed

Lines changed: 441 additions & 10 deletions

packages/react-native-codegen/src/generators/modules/GenerateModuleH.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const {
4040
getModules,
4141
isArrayRecursiveMember,
4242
isDirectRecursiveMember,
43+
throwIfUnsupportedEventEmitterPayload,
4344
} = require('./Utils');
4445

4546
type FilesOutput = Map<string, string>;
@@ -638,6 +639,11 @@ function translateEventEmitterToCpp(
638639
resolveAlias: AliasResolver,
639640
enumMap: NativeModuleEnumMap,
640641
): EventEmitterCpp {
642+
throwIfUnsupportedEventEmitterPayload(
643+
eventEmitter.name,
644+
eventEmitter.typeAnnotation.typeAnnotation,
645+
);
646+
641647
const isVoidTypeAnnotation =
642648
eventEmitter.typeAnnotation.typeAnnotation.type === 'VoidTypeAnnotation';
643649
const templateName = `${toPascalCase(eventEmitter.name)}Type`;

packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const {parseValidUnionType, toPascalCase} = require('../Utils');
2828
const {
2929
createAliasResolver,
3030
getModules,
31+
throwIfUnsupportedEventEmitterPayload,
3132
throwIfUnsupportedPromiseArrayBuffer,
3233
} = require('./Utils');
3334

@@ -141,6 +142,9 @@ function translateEventEmitterTypeToJavaType(
141142
imports: Set<string>,
142143
): string {
143144
const typeAnnotation = eventEmitter.typeAnnotation.typeAnnotation;
145+
146+
throwIfUnsupportedEventEmitterPayload(eventEmitter.name, typeAnnotation);
147+
144148
switch (typeAnnotation.type) {
145149
case 'StringTypeAnnotation':
146150
return 'String';
@@ -179,12 +183,8 @@ function translateEventEmitterTypeToJavaType(
179183
case 'ArrayTypeAnnotation':
180184
imports.add('com.facebook.react.bridge.ReadableArray');
181185
return 'ReadableArray';
182-
case 'DoubleTypeAnnotation':
183-
case 'FloatTypeAnnotation':
184-
case 'Int32TypeAnnotation':
185186
case 'VoidTypeAnnotation':
186-
case 'ArrayBufferTypeAnnotation':
187-
// TODO: Add support for these types
187+
// Void emitters take no argument, so the caller never asks for a type.
188188
throw new Error(
189189
`Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`,
190190
);

packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeEventEmitter.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
import type {NativeModuleEventEmitterShape} from '../../../CodegenSchema';
1212

1313
const {parseValidUnionType, toPascalCase} = require('../../Utils');
14+
const {throwIfUnsupportedEventEmitterPayload} = require('../Utils');
1415

1516
function getEventEmitterTypeObjCType(
1617
eventEmitter: NativeModuleEventEmitterShape,
1718
): string {
1819
const typeAnnotation = eventEmitter.typeAnnotation.typeAnnotation;
1920

21+
throwIfUnsupportedEventEmitterPayload(eventEmitter.name, typeAnnotation);
22+
2023
switch (typeAnnotation.type) {
2124
case 'StringTypeAnnotation':
2225
return 'NSString *_Nonnull';
@@ -39,6 +42,9 @@ function getEventEmitterTypeObjCType(
3942
}
4043
case 'NumberTypeAnnotation':
4144
case 'NumberLiteralTypeAnnotation':
45+
case 'DoubleTypeAnnotation':
46+
case 'FloatTypeAnnotation':
47+
case 'Int32TypeAnnotation':
4248
return 'NSNumber *_Nonnull';
4349
case 'BooleanTypeAnnotation':
4450
case 'BooleanLiteralTypeAnnotation':
@@ -49,11 +55,8 @@ function getEventEmitterTypeObjCType(
4955
return 'NSDictionary *';
5056
case 'ArrayTypeAnnotation':
5157
return 'NSArray<id<NSObject>> *';
52-
case 'DoubleTypeAnnotation':
53-
case 'FloatTypeAnnotation':
54-
case 'Int32TypeAnnotation':
5558
case 'VoidTypeAnnotation':
56-
// TODO: Add support for these types
59+
// Void emitters take no argument, so both callers skip this function.
5760
throw new Error(
5861
`Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`,
5962
);

packages/react-native-codegen/src/generators/modules/Utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,29 @@ function throwIfUnsupportedPromiseArrayBuffer(
118118
}
119119
}
120120

121+
// ArrayBuffer is not emittable on any platform: Android emitters always carry a
122+
// folly::dynamic payload, which cannot hold raw bytes, and neither the ObjC nor
123+
// the C++ emitter contract can hand out a buffer that outlives the emit call.
124+
// The parser rejects this too; the guard here also covers schemas built without
125+
// going through the parser.
126+
function throwIfUnsupportedEventEmitterPayload(
127+
eventEmitterName: string,
128+
typeAnnotation: NativeModuleTypeAnnotation,
129+
): void {
130+
if (typeAnnotation.type === 'ArrayBufferTypeAnnotation') {
131+
throw new Error(
132+
`Unsupported eventType for ${eventEmitterName}. Found: ${typeAnnotation.type}. ` +
133+
'ArrayBuffer is not supported as an EventEmitter payload on any platform. ' +
134+
'Pass the ArrayBuffer through a method instead.',
135+
);
136+
}
137+
}
138+
121139
module.exports = {
122140
createAliasResolver,
123141
getModules,
124142
isDirectRecursiveMember,
125143
isArrayRecursiveMember,
144+
throwIfUnsupportedEventEmitterPayload,
126145
throwIfUnsupportedPromiseArrayBuffer,
127146
};

packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,36 @@ const EVENT_EMITTER_MODULES: SchemaType = {
130130
},
131131
},
132132
},
133+
{
134+
name: 'onEvent7',
135+
optional: false,
136+
typeAnnotation: {
137+
type: 'EventEmitterTypeAnnotation',
138+
typeAnnotation: {
139+
type: 'DoubleTypeAnnotation',
140+
},
141+
},
142+
},
143+
{
144+
name: 'onEvent8',
145+
optional: false,
146+
typeAnnotation: {
147+
type: 'EventEmitterTypeAnnotation',
148+
typeAnnotation: {
149+
type: 'FloatTypeAnnotation',
150+
},
151+
},
152+
},
153+
{
154+
name: 'onEvent9',
155+
optional: false,
156+
typeAnnotation: {
157+
type: 'EventEmitterTypeAnnotation',
158+
typeAnnotation: {
159+
type: 'Int32TypeAnnotation',
160+
},
161+
},
162+
},
133163
],
134164
methods: [
135165
{

packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleH-test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,37 @@
1010

1111
'use strict';
1212

13+
import type {SchemaType} from '../../../CodegenSchema';
14+
1315
const fixtures = require('../__test_fixtures__/fixtures.js');
1416
const generator = require('../GenerateModuleH.js');
1517

18+
const ARRAY_BUFFER_EVENT_EMITTER_SCHEMA: SchemaType = {
19+
modules: {
20+
NativeSampleTurboModule: {
21+
type: 'NativeModule',
22+
aliasMap: {},
23+
enumMap: {},
24+
spec: {
25+
eventEmitters: [
26+
{
27+
name: 'onBuffer',
28+
optional: false,
29+
typeAnnotation: {
30+
type: 'EventEmitterTypeAnnotation',
31+
typeAnnotation: {
32+
type: 'ArrayBufferTypeAnnotation',
33+
},
34+
},
35+
},
36+
],
37+
methods: [],
38+
},
39+
moduleName: 'SampleTurboModule',
40+
},
41+
},
42+
};
43+
1644
describe('GenerateModuleH', () => {
1745
Object.keys(fixtures)
1846
.sort()
@@ -29,4 +57,13 @@ describe('GenerateModuleH', () => {
2957
).toMatchSnapshot();
3058
});
3159
});
60+
61+
it('throws for an EventEmitter with an ArrayBuffer payload', () => {
62+
expect(() =>
63+
generator.generate(
64+
'array_buffer_event_emitter_throws',
65+
ARRAY_BUFFER_EVENT_EMITTER_SCHEMA,
66+
),
67+
).toThrow(/ArrayBuffer is not supported as an EventEmitter payload/);
68+
});
3269
});

packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,40 @@ describe('GenerateModuleHObjCpp', () => {
7171
),
7272
).toThrow(/Promise<ArrayBuffer> is not supported/);
7373
});
74+
75+
it('throws for an EventEmitter with an ArrayBuffer payload', () => {
76+
const schema: SchemaType = {
77+
modules: {
78+
NativeSampleTurboModule: {
79+
type: 'NativeModule',
80+
aliasMap: {},
81+
enumMap: {},
82+
spec: {
83+
eventEmitters: [
84+
{
85+
name: 'onBuffer',
86+
optional: false,
87+
typeAnnotation: {
88+
type: 'EventEmitterTypeAnnotation',
89+
typeAnnotation: {
90+
type: 'ArrayBufferTypeAnnotation',
91+
},
92+
},
93+
},
94+
],
95+
methods: [],
96+
},
97+
moduleName: 'SampleTurboModule',
98+
},
99+
},
100+
};
101+
expect(() =>
102+
generator.generate(
103+
'array_buffer_event_emitter_throws',
104+
schema,
105+
'com.facebook.fbreact.specs',
106+
false,
107+
),
108+
).toThrow(/ArrayBuffer is not supported as an EventEmitter payload/);
109+
});
74110
});

packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,35 @@ describe('GenerateModuleJavaSpec', () => {
6464
generator.generate('array_buffer_promise_throws', schema),
6565
).toThrow(/Promise<ArrayBuffer> is not supported/);
6666
});
67+
68+
it('throws for an EventEmitter with an ArrayBuffer payload', () => {
69+
const schema: SchemaType = {
70+
modules: {
71+
NativeSampleTurboModule: {
72+
type: 'NativeModule',
73+
aliasMap: {},
74+
enumMap: {},
75+
spec: {
76+
eventEmitters: [
77+
{
78+
name: 'onBuffer',
79+
optional: false,
80+
typeAnnotation: {
81+
type: 'EventEmitterTypeAnnotation',
82+
typeAnnotation: {
83+
type: 'ArrayBufferTypeAnnotation',
84+
},
85+
},
86+
},
87+
],
88+
methods: [],
89+
},
90+
moduleName: 'SampleTurboModule',
91+
},
92+
},
93+
};
94+
expect(() =>
95+
generator.generate('array_buffer_event_emitter_throws', schema),
96+
).toThrow(/ArrayBuffer is not supported as an EventEmitter payload/);
97+
});
6798
});

packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,9 @@ protected:
10831083
eventEmitterMap_[\\"onEvent4\\"] = std::make_shared<AsyncEventEmitter<jsi::Value>>();
10841084
eventEmitterMap_[\\"onEvent5\\"] = std::make_shared<AsyncEventEmitter<jsi::Value>>();
10851085
eventEmitterMap_[\\"onEvent6\\"] = std::make_shared<AsyncEventEmitter<jsi::Value>>();
1086+
eventEmitterMap_[\\"onEvent7\\"] = std::make_shared<AsyncEventEmitter<jsi::Value>>();
1087+
eventEmitterMap_[\\"onEvent8\\"] = std::make_shared<AsyncEventEmitter<jsi::Value>>();
1088+
eventEmitterMap_[\\"onEvent9\\"] = std::make_shared<AsyncEventEmitter<jsi::Value>>();
10861089
}
10871090

10881091
void emitOnEvent1() {
@@ -1123,6 +1126,27 @@ protected:
11231126
return bridging::toJs(rt, eventValue, jsInvoker);
11241127
});
11251128
}
1129+
1130+
template <typename OnEvent7Type> void emitOnEvent7(OnEvent7Type value) {
1131+
static_assert(bridging::supportsFromJs<OnEvent7Type, double>, \\"value cannnot be converted to double\\");
1132+
static_cast<AsyncEventEmitter<jsi::Value>&>(*eventEmitterMap_[\\"onEvent7\\"]).emit([jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value {
1133+
return bridging::toJs(rt, eventValue, jsInvoker);
1134+
});
1135+
}
1136+
1137+
template <typename OnEvent8Type> void emitOnEvent8(OnEvent8Type value) {
1138+
static_assert(bridging::supportsFromJs<OnEvent8Type, double>, \\"value cannnot be converted to double\\");
1139+
static_cast<AsyncEventEmitter<jsi::Value>&>(*eventEmitterMap_[\\"onEvent8\\"]).emit([jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value {
1140+
return bridging::toJs(rt, eventValue, jsInvoker);
1141+
});
1142+
}
1143+
1144+
template <typename OnEvent9Type> void emitOnEvent9(OnEvent9Type value) {
1145+
static_assert(bridging::supportsFromJs<OnEvent9Type, int>, \\"value cannnot be converted to int\\");
1146+
static_cast<AsyncEventEmitter<jsi::Value>&>(*eventEmitterMap_[\\"onEvent9\\"]).emit([jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value {
1147+
return bridging::toJs(rt, eventValue, jsInvoker);
1148+
});
1149+
}
11261150
private:
11271151
static jsi::Value __voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) {
11281152
static_assert(

packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,9 @@ facebook::react::EventEmitterCallback _eventEmitterCallback;
614614
- (void)emitOnEvent4:(BOOL)value;
615615
- (void)emitOnEvent5:(NSDictionary *)value;
616616
- (void)emitOnEvent6:(NSArray<id<NSObject>> *)value;
617+
- (void)emitOnEvent7:(NSNumber *_Nonnull)value;
618+
- (void)emitOnEvent8:(NSNumber *_Nonnull)value;
619+
- (void)emitOnEvent9:(NSNumber *_Nonnull)value;
617620
@end
618621
619622
namespace facebook::react {

0 commit comments

Comments
 (0)