Skip to content

Commit 54b88d9

Browse files
paradowstackmeta-codesync[bot]
authored andcommitted
ArrayBuffer support to ObjC TurboModules
Summary: Adds ArrayBuffer support to ObjC TurboModules, following the C++ ArrayBuffer PR ([`226ef2e`](226ef2e)). - Codegen support for `ArrayBufferTypeAnnotation` in ObjC module specs (`NSMutableData *` params/returns, new `ArrayBufferKind`) - JSI↔ObjC conversion wraps native-backed buffers zero-copy via `-[NSMutableData initWithBytesNoCopy:length:deallocator:]`; the deallocator retains the backing store so the bytes stay valid even if the `NSMutableData` escapes the call or the source ArrayBuffer is garbage-collected - JS-backed buffers are copied, which is safe on both the synchronous and asynchronous paths This PR is iOS-only; Android support follows in a separate PR. ## Changelog: [IOS] [ADDED] - Add ArrayBuffer support to ObjC TurboModules X-link: #56986 Reviewed By: javache Differential Revision: D106846249 Pulled By: christophpurrer fbshipit-source-id: 3393d5d6f31a1412f5d52328c90e51205aa6b153
1 parent a3582a9 commit 54b88d9

18 files changed

Lines changed: 570 additions & 11 deletions

File tree

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

Lines changed: 7 additions & 7 deletions
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(
@@ -388,9 +392,7 @@ function getReturnObjCType(
388392
case 'GenericObjectTypeAnnotation':
389393
return wrapOptional('NSDictionary *', isRequired);
390394
case 'ArrayBufferTypeAnnotation':
391-
throw new Error(
392-
`Unsupported return type for ${methodName}: ArrayBuffer is only supported for C++ TurboModules.`,
393-
);
395+
return wrapOptional('NSMutableData *', isRequired);
394396
default:
395397
typeAnnotation.type as 'MixedTypeAnnotation';
396398
throw new Error(
@@ -464,9 +466,7 @@ function getReturnJSType(
464466
throw new Error(`Unsupported union member types`);
465467
}
466468
case 'ArrayBufferTypeAnnotation':
467-
throw new Error(
468-
`Unsupported return type for ${methodName}: ArrayBuffer is only supported for C++ TurboModules.`,
469-
);
469+
return 'ArrayBufferKind';
470470
default:
471471
typeAnnotation.type as 'MixedTypeAnnotation';
472472
throw new Error(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2678,7 +2678,7 @@ const ARRAY_BUFFER_NATIVE_MODULE: SchemaType = {
26782678
],
26792679
},
26802680
moduleName: 'SampleTurboModule',
2681-
excludedPlatforms: ['iOS', 'android'],
2681+
excludedPlatforms: ['android'],
26822682
},
26832683
},
26842684
};

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ Map {
100100
#import <vector>
101101
102102
103+
@protocol NativeSampleTurboModuleSpec <RCTBridgeModule, RCTTurboModule>
104+
105+
- (NSMutableData *)getArrayBuffer;
106+
- (void)voidArrayBuffer:(NSMutableData *)arg;
107+
- (void)voidNullableArrayBuffer:(NSMutableData * _Nullable)arg;
108+
- (void)promiseArrayBuffer:(RCTPromiseResolveBlock)resolve
109+
reject:(RCTPromiseRejectBlock)reject;
110+
111+
@end
112+
113+
@interface NativeSampleTurboModuleSpecBase : NSObject {
114+
@protected
115+
facebook::react::EventEmitterCallback _eventEmitterCallback;
116+
}
117+
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper;
118+
119+
120+
@end
121+
122+
namespace facebook::react {
123+
/**
124+
* ObjC++ class for module 'NativeSampleTurboModule'
125+
*/
126+
class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public ObjCTurboModule {
127+
public:
128+
NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams &params);
129+
};
130+
} // namespace facebook::react
103131
104132
#endif // array_buffer_native_module_H
105133
",

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,50 @@ Map {
5858
#import \\"array_buffer_native_module.h\\"
5959
6060
61+
@implementation NativeSampleTurboModuleSpecBase
62+
63+
64+
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper
65+
{
66+
_eventEmitterCallback = std::move(eventEmitterCallbackWrapper->_eventEmitterCallback);
67+
}
68+
@end
69+
70+
71+
namespace facebook::react {
72+
73+
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
74+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", @selector(getArrayBuffer), args, count);
75+
}
76+
77+
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
78+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, \\"voidArrayBuffer\\", @selector(voidArrayBuffer:), args, count);
79+
}
80+
81+
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
82+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", @selector(voidNullableArrayBuffer:), args, count);
83+
}
84+
85+
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
86+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, \\"promiseArrayBuffer\\", @selector(promiseArrayBuffer:reject:), args, count);
87+
}
88+
89+
NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams &params)
90+
: ObjCTurboModule(params) {
91+
92+
methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer};
93+
94+
95+
methodMap_[\\"voidArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer};
96+
97+
98+
methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer};
99+
100+
101+
methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer};
102+
103+
}
104+
} // namespace facebook::react
61105
",
62106
}
63107
`;

packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ enum TurboModuleMethodValueKind {
3131
ArrayKind,
3232
FunctionKind,
3333
PromiseKind,
34+
ArrayBufferKind,
3435
};
3536

3637
/**

packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#import <hermes/hermes.h>
1212
#import <react/featureflags/ReactNativeFeatureFlags.h>
1313

14+
#import <vector>
15+
1416
#import <OCMock/OCMock.h>
1517

1618
using namespace facebook::react;
@@ -27,6 +29,23 @@ @implementation RCTTestTurboModule
2729

2830
@end
2931

32+
// Minimal concrete MutableBuffer that owns its bytes, used to observe lifetime.
33+
class TestMutableBuffer : public facebook::jsi::MutableBuffer {
34+
public:
35+
explicit TestMutableBuffer(size_t size) : bytes_(size, 0) {}
36+
size_t size() const override
37+
{
38+
return bytes_.size();
39+
}
40+
uint8_t *data() override
41+
{
42+
return bytes_.data();
43+
}
44+
45+
private:
46+
std::vector<uint8_t> bytes_;
47+
};
48+
3049
class StubNativeMethodCallInvoker : public NativeMethodCallInvoker {
3150
public:
3251
void invokeAsync(const std::string &methodName, NativeMethodCallFunc &&func) noexcept override
@@ -104,4 +123,34 @@ - (void)testInvokeTurboModuleWithNull
104123
OCMVerify(OCMTimes(1), [instance_ testMethodWhichTakesObject:nil]);
105124
}
106125

126+
// A JS ArrayBuffer converts to an NSMutableData that owns an independent copy of
127+
// the bytes — NSMutableData cannot alias a foreign buffer, so the result stays
128+
// valid and is safe to mutate after the source buffer is gone. This covers the
129+
// ArrayBuffer-backed-by-native-MutableBuffer case, which is the one that could in
130+
// principle have been aliased zero-copy.
131+
- (void)testArrayBufferConvertsToIndependentNSMutableData
132+
{
133+
constexpr size_t kBufferSize = 64 * 1024;
134+
135+
auto hermesRuntime = facebook::hermes::makeHermesRuntime();
136+
facebook::jsi::Runtime *rt = hermesRuntime.get();
137+
138+
auto buffer = std::make_shared<TestMutableBuffer>(kBufferSize);
139+
*buffer->data() = 0xAB;
140+
141+
facebook::jsi::ArrayBuffer arrayBuffer(*rt, buffer);
142+
id converted =
143+
TurboModuleConvertUtils::convertJSIValueToObjCObject(*rt, facebook::jsi::Value(*rt, arrayBuffer), nullptr);
144+
145+
XCTAssertTrue([converted isKindOfClass:[NSMutableData class]]);
146+
NSMutableData *data = (NSMutableData *)converted;
147+
XCTAssertEqual(data.length, (NSUInteger)kBufferSize);
148+
XCTAssertEqual(*static_cast<const uint8_t *>(data.bytes), 0xAB);
149+
150+
// Independent copy: mutating the NSMutableData must not write through to the
151+
// source MutableBuffer.
152+
*static_cast<uint8_t *>(data.mutableBytes) = 0xCD;
153+
XCTAssertEqual(*buffer->data(), 0xAB, @"NSMutableData must not alias the source buffer");
154+
}
155+
107156
@end

packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,37 @@ static int32_t getUniqueId()
8989
return result;
9090
}
9191

92+
static jsi::ArrayBuffer convertNSMutableDataToJSIArrayBuffer(jsi::Runtime &runtime, NSMutableData *value)
93+
{
94+
class NSMutableDataBuffer final : public jsi::MutableBuffer {
95+
public:
96+
explicit NSMutableDataBuffer(NSMutableData *data) : data_(data) {}
97+
98+
size_t size() const override
99+
{
100+
return data_.length;
101+
}
102+
103+
uint8_t *data() override
104+
{
105+
return static_cast<uint8_t *>(data_.mutableBytes);
106+
}
107+
108+
private:
109+
NSMutableData *data_;
110+
};
111+
112+
// A nil NSMutableData would silently yield a NULL `mutableBytes` pointer
113+
// (ObjC nil-messaging) and corrupt the resulting ArrayBuffer. Substitute a
114+
// fresh empty NSMutableData so the wrapped pointer is always well-defined.
115+
if (value == nil) {
116+
RCTLogWarn(@"convertNSMutableDataToJSIArrayBuffer: received nil NSMutableData; returning empty ArrayBuffer");
117+
value = [NSMutableData data];
118+
}
119+
auto buffer = std::make_shared<NSMutableDataBuffer>(value);
120+
return {runtime, std::move(buffer)};
121+
}
122+
92123
jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value)
93124
{
94125
if ([value isKindOfClass:[NSString class]]) {
@@ -102,6 +133,8 @@ static int32_t getUniqueId()
102133
return convertNSDictionaryToJSIObject(runtime, (NSDictionary *)value);
103134
} else if ([value isKindOfClass:[NSArray class]]) {
104135
return convertNSArrayToJSIArray(runtime, (NSArray *)value);
136+
} else if ([value isKindOfClass:[NSMutableData class]]) {
137+
return convertNSMutableDataToJSIArrayBuffer(runtime, (NSMutableData *)value);
105138
} else if (value == (id)kCFNull) {
106139
return jsi::Value::null();
107140
}
@@ -166,6 +199,18 @@ static int32_t getUniqueId()
166199
};
167200
}
168201

202+
// Copy the ArrayBuffer's bytes into an NSMutableData. A zero-copy wrap is not
203+
// possible here: NSMutableData needs its own resizable backing store and cannot
204+
// alias a foreign buffer (even via initWithBytesNoCopy:length:deallocator:, which
205+
// copies eagerly for the mutable subclass). Copying also makes the result safe to
206+
// retain in a block, store, or dispatch to another thread, regardless of whether
207+
// the bytes were owned by JS (valid only for this callstack) or by a native
208+
// MutableBuffer (which the JS ArrayBuffer may GC concurrently).
209+
static NSMutableData *convertJSIArrayBufferToNSMutableData(jsi::Runtime &rt, const jsi::ArrayBuffer &value)
210+
{
211+
return [NSMutableData dataWithBytes:value.data(rt) length:value.size(rt)];
212+
}
213+
169214
id convertJSIValueToObjCObject(
170215
jsi::Runtime &runtime,
171216
const jsi::Value &value,
@@ -195,6 +240,9 @@ id convertJSIValueToObjCObject(
195240
if (o.isFunction(runtime)) {
196241
return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker);
197242
}
243+
if (o.isArrayBuffer(runtime)) {
244+
return convertJSIArrayBufferToNSMutableData(runtime, o.getArrayBuffer(runtime));
245+
}
198246
return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull);
199247
}
200248

@@ -522,6 +570,14 @@ TraceSection s(
522570
returnValue = convertNSArrayToJSIArray(runtime, (NSArray *)result);
523571
break;
524572
}
573+
case ArrayBufferKind: {
574+
if (result != nil && ![result isKindOfClass:[NSMutableData class]]) {
575+
RCTLogError(@"convertReturnIdToJSIValue: expected NSMutableData for ArrayBufferKind, got %@", [result class]);
576+
break;
577+
}
578+
returnValue = convertNSMutableDataToJSIArrayBuffer(runtime, (NSMutableData *)result);
579+
break;
580+
}
525581
case FunctionKind:
526582
throw jsi::JSError(runtime, "convertReturnIdToJSIValue: FunctionKind is not supported yet.");
527583
case PromiseKind:
@@ -605,7 +661,7 @@ TraceSection s(
605661
* JS type checking ensures the Objective C argument here is either a BOOL or NSNumber*.
606662
*/
607663
if (objCArgType == @encode(id)) {
608-
id objCArg = [NSNumber numberWithBool:v];
664+
id objCArg = @(v);
609665
[inv setArgument:(void *)&objCArg atIndex:i + 2];
610666
[retainedObjectsForInvocation addObject:objCArg];
611667
} else {
@@ -622,7 +678,7 @@ TraceSection s(
622678
* JS type checking ensures the Objective C argument here is either a double or NSNumber* or NSInteger.
623679
*/
624680
if (objCArgType == @encode(id)) {
625-
id objCArg = [NSNumber numberWithDouble:v];
681+
id objCArg = @(v);
626682
[inv setArgument:(void *)&objCArg atIndex:i + 2];
627683
[retainedObjectsForInvocation addObject:objCArg];
628684
} else if (objCArgType == @encode(NSInteger)) {
@@ -813,7 +869,8 @@ TraceSection s(
813869
case StringKind:
814870
case ObjectKind:
815871
case ArrayKind:
816-
case FunctionKind: {
872+
case FunctionKind:
873+
case ArrayBufferKind: {
817874
id result = performMethodInvocation(runtime, true, methodName, inv, retainedObjectsForInvocation);
818875
TurboModulePerfLogger::syncMethodCallReturnConversionStart(moduleName, methodName);
819876
returnValue = convertReturnIdToJSIValue(runtime, methodName, returnType, result);

packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
E7DB216722B2F69F005AC45F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB213022B2C649005AC45F /* JavaScriptCore.framework */; };
5454
E7DB218C22B41FCD005AC45F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB218B22B41FCD005AC45F /* XCTest.framework */; };
5555
F0D621C32BBB9E38005960AC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */; };
56+
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */; };
5657
/* End PBXBuildFile section */
5758

5859
/* Begin PBXContainerItemProxy section */
@@ -156,6 +157,7 @@
156157
E7DB215D22B2F3EC005AC45F /* RNTesterTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterTestModule.m; sourceTree = "<group>"; };
157158
E7DB218B22B41FCD005AC45F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = XCTest.framework; sourceTree = DEVELOPER_DIR; };
158159
F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
160+
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleArrayBufferTests.mm; sourceTree = "<group>"; };
159161
/* End PBXFileReference section */
160162

161163
/* Begin PBXFrameworksBuildPhase section */
@@ -332,6 +334,7 @@
332334
E7DB20AA22B2BAA3005AC45F /* RCTModuleInitNotificationRaceTests.m */,
333335
E7DB20B022B2BAA4005AC45F /* RCTModuleInitTests.m */,
334336
E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */,
337+
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */,
335338
E7DB20CF22B2BAA5005AC45F /* RCTMultipartStreamReaderTests.m */,
336339
A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */,
337340
E7DB20BE22B2BAA4005AC45F /* RCTNativeAnimatedNodesManagerTests.m */,
@@ -746,6 +749,7 @@
746749
E7DB20EC22B2BAA6005AC45F /* RCTMultipartStreamReaderTests.m in Sources */,
747750
E7DB20E022B2BAA6005AC45F /* RCTMethodArgumentTests.m in Sources */,
748751
E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */,
752+
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */,
749753
E7DB20E222B2BAA6005AC45F /* RCTGzipTests.m in Sources */,
750754
E7DB20ED22B2BAA6005AC45F /* RCTURLUtilsTests.m in Sources */,
751755
E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */,

0 commit comments

Comments
 (0)