Skip to content

Commit d50699e

Browse files
Kamil Paradowskimeta-codesync[bot]
authored andcommitted
RCTArrayBuffer zero-copy class for ObjC TM (#57879)
Summary: iOS TurboModules mapped a JS `ArrayBuffer` to `NSData` on arguments and `NSMutableData` on returns, so every crossing copied — and `NSMutableData` cannot alias foreign memory, so there was no way to express "these bytes live somewhere else". This adds `RCTArrayBuffer` (`packages/react-native/React/Base/`) as the ObjC representation of an `ArrayBuffer`. It carries an `isOwningBytes` flag: an owning buffer can be stored and read from any thread, a non-owning one aliases bytes valid only for the synchronous call that produced it. Codegen now emits `RCTArrayBuffer *` for `ArrayBufferTypeAnnotation` params (was `NSData *`) and returns (was `NSMutableData *`). ## Changelog: [IOS] [BREAKING] - Add `RCTArrayBuffer`, the ObjC representation of a JS `ArrayBuffer` for TurboModules, with an explicit byte-ownership contract Pull Request resolved: #57879 Test Plan: - `RCTTurboModuleArrayBufferTests` — 9 tests over the sync in-place path, `isOwningBytes` on a sync argument, returning one's own argument, the void/Promise copy paths, nesting, and a zero-length round trip. - `RCTTurboModuleTests.mm` adds `testNativeBackedArrayBufferIsAliasedAndKeepsBackingStoreAlive`. - `RCTSampleTurboModule` doubles its argument in place and returns the same buffer, covering the path end to end. - Codegen and C++ API snapshots regenerated. Differential Revision: D115629409
1 parent 0c1fe3a commit d50699e

16 files changed

Lines changed: 605 additions & 129 deletions

File tree

‎packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ exports[`GenerateModuleObjCpp can generate a header file NativeModule specs 1`]
2626
#import <RCTRequired/RCTRequired.h>
2727
#import <RCTTypeSafety/RCTConvertHelpers.h>
2828
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
29+
#import <React/RCTArrayBuffer.h>
2930
#import <React/RCTBridgeModule.h>
3031
#import <React/RCTCxxConvert.h>
3132
#import <React/RCTManagedPointer.h>
@@ -1578,6 +1579,7 @@ exports[`GenerateModuleObjCpp can generate a header file NativeModule specs with
15781579
#import <RCTRequired/RCTRequired.h>
15791580
#import <RCTTypeSafety/RCTConvertHelpers.h>
15801581
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
1582+
#import <React/RCTArrayBuffer.h>
15811583
#import <React/RCTBridgeModule.h>
15821584
#import <React/RCTCxxConvert.h>
15831585
#import <React/RCTManagedPointer.h>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const HeaderFileTemplate = ({
9797
#import <RCTRequired/RCTRequired.h>
9898
#import <RCTTypeSafety/RCTConvertHelpers.h>
9999
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
100+
#import <React/RCTArrayBuffer.h>
100101
#import <React/RCTBridgeModule.h>
101102
#import <React/RCTCxxConvert.h>
102103
#import <React/RCTManagedPointer.h>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ function getParamObjCType(
227227
return notStruct(wrapOptional('NSArray *', !nullable));
228228
}
229229
case 'ArrayBufferTypeAnnotation': {
230-
return notStruct(wrapOptional('NSData *', !nullable));
230+
return notStruct(wrapOptional('RCTArrayBuffer *', !nullable));
231231
}
232232
}
233233

@@ -398,7 +398,7 @@ function getReturnObjCType(
398398
case 'GenericObjectTypeAnnotation':
399399
return wrapOptional('NSDictionary *', isRequired);
400400
case 'ArrayBufferTypeAnnotation':
401-
return wrapOptional('NSMutableData *', isRequired);
401+
return wrapOptional('RCTArrayBuffer *', isRequired);
402402
default:
403403
typeAnnotation.type as 'MixedTypeAnnotation';
404404
throw new Error(

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Map {
2727
#import <RCTRequired/RCTRequired.h>
2828
#import <RCTTypeSafety/RCTConvertHelpers.h>
2929
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
30+
#import <React/RCTArrayBuffer.h>
3031
#import <React/RCTBridgeModule.h>
3132
#import <React/RCTCxxConvert.h>
3233
#import <React/RCTManagedPointer.h>
@@ -92,6 +93,7 @@ Map {
9293
#import <RCTRequired/RCTRequired.h>
9394
#import <RCTTypeSafety/RCTConvertHelpers.h>
9495
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
96+
#import <React/RCTArrayBuffer.h>
9597
#import <React/RCTBridgeModule.h>
9698
#import <React/RCTCxxConvert.h>
9799
#import <React/RCTManagedPointer.h>
@@ -102,9 +104,9 @@ Map {
102104
103105
@protocol NativeSampleTurboModuleSpec <RCTBridgeModule, RCTTurboModule>
104106
105-
- (NSMutableData *)getArrayBuffer;
106-
- (void)voidArrayBuffer:(NSData *)arg;
107-
- (void)voidNullableArrayBuffer:(NSData * _Nullable)arg;
107+
- (RCTArrayBuffer *)getArrayBuffer;
108+
- (void)voidArrayBuffer:(RCTArrayBuffer *)arg;
109+
- (void)voidNullableArrayBuffer:(RCTArrayBuffer * _Nullable)arg;
108110
109111
@end
110112
@@ -159,6 +161,7 @@ Map {
159161
#import <RCTRequired/RCTRequired.h>
160162
#import <RCTTypeSafety/RCTConvertHelpers.h>
161163
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
164+
#import <React/RCTArrayBuffer.h>
162165
#import <React/RCTBridgeModule.h>
163166
#import <React/RCTCxxConvert.h>
164167
#import <React/RCTManagedPointer.h>
@@ -200,6 +203,7 @@ Map {
200203
#import <RCTRequired/RCTRequired.h>
201204
#import <RCTTypeSafety/RCTConvertHelpers.h>
202205
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
206+
#import <React/RCTArrayBuffer.h>
203207
#import <React/RCTBridgeModule.h>
204208
#import <React/RCTCxxConvert.h>
205209
#import <React/RCTManagedPointer.h>
@@ -475,6 +479,7 @@ Map {
475479
#import <RCTRequired/RCTRequired.h>
476480
#import <RCTTypeSafety/RCTConvertHelpers.h>
477481
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
482+
#import <React/RCTArrayBuffer.h>
478483
#import <React/RCTBridgeModule.h>
479484
#import <React/RCTCxxConvert.h>
480485
#import <React/RCTManagedPointer.h>
@@ -516,6 +521,7 @@ Map {
516521
#import <RCTRequired/RCTRequired.h>
517522
#import <RCTTypeSafety/RCTConvertHelpers.h>
518523
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
524+
#import <React/RCTArrayBuffer.h>
519525
#import <React/RCTBridgeModule.h>
520526
#import <React/RCTCxxConvert.h>
521527
#import <React/RCTManagedPointer.h>
@@ -581,6 +587,7 @@ Map {
581587
#import <RCTRequired/RCTRequired.h>
582588
#import <RCTTypeSafety/RCTConvertHelpers.h>
583589
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
590+
#import <React/RCTArrayBuffer.h>
584591
#import <React/RCTBridgeModule.h>
585592
#import <React/RCTCxxConvert.h>
586593
#import <React/RCTManagedPointer.h>
@@ -651,6 +658,7 @@ Map {
651658
#import <RCTRequired/RCTRequired.h>
652659
#import <RCTTypeSafety/RCTConvertHelpers.h>
653660
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
661+
#import <React/RCTArrayBuffer.h>
654662
#import <React/RCTBridgeModule.h>
655663
#import <React/RCTCxxConvert.h>
656664
#import <React/RCTManagedPointer.h>
@@ -836,6 +844,7 @@ Map {
836844
#import <RCTRequired/RCTRequired.h>
837845
#import <RCTTypeSafety/RCTConvertHelpers.h>
838846
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
847+
#import <React/RCTArrayBuffer.h>
839848
#import <React/RCTBridgeModule.h>
840849
#import <React/RCTCxxConvert.h>
841850
#import <React/RCTManagedPointer.h>
@@ -1104,6 +1113,7 @@ Map {
11041113
#import <RCTRequired/RCTRequired.h>
11051114
#import <RCTTypeSafety/RCTConvertHelpers.h>
11061115
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
1116+
#import <React/RCTArrayBuffer.h>
11071117
#import <React/RCTBridgeModule.h>
11081118
#import <React/RCTCxxConvert.h>
11091119
#import <React/RCTManagedPointer.h>
@@ -1233,6 +1243,7 @@ Map {
12331243
#import <RCTRequired/RCTRequired.h>
12341244
#import <RCTTypeSafety/RCTConvertHelpers.h>
12351245
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
1246+
#import <React/RCTArrayBuffer.h>
12361247
#import <React/RCTBridgeModule.h>
12371248
#import <React/RCTCxxConvert.h>
12381249
#import <React/RCTManagedPointer.h>
@@ -1298,6 +1309,7 @@ Map {
12981309
#import <RCTRequired/RCTRequired.h>
12991310
#import <RCTTypeSafety/RCTConvertHelpers.h>
13001311
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
1312+
#import <React/RCTArrayBuffer.h>
13011313
#import <React/RCTBridgeModule.h>
13021314
#import <React/RCTCxxConvert.h>
13031315
#import <React/RCTManagedPointer.h>
@@ -1389,6 +1401,7 @@ Map {
13891401
#import <RCTRequired/RCTRequired.h>
13901402
#import <RCTTypeSafety/RCTConvertHelpers.h>
13911403
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
1404+
#import <React/RCTArrayBuffer.h>
13921405
#import <React/RCTBridgeModule.h>
13931406
#import <React/RCTCxxConvert.h>
13941407
#import <React/RCTManagedPointer.h>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
/**
13+
* A fixed-length byte buffer shared between JS `ArrayBuffer`s and ObjC TurboModules. Fixed
14+
* length lets the backing store alias JS memory; `NSMutableData` cannot do that.
15+
*
16+
* `isOwningBytes`:
17+
* - `YES` — safe to retain and use from any thread (synchronize if aliasing JS
18+
* memory).
19+
* - `NO` — valid only during the synchronous call on the calling thread; copy with
20+
* `arrayBufferWithCopiedBytes:length:` to keep the bytes.
21+
*/
22+
@interface RCTArrayBuffer : NSObject
23+
24+
/**
25+
* NULL when `length` is 0, non-NULL otherwise, matching `NSData.bytes`.
26+
*/
27+
@property (nonatomic, readonly, nullable) void *mutableBytes NS_RETURNS_INNER_POINTER;
28+
29+
@property (nonatomic, readonly) NSUInteger length;
30+
31+
/** Whether the buffer owns its bytes. See the class comment. */
32+
@property (nonatomic, readonly, getter=isOwningBytes) BOOL owningBytes;
33+
34+
/**
35+
* A new zero-filled owning buffer.
36+
*/
37+
+ (instancetype)arrayBufferWithLength:(NSUInteger)length;
38+
39+
/**
40+
* A new owning buffer holding a copy of `bytes`. Passing NULL zero-fills.
41+
*/
42+
+ (instancetype)arrayBufferWithCopiedBytes:(const void *_Nullable)bytes length:(NSUInteger)length;
43+
44+
/**
45+
* An owning buffer aliasing `bytes` without copying. If `cleanup` is non-nil, it runs on dealloc,
46+
* like `-[NSData dataWithBytesNoCopy:length:freeWhenDone:YES]`; if nil, the caller must keep
47+
* `bytes` valid until dealloc. NULL `bytes` with non-zero `length` raises NSInvalidArgumentException.
48+
*/
49+
+ (instancetype)arrayBufferWithOwnedBytes:(nullable void *)bytes
50+
length:(NSUInteger)length
51+
cleanup:(nullable void (^)(void))cleanup;
52+
53+
/**
54+
* A non-owning, zero-copy alias of `bytes`. See the class comment for lifetime rules. NULL `bytes`
55+
* with non-zero `length` raises NSInvalidArgumentException.
56+
*/
57+
+ (instancetype)arrayBufferWithUnownedBytes:(nullable void *)bytes length:(NSUInteger)length;
58+
59+
- (instancetype)init NS_UNAVAILABLE;
60+
+ (instancetype)new NS_UNAVAILABLE;
61+
62+
@end
63+
64+
NS_ASSUME_NONNULL_END
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTArrayBuffer.h"
9+
10+
#include <cstdint>
11+
#include <cstring>
12+
#include <utility>
13+
#include <vector>
14+
15+
@interface RCTArrayBuffer ()
16+
17+
- (instancetype)initWithBytesNoCopy:(nullable void *)bytes
18+
length:(NSUInteger)length
19+
owningBytes:(BOOL)owningBytes
20+
cleanup:(nullable void (^)(void))cleanup NS_DESIGNATED_INITIALIZER;
21+
- (instancetype)initWithCopiedBytes:(const void *_Nullable)bytes length:(NSUInteger)length;
22+
23+
@end
24+
25+
@implementation RCTArrayBuffer {
26+
void *_bytes;
27+
NSUInteger _length;
28+
BOOL _owningBytes;
29+
void (^_cleanup)(void);
30+
std::vector<uint8_t> _copiedBytes;
31+
}
32+
33+
#pragma mark - Initializers
34+
35+
- (instancetype)initWithBytesNoCopy:(void *)bytes
36+
length:(NSUInteger)length
37+
owningBytes:(BOOL)owningBytes
38+
cleanup:(void (^)(void))cleanup
39+
{
40+
if (bytes == NULL && length != 0) {
41+
[NSException raise:NSInvalidArgumentException
42+
format:@"RCTArrayBuffer: NULL bytes with length %lu", (unsigned long)length];
43+
}
44+
45+
if (self = [super init]) {
46+
_bytes = bytes;
47+
_length = length;
48+
_owningBytes = owningBytes;
49+
_cleanup = [cleanup copy];
50+
}
51+
return self;
52+
}
53+
54+
- (instancetype)initWithCopiedBytes:(const void *)bytes length:(NSUInteger)length
55+
{
56+
if (length == 0) {
57+
return [self initWithBytesNoCopy:NULL length:0 owningBytes:YES cleanup:nil];
58+
}
59+
60+
std::vector<uint8_t> copy(length);
61+
if (bytes != NULL) {
62+
std::memcpy(copy.data(), bytes, length);
63+
}
64+
65+
// Moving a vector hands over its heap buffer, so `data()` stays valid in `_copiedBytes`.
66+
if (self = [self initWithBytesNoCopy:copy.data() length:length owningBytes:YES cleanup:nil]) {
67+
_copiedBytes = std::move(copy);
68+
}
69+
return self;
70+
}
71+
72+
+ (instancetype)arrayBufferWithLength:(NSUInteger)length
73+
{
74+
return [[self alloc] initWithCopiedBytes:NULL length:length];
75+
}
76+
77+
+ (instancetype)arrayBufferWithCopiedBytes:(const void *)bytes length:(NSUInteger)length
78+
{
79+
return [[self alloc] initWithCopiedBytes:bytes length:length];
80+
}
81+
82+
+ (instancetype)arrayBufferWithOwnedBytes:(void *)bytes
83+
length:(NSUInteger)length
84+
cleanup:(nullable void (^)(void))cleanup
85+
{
86+
return [[self alloc] initWithBytesNoCopy:bytes length:length owningBytes:YES cleanup:cleanup];
87+
}
88+
89+
+ (instancetype)arrayBufferWithUnownedBytes:(void *)bytes length:(NSUInteger)length
90+
{
91+
return [[self alloc] initWithBytesNoCopy:bytes length:length owningBytes:NO cleanup:nil];
92+
}
93+
94+
#pragma mark - Accessors
95+
96+
- (void *)mutableBytes
97+
{
98+
return _bytes;
99+
}
100+
101+
- (NSUInteger)length
102+
{
103+
return _length;
104+
}
105+
106+
- (BOOL)isOwningBytes
107+
{
108+
return _owningBytes;
109+
}
110+
111+
- (NSString *)description
112+
{
113+
return [NSString stringWithFormat:@"<%@: %p; length = %lu; owningBytes = %@>",
114+
NSStringFromClass([self class]),
115+
self,
116+
(unsigned long)_length,
117+
_owningBytes ? @"YES" : @"NO"];
118+
}
119+
120+
- (void)dealloc
121+
{
122+
if (_cleanup != nil) {
123+
_cleanup();
124+
}
125+
}
126+
127+
@end

0 commit comments

Comments
 (0)