Skip to content

Commit fb155df

Browse files
christophpurrermeta-codesync[bot]
authored andcommitted
Add ArrayBuffer support for React Native TurboModules - copies data between JavaScript and native
Summary: ## Changelog: [General] [Added] - Add ArrayBuffer support for React Native Turbo Modules react-native-community/discussions-and-proposals#947 Adds first-class `ArrayBuffer` support across the entire TurboModule pipeline, enabling efficient binary data transfer between JavaScript and native code without base64 encoding overhead. **Schema & Parser:** - Added `NativeModuleArrayBufferTypeAnnotation` to `CodegenSchema.js` and `.d.ts` - Added `emitArrayBuffer` to `parsers-primitives.js` type map **C++ (Bridging & Generator):** - Added `ArrayBufferKind` to `TurboModuleMethodValueKind` enum - Created `ArrayBuffer.h` bridging header with `OwnedMutableBuffer` and `Bridging<std::vector<uint8_t>>` - Added `jsi::ArrayBuffer` conversion operators to `Convert.h` - Mapped `ArrayBufferTypeAnnotation` → `jsi::ArrayBuffer` in `GenerateModuleH.js` **Android (Java/JNI):** - Mapped `ArrayBufferTypeAnnotation` → `java.nio.ByteBuffer` in Java spec generator - Mapped to `Ljava/nio/ByteBuffer;` JNI signature in JNI generator - Added `ByteBuffer` arg/return handling in `JavaTurboModule.cpp` via `NewDirectByteBuffer`/`GetDirectBufferAddress` **iOS (ObjC):** - Mapped `ArrayBufferTypeAnnotation` → `NSData *` in ObjC spec generator - Added ArrayBuffer→NSData conversion in `convertJSIValueToObjCObject()` - Added NSData→ArrayBuffer conversion in `convertReturnIdToJSIValue()` **Flow-Schema:** - Added `ArrayBuffer` to `JAVASCRIPT_BUILTINS` in `BoundaryTypes.js` **Tests:** - Added Flow and TypeScript parser fixtures (`NATIVE_MODULE_WITH_ARRAYBUFFER`) - Added generator schema fixture (`ARRAYBUFFER_MODULE`) - Added `emitArrayBuffer` unit tests Differential Revision: D95649873
1 parent ba204fa commit fb155df

39 files changed

Lines changed: 1219 additions & 30 deletions

packages/react-native-codegen/src/CodegenSchema.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@ export interface NativeModuleMixedTypeAnnotation {
397397
readonly type: 'MixedTypeAnnotation';
398398
}
399399

400+
export interface NativeModuleArrayBufferTypeAnnotation {
401+
readonly type: 'ArrayBufferTypeAnnotation';
402+
}
403+
400404
export type NativeModuleEventEmitterBaseTypeAnnotation =
401405
| NativeModuleBooleanTypeAnnotation
402406
| NativeModuleDoubleTypeAnnotation
@@ -434,6 +438,7 @@ export type NativeModuleBaseTypeAnnotation =
434438
| NativeModuleObjectTypeAnnotation
435439
| NativeModuleUnionTypeAnnotation
436440
| NativeModuleMixedTypeAnnotation
441+
| NativeModuleArrayBufferTypeAnnotation
437442
| NativeModuleArrayTypeAnnotation<NativeModuleBaseTypeAnnotation>;
438443

439444
export type NativeModuleParamTypeAnnotation =

packages/react-native-codegen/src/CodegenSchema.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,10 @@ export type NativeModuleMixedTypeAnnotation = Readonly<{
391391
type: 'MixedTypeAnnotation',
392392
}>;
393393

394+
export type NativeModuleArrayBufferTypeAnnotation = Readonly<{
395+
type: 'ArrayBufferTypeAnnotation',
396+
}>;
397+
394398
type NativeModuleEventEmitterBaseTypeAnnotation =
395399
| BooleanTypeAnnotation
396400
| DoubleTypeAnnotation
@@ -428,7 +432,8 @@ export type NativeModuleBaseTypeAnnotation =
428432
| NativeModuleArrayTypeAnnotation<Nullable<NativeModuleBaseTypeAnnotation>>
429433
| NativeModuleObjectTypeAnnotation
430434
| NativeModuleUnionTypeAnnotation
431-
| NativeModuleMixedTypeAnnotation;
435+
| NativeModuleMixedTypeAnnotation
436+
| NativeModuleArrayBufferTypeAnnotation;
432437

433438
export type NativeModuleParamTypeAnnotation =
434439
| NativeModuleBaseTypeAnnotation

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ function serializeArg(
146146
return wrap(val => `${val}.asObject(rt)`);
147147
case 'MixedTypeAnnotation':
148148
return wrap(val => `jsi::Value(rt, ${val})`);
149+
case 'ArrayBufferTypeAnnotation':
150+
return wrap(val => `${val}.asObject(rt).getArrayBuffer(rt)`);
149151
default:
150152
realTypeAnnotation.type as empty;
151153
throw new Error(
@@ -307,6 +309,8 @@ function translatePrimitiveJSTypeToCpp(
307309
return wrapOptional('jsi::Value', isRequired);
308310
case 'MixedTypeAnnotation':
309311
return wrapOptional('jsi::Value', isRequired);
312+
case 'ArrayBufferTypeAnnotation':
313+
return wrapOptional('jsi::ArrayBuffer', isRequired);
310314
default:
311315
realTypeAnnotation.type as empty;
312316
throw new Error(createErrorMessage(realTypeAnnotation.type));

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ function translateEventEmitterTypeToJavaType(
170170
case 'FloatTypeAnnotation':
171171
case 'Int32TypeAnnotation':
172172
case 'VoidTypeAnnotation':
173+
case 'ArrayBufferTypeAnnotation':
173174
// TODO: Add support for these types
174175
throw new Error(
175176
`Unsupported eventType for ${eventEmitter.name}. Found: ${eventEmitter.typeAnnotation.typeAnnotation.type}`,
@@ -267,6 +268,9 @@ function translateFunctionParamToJavaType(
267268
case 'FunctionTypeAnnotation':
268269
imports.add('com.facebook.react.bridge.Callback');
269270
return wrapOptional('Callback', isRequired);
271+
case 'ArrayBufferTypeAnnotation':
272+
imports.add('java.nio.ByteBuffer');
273+
return wrapOptional('ByteBuffer', isRequired);
270274
default:
271275
realTypeAnnotation.type as 'MixedTypeAnnotation';
272276
throw new Error(createErrorMessage(realTypeAnnotation.type));
@@ -361,6 +365,9 @@ function translateFunctionReturnTypeToJavaType(
361365
case 'ArrayTypeAnnotation':
362366
imports.add('com.facebook.react.bridge.WritableArray');
363367
return wrapOptional('WritableArray', isRequired);
368+
case 'ArrayBufferTypeAnnotation':
369+
imports.add('java.nio.ByteBuffer');
370+
return wrapOptional('ByteBuffer', isRequired);
364371
default:
365372
realTypeAnnotation.type as 'MixedTypeAnnotation';
366373
throw new Error(createErrorMessage(realTypeAnnotation.type));
@@ -443,6 +450,8 @@ function getFalsyReturnStatementFromReturnType(
443450
return 'return null;';
444451
case 'ArrayTypeAnnotation':
445452
return 'return null;';
453+
case 'ArrayBufferTypeAnnotation':
454+
return 'return null;';
446455
default:
447456
realTypeAnnotation.type as 'MixedTypeAnnotation';
448457
throw new Error(createErrorMessage(realTypeAnnotation.type));

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ type JSReturnType =
3535
| 'NumberKind'
3636
| 'PromiseKind'
3737
| 'ObjectKind'
38-
| 'ArrayKind';
38+
| 'ArrayKind'
39+
| 'ArrayBufferKind';
3940

4041
const HostFunctionTemplate = ({
4142
hasteModuleName,
@@ -216,6 +217,8 @@ function translateReturnTypeToKind(
216217
return 'ObjectKind';
217218
case 'ArrayTypeAnnotation':
218219
return 'ArrayKind';
220+
case 'ArrayBufferTypeAnnotation':
221+
return 'ArrayBufferKind';
219222
default:
220223
realTypeAnnotation.type as 'MixedTypeAnnotation';
221224
throw new Error(
@@ -303,6 +306,8 @@ function translateParamTypeToJniType(
303306
return 'Lcom/facebook/react/bridge/ReadableArray;';
304307
case 'FunctionTypeAnnotation':
305308
return 'Lcom/facebook/react/bridge/Callback;';
309+
case 'ArrayBufferTypeAnnotation':
310+
return 'Ljava/nio/ByteBuffer;';
306311
default:
307312
realTypeAnnotation.type as 'MixedTypeAnnotation';
308313
throw new Error(
@@ -387,6 +392,8 @@ function translateReturnTypeToJniType(
387392
return 'Lcom/facebook/react/bridge/WritableMap;';
388393
case 'ArrayTypeAnnotation':
389394
return 'Lcom/facebook/react/bridge/WritableArray;';
395+
case 'ArrayBufferTypeAnnotation':
396+
return 'Ljava/nio/ByteBuffer;';
390397
default:
391398
realTypeAnnotation.type as 'MixedTypeAnnotation';
392399
throw new Error(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class StructCollector {
132132
return wrapNullable(nullable, typeAnnotation);
133133
case 'MixedTypeAnnotation':
134134
throw new Error('Mixed types are unsupported in structs');
135+
case 'ArrayBufferTypeAnnotation':
136+
throw new Error('ArrayBuffer types are unsupported in structs');
135137
case 'UnionTypeAnnotation':
136138
try {
137139
const validUnionType = parseValidUnionType(typeAnnotation);

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ type ReturnJSType =
5151
| 'ObjectKind'
5252
| 'ArrayKind'
5353
| 'NumberKind'
54-
| 'StringKind';
54+
| 'StringKind'
55+
| 'ArrayBufferKind';
5556

5657
export type MethodSerializationOutput = Readonly<{
5758
methodName: string,
@@ -219,6 +220,9 @@ function getParamObjCType(
219220
*/
220221
return notStruct(wrapOptional('NSArray *', !nullable));
221222
}
223+
case 'ArrayBufferTypeAnnotation': {
224+
return notStruct(wrapOptional('NSMutableData *', !nullable));
225+
}
222226
}
223227

224228
const [structTypeAnnotation] = unwrapNullable(
@@ -387,6 +391,8 @@ function getReturnObjCType(
387391
}
388392
case 'GenericObjectTypeAnnotation':
389393
return wrapOptional('NSDictionary *', isRequired);
394+
case 'ArrayBufferTypeAnnotation':
395+
return wrapOptional('NSMutableData *', isRequired);
390396
default:
391397
typeAnnotation.type as 'MixedTypeAnnotation';
392398
throw new Error(
@@ -433,6 +439,8 @@ function getReturnJSType(
433439
return 'BooleanKind';
434440
case 'GenericObjectTypeAnnotation':
435441
return 'ObjectKind';
442+
case 'ArrayBufferTypeAnnotation':
443+
return 'ArrayBufferKind';
436444
case 'EnumDeclaration':
437445
switch (typeAnnotation.memberType) {
438446
case 'NumberTypeAnnotation':

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,6 +2792,74 @@ const STRING_LITERALS: SchemaType = {
27922792
},
27932793
};
27942794

2795+
const ARRAYBUFFER_MODULE: SchemaType = {
2796+
modules: {
2797+
NativeSampleTurboModule: {
2798+
type: 'NativeModule',
2799+
aliasMap: {},
2800+
enumMap: {},
2801+
spec: {
2802+
eventEmitters: [],
2803+
methods: [
2804+
{
2805+
name: 'getArrayBuffer',
2806+
optional: false,
2807+
typeAnnotation: {
2808+
type: 'FunctionTypeAnnotation',
2809+
returnTypeAnnotation: {
2810+
type: 'ArrayBufferTypeAnnotation',
2811+
},
2812+
params: [
2813+
{
2814+
name: 'buffer',
2815+
optional: false,
2816+
typeAnnotation: {
2817+
type: 'ArrayBufferTypeAnnotation',
2818+
},
2819+
},
2820+
],
2821+
},
2822+
},
2823+
{
2824+
name: 'saveData',
2825+
optional: false,
2826+
typeAnnotation: {
2827+
type: 'FunctionTypeAnnotation',
2828+
returnTypeAnnotation: {
2829+
type: 'VoidTypeAnnotation',
2830+
},
2831+
params: [
2832+
{
2833+
name: 'data',
2834+
optional: false,
2835+
typeAnnotation: {
2836+
type: 'ArrayBufferTypeAnnotation',
2837+
},
2838+
},
2839+
],
2840+
},
2841+
},
2842+
{
2843+
name: 'loadData',
2844+
optional: false,
2845+
typeAnnotation: {
2846+
type: 'FunctionTypeAnnotation',
2847+
returnTypeAnnotation: {
2848+
type: 'NullableTypeAnnotation',
2849+
typeAnnotation: {
2850+
type: 'ArrayBufferTypeAnnotation',
2851+
},
2852+
},
2853+
params: [],
2854+
},
2855+
},
2856+
],
2857+
},
2858+
moduleName: 'SampleTurboModule',
2859+
},
2860+
},
2861+
};
2862+
27952863
module.exports = {
27962864
complex_objects: COMPLEX_OBJECTS,
27972865
two_modules_different_files: TWO_MODULES_DIFFERENT_FILES,
@@ -2804,4 +2872,5 @@ module.exports = {
28042872
SampleWithUppercaseName: SAMPLE_WITH_UPPERCASE_NAME,
28052873
union_module: UNION_MODULE,
28062874
string_literals: STRING_LITERALS,
2875+
arraybuffer_module: ARRAYBUFFER_MODULE,
28072876
};

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,67 @@ private:
3838
}
3939
`;
4040

41+
exports[`GenerateModuleH can generate fixture arraybuffer_module 1`] = `
42+
Map {
43+
"arraybuffer_moduleJSI.h" => "/**
44+
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
45+
*
46+
* Do not edit this file as changes may cause incorrect behavior and will be lost
47+
* once the code is regenerated.
48+
*
49+
* @generated by codegen project: GenerateModuleH.js
50+
*/
51+
52+
#pragma once
53+
54+
#include <ReactCommon/TurboModule.h>
55+
#include <react/bridging/Bridging.h>
56+
57+
namespace facebook::react {
58+
59+
60+
template <typename T>
61+
class JSI_EXPORT NativeSampleTurboModuleCxxSpec : public TurboModule {
62+
public:
63+
static constexpr std::string_view kModuleName = \\"SampleTurboModule\\";
64+
65+
protected:
66+
NativeSampleTurboModuleCxxSpec(std::shared_ptr<CallInvoker> jsInvoker) : TurboModule(std::string{NativeSampleTurboModuleCxxSpec::kModuleName}, jsInvoker) {
67+
methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {.argCount = 1, .invoker = __getArrayBuffer};
68+
methodMap_[\\"saveData\\"] = MethodMetadata {.argCount = 1, .invoker = __saveData};
69+
methodMap_[\\"loadData\\"] = MethodMetadata {.argCount = 0, .invoker = __loadData};
70+
}
71+
72+
private:
73+
static jsi::Value __getArrayBuffer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
74+
static_assert(
75+
bridging::getParameterCount(&T::getArrayBuffer) == 2,
76+
\\"Expected getArrayBuffer(...) to have 2 parameters\\");
77+
return bridging::callFromJs<jsi::ArrayBuffer>(rt, &T::getArrayBuffer, static_cast<NativeSampleTurboModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
78+
count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asObject(rt).getArrayBuffer(rt));
79+
}
80+
81+
static jsi::Value __saveData(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
82+
static_assert(
83+
bridging::getParameterCount(&T::saveData) == 2,
84+
\\"Expected saveData(...) to have 2 parameters\\");
85+
bridging::callFromJs<void>(rt, &T::saveData, static_cast<NativeSampleTurboModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
86+
count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asObject(rt).getArrayBuffer(rt));return jsi::Value::undefined();
87+
}
88+
89+
static jsi::Value __loadData(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) {
90+
static_assert(
91+
bridging::getParameterCount(&T::loadData) == 1,
92+
\\"Expected loadData(...) to have 1 parameters\\");
93+
auto result = bridging::callFromJs<std::optional<jsi::ArrayBuffer>>(rt, &T::loadData, static_cast<NativeSampleTurboModuleCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule));return result ? jsi::Value(std::move(*result)) : jsi::Value::null();
94+
}
95+
};
96+
97+
} // namespace facebook::react
98+
",
99+
}
100+
`;
101+
41102
exports[`GenerateModuleH can generate fixture complex_objects 1`] = `
42103
Map {
43104
"complex_objectsJSI.h" => "/**

0 commit comments

Comments
 (0)