Skip to content

Commit 5e86c32

Browse files
christophpurrermeta-codesync[bot]
authored andcommitted
Use NSData for the JS->ObjC ArrayBuffer argument path (#57596)
Summary: Pull Request resolved: #57596 The original implementation used `NSMutableData` for both directions of the JSI<->ObjC ArrayBuffer conversion. That is the wrong contract on the argument (JS -> ObjC/Native) side: an inbound buffer is owned by the caller, not the native module, and `NSMutableData` must own a resizable backing store it can `realloc`/`free`, so it cannot durably alias a foreign buffer. This diff keeps `NSMutableData` for the ObjC/Native -> JS (return) path, but switches the JS -> ObjC/Native (argument) path to the immutable `NSData`: - Codegen: `getParamObjCType` now maps `ArrayBufferTypeAnnotation` params to `NSData *` (return type stays `NSMutableData *`). - Runtime: `convertJSIArrayBufferToNSMutableData` is renamed to `convertJSIArrayBufferToNSData` and returns an immutable `NSData`. The bytes are still eagerly copied, which keeps the result safe to retain, store, or dispatch to another thread regardless of whether the source bytes were owned by JS or by a native `MutableBuffer`. - Updated the ObjC unit tests and the codegen `GenerateModuleHObjCpp` snapshot. Changelog: [INTERNAL] Reviewed By: javache Differential Revision: D112582384 fbshipit-source-id: 8ed2dfb3f23e7f39677dfb1824c5530f378705bc
1 parent 59d6947 commit 5e86c32

5 files changed

Lines changed: 31 additions & 32 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ function getParamObjCType(
221221
return notStruct(wrapOptional('NSArray *', !nullable));
222222
}
223223
case 'ArrayBufferTypeAnnotation': {
224-
return notStruct(wrapOptional('NSMutableData *', !nullable));
224+
return notStruct(wrapOptional('NSData *', !nullable));
225225
}
226226
}
227227

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ Map {
103103
@protocol NativeSampleTurboModuleSpec <RCTBridgeModule, RCTTurboModule>
104104
105105
- (NSMutableData *)getArrayBuffer;
106-
- (void)voidArrayBuffer:(NSMutableData *)arg;
107-
- (void)voidNullableArrayBuffer:(NSMutableData * _Nullable)arg;
106+
- (void)voidArrayBuffer:(NSData *)arg;
107+
- (void)voidNullableArrayBuffer:(NSData * _Nullable)arg;
108108
- (void)promiseArrayBuffer:(RCTPromiseResolveBlock)resolve
109109
reject:(RCTPromiseRejectBlock)reject;
110110

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,11 @@ - (void)testInvokeTurboModuleWithNull
123123
OCMVerify(OCMTimes(1), [instance_ testMethodWhichTakesObject:nil]);
124124
}
125125

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
126+
// A JS ArrayBuffer converts (on the argument path) to an immutable NSData that
127+
// owns an independent copy of the bytes, so the result stays valid after the
128+
// source buffer is gone. This covers the ArrayBuffer-backed-by-native-MutableBuffer
129+
// case, which is the one that could in principle have been aliased zero-copy.
130+
- (void)testArrayBufferConvertsToIndependentNSData
132131
{
133132
constexpr size_t kBufferSize = 64 * 1024;
134133

@@ -142,15 +141,15 @@ - (void)testArrayBufferConvertsToIndependentNSMutableData
142141
id converted =
143142
TurboModuleConvertUtils::convertJSIValueToObjCObject(*rt, facebook::jsi::Value(*rt, arrayBuffer), nullptr);
144143

145-
XCTAssertTrue([converted isKindOfClass:[NSMutableData class]]);
146-
NSMutableData *data = (NSMutableData *)converted;
144+
XCTAssertTrue([converted isKindOfClass:[NSData class]]);
145+
NSData *data = (NSData *)converted;
147146
XCTAssertEqual(data.length, (NSUInteger)kBufferSize);
148147
XCTAssertEqual(*static_cast<const uint8_t *>(data.bytes), 0xAB);
149148

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");
149+
// Independent copy: mutating the source MutableBuffer must not write through to
150+
// the NSData.
151+
*buffer->data() = 0xCD;
152+
XCTAssertEqual(*static_cast<const uint8_t *>(data.bytes), 0xAB, @"NSData must not alias the source buffer");
154153
}
155154

156155
@end

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,16 +199,15 @@ size_t size() const override
199199
};
200200
}
201201

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)
202+
// Copy the ArrayBuffer's bytes into an immutable NSData. An inbound buffer is
203+
// owned by the caller, not the native module, so NSData (not NSMutableData) is
204+
// the correct read-only contract. Copying makes the NSData self-contained and
205+
// safe to retain in a block, store, or dispatch to another thread, regardless of
206+
// whether the bytes were owned by JS (valid only for this callstack) or by a
207+
// native MutableBuffer (which the JS ArrayBuffer may GC concurrently).
208+
static NSData *convertJSIArrayBufferToNSData(jsi::Runtime &rt, const jsi::ArrayBuffer &value)
210209
{
211-
return [NSMutableData dataWithBytes:value.data(rt) length:value.size(rt)];
210+
return [NSData dataWithBytes:value.data(rt) length:value.size(rt)];
212211
}
213212

214213
id convertJSIValueToObjCObject(
@@ -241,7 +240,7 @@ id convertJSIValueToObjCObject(
241240
return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker);
242241
}
243242
if (o.isArrayBuffer(runtime)) {
244-
return convertJSIArrayBufferToNSMutableData(runtime, o.getArrayBuffer(runtime));
243+
return convertJSIArrayBufferToNSData(runtime, o.getArrayBuffer(runtime));
245244
}
246245
return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull);
247246
}

packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,17 @@ @implementation RCTTestArrayBufferTurboModule
105105

106106
RCT_EXPORT_MODULE()
107107

108-
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSMutableData *, testMethodWhichMutatesArrayBuffer : (NSMutableData *)buffer)
108+
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSMutableData *, testMethodWhichTransformsArrayBuffer : (NSData *)buffer)
109109
{
110-
auto *bytes = static_cast<uint8_t *>(buffer.mutableBytes);
111-
for (NSUInteger i = 0; i < buffer.length; ++i) {
110+
NSMutableData *result = [buffer mutableCopy];
111+
auto *bytes = static_cast<uint8_t *>(result.mutableBytes);
112+
for (NSUInteger i = 0; i < result.length; ++i) {
112113
bytes[i] = static_cast<uint8_t>((i + 1) * 10);
113114
}
114-
return buffer;
115+
return result;
115116
}
116117

117-
RCT_EXPORT_METHOD(testMethodWhichStoresArrayBuffer : (NSMutableData *)payload)
118+
RCT_EXPORT_METHOD(testMethodWhichStoresArrayBuffer : (NSData *)payload)
118119
{
119120
self.lastReceivedPayload = [payload copy];
120121
}
@@ -169,8 +170,8 @@ - (void)testSyncArrayBufferRoundTrip
169170
auto result = module.invokeObjCMethod(
170171
*rt,
171172
ArrayBufferKind,
172-
"testMethodWhichMutatesArrayBuffer",
173-
@selector(testMethodWhichMutatesArrayBuffer:),
173+
"testMethodWhichTransformsArrayBuffer",
174+
@selector(testMethodWhichTransformsArrayBuffer:),
174175
args,
175176
1);
176177

0 commit comments

Comments
 (0)