diff --git a/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap b/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap index 9bf2025a3a4f..e0bd0c195c0e 100644 --- a/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap +++ b/packages/react-native-codegen/e2e/deep_imports/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap @@ -26,6 +26,7 @@ exports[`GenerateModuleObjCpp can generate a header file NativeModule specs 1`] #import #import #import +#import #import #import #import @@ -1578,6 +1579,7 @@ exports[`GenerateModuleObjCpp can generate a header file NativeModule specs with #import #import #import +#import #import #import #import diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js index 8d93d9ad29c2..1bf4e32b3275 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js @@ -97,6 +97,7 @@ const HeaderFileTemplate = ({ #import #import #import +#import #import #import #import diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js index 6811646078e2..397516dc5c22 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -227,7 +227,7 @@ function getParamObjCType( return notStruct(wrapOptional('NSArray *', !nullable)); } case 'ArrayBufferTypeAnnotation': { - return notStruct(wrapOptional('NSData *', !nullable)); + return notStruct(wrapOptional('RCTArrayBuffer *', !nullable)); } } @@ -398,7 +398,7 @@ function getReturnObjCType( case 'GenericObjectTypeAnnotation': return wrapOptional('NSDictionary *', isRequired); case 'ArrayBufferTypeAnnotation': - return wrapOptional('NSMutableData *', isRequired); + return wrapOptional('RCTArrayBuffer *', isRequired); default: typeAnnotation.type as 'MixedTypeAnnotation'; throw new Error( diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap index 39ea57ca8300..4726cdcd0d96 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap @@ -27,6 +27,7 @@ Map { #import #import #import +#import #import #import #import @@ -92,6 +93,7 @@ Map { #import #import #import +#import #import #import #import @@ -102,9 +104,9 @@ Map { @protocol NativeSampleTurboModuleSpec -- (NSMutableData *)getArrayBuffer; -- (void)voidArrayBuffer:(NSData *)arg; -- (void)voidNullableArrayBuffer:(NSData * _Nullable)arg; +- (RCTArrayBuffer *)getArrayBuffer; +- (void)voidArrayBuffer:(RCTArrayBuffer *)arg; +- (void)voidNullableArrayBuffer:(RCTArrayBuffer * _Nullable)arg; @end @@ -159,6 +161,7 @@ Map { #import #import #import +#import #import #import #import @@ -200,6 +203,7 @@ Map { #import #import #import +#import #import #import #import @@ -475,6 +479,7 @@ Map { #import #import #import +#import #import #import #import @@ -516,6 +521,7 @@ Map { #import #import #import +#import #import #import #import @@ -581,6 +587,7 @@ Map { #import #import #import +#import #import #import #import @@ -651,6 +658,7 @@ Map { #import #import #import +#import #import #import #import @@ -836,6 +844,7 @@ Map { #import #import #import +#import #import #import #import @@ -1104,6 +1113,7 @@ Map { #import #import #import +#import #import #import #import @@ -1233,6 +1243,7 @@ Map { #import #import #import +#import #import #import #import @@ -1298,6 +1309,7 @@ Map { #import #import #import +#import #import #import #import @@ -1389,6 +1401,7 @@ Map { #import #import #import +#import #import #import #import diff --git a/packages/react-native/React/Base/RCTArrayBuffer.h b/packages/react-native/React/Base/RCTArrayBuffer.h new file mode 100644 index 000000000000..7702d6ff89f8 --- /dev/null +++ b/packages/react-native/React/Base/RCTArrayBuffer.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A fixed-length byte buffer shared between JS `ArrayBuffer`s and ObjC TurboModules. Fixed + * length lets the backing store alias JS memory; `NSMutableData` cannot do that. + * + * `isOwningBytes`: + * - `YES` — safe to retain and use from any thread (synchronize if aliasing JS + * memory). + * - `NO` — valid only during the synchronous call on the calling thread; copy with + * `arrayBufferWithCopiedBytes:length:` to keep the bytes. + */ +@interface RCTArrayBuffer : NSObject + +/** + * NULL when `length` is 0, non-NULL otherwise, matching `NSData.bytes`. + */ +@property (nonatomic, readonly, nullable) void *mutableBytes NS_RETURNS_INNER_POINTER; + +@property (nonatomic, readonly) NSUInteger length; + +/** Whether the buffer owns its bytes. See the class comment. */ +@property (nonatomic, readonly, getter=isOwningBytes) BOOL owningBytes; + +/** + * A new zero-filled owning buffer. + */ ++ (instancetype)arrayBufferWithLength:(NSUInteger)length; + +/** + * A new owning buffer holding a copy of `bytes`. Passing NULL zero-fills. + */ ++ (instancetype)arrayBufferWithCopiedBytes:(nullable const void *)bytes length:(NSUInteger)length; + +/** + * An owning buffer aliasing `bytes` without copying. If `cleanup` is non-nil, it runs on dealloc, + * like `-[NSData dataWithBytesNoCopy:length:freeWhenDone:YES]`; if nil, the caller must keep + * `bytes` valid until dealloc. NULL `bytes` with non-zero `length` raises NSInvalidArgumentException. + */ ++ (instancetype)arrayBufferWithOwnedBytes:(nullable void *)bytes + length:(NSUInteger)length + cleanup:(nullable void (^)(void))cleanup; + +/** + * A non-owning, zero-copy alias of `bytes`. See the class comment for lifetime rules. NULL `bytes` + * with non-zero `length` raises NSInvalidArgumentException. + */ ++ (instancetype)arrayBufferWithUnownedBytes:(nullable void *)bytes length:(NSUInteger)length; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Base/RCTArrayBuffer.mm b/packages/react-native/React/Base/RCTArrayBuffer.mm new file mode 100644 index 000000000000..e76451cffb65 --- /dev/null +++ b/packages/react-native/React/Base/RCTArrayBuffer.mm @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTArrayBuffer.h" + +@interface RCTArrayBuffer () + +- (instancetype)initWithBytesNoCopy:(nullable void *)bytes + length:(NSUInteger)length + owningBytes:(BOOL)owningBytes + cleanup:(nullable void (^)(void))cleanup NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithCopiedBytes:(nullable const void *)bytes length:(NSUInteger)length; + +@end + +@implementation RCTArrayBuffer { + void *_bytes; + NSUInteger _length; + BOOL _owningBytes; + void (^_cleanup)(void); +} + +#pragma mark - Initializers + +- (instancetype)initWithBytesNoCopy:(void *)bytes + length:(NSUInteger)length + owningBytes:(BOOL)owningBytes + cleanup:(void (^)(void))cleanup +{ + if (bytes == NULL && length != 0) { + [NSException raise:NSInvalidArgumentException + format:@"RCTArrayBuffer: NULL bytes with length %lu", (unsigned long)length]; + } + + if (self = [super init]) { + _bytes = bytes; + _length = length; + _owningBytes = owningBytes; + _cleanup = [cleanup copy]; + } + return self; +} + +- (instancetype)initWithCopiedBytes:(const void *)bytes length:(NSUInteger)length +{ + if (length == 0) { + return [self initWithBytesNoCopy:NULL length:0 owningBytes:YES cleanup:nil]; + } + + void *copy = malloc(length); + if (copy == NULL) { + [NSException raise:NSMallocException format:@"RCTArrayBuffer: failed to allocate %lu bytes", (unsigned long)length]; + } + if (bytes != NULL) { + memcpy(copy, bytes, length); + } else { + memset(copy, 0, length); + } + + return [self initWithBytesNoCopy:copy + length:length + owningBytes:YES + cleanup:^{ + free(copy); + }]; +} + ++ (instancetype)arrayBufferWithLength:(NSUInteger)length +{ + return [[self alloc] initWithCopiedBytes:NULL length:length]; +} + ++ (instancetype)arrayBufferWithCopiedBytes:(const void *)bytes length:(NSUInteger)length +{ + return [[self alloc] initWithCopiedBytes:bytes length:length]; +} + ++ (instancetype)arrayBufferWithOwnedBytes:(void *)bytes + length:(NSUInteger)length + cleanup:(nullable void (^)(void))cleanup +{ + return [[self alloc] initWithBytesNoCopy:bytes length:length owningBytes:YES cleanup:cleanup]; +} + ++ (instancetype)arrayBufferWithUnownedBytes:(void *)bytes length:(NSUInteger)length +{ + return [[self alloc] initWithBytesNoCopy:bytes length:length owningBytes:NO cleanup:nil]; +} + +#pragma mark - Accessors + +- (void *)mutableBytes +{ + return _bytes; +} + +- (NSUInteger)length +{ + return _length; +} + +- (BOOL)isOwningBytes +{ + return _owningBytes; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; length = %lu; owningBytes = %@>", + NSStringFromClass([self class]), + self, + (unsigned long)_length, + _owningBytes ? @"YES" : @"NO"]; +} + +- (void)dealloc +{ + if (_cleanup != nil) { + _cleanup(); + } +} + +@end diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm b/packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm index 41612d2279b3..88ac5ea2da06 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm @@ -7,6 +7,7 @@ #import +#import #import #import #import @@ -123,11 +124,9 @@ - (void)testInvokeTurboModuleWithNull OCMVerify(OCMTimes(1), [instance_ testMethodWhichTakesObject:nil]); } -// A JS ArrayBuffer converts (on the argument path) to an immutable NSData that -// owns an independent copy of the bytes, so the result stays valid after the -// source buffer is gone. This covers the ArrayBuffer-backed-by-native-MutableBuffer -// case, which is the one that could in principle have been aliased zero-copy. -- (void)testArrayBufferConvertsToIndependentNSData +// A native-backed ArrayBuffer is aliased rather than copied, and the RCTArrayBuffer retains +// the backing MutableBuffer, so the alias outlives the JS object. +- (void)testNativeBackedArrayBufferIsAliasedAndKeepsBackingStoreAlive { constexpr size_t kBufferSize = 64 * 1024; @@ -136,20 +135,31 @@ - (void)testArrayBufferConvertsToIndependentNSData auto buffer = std::make_shared(kBufferSize); *buffer->data() = 0xAB; + const uint8_t *sourceBytes = buffer->data(); - facebook::jsi::ArrayBuffer arrayBuffer(*rt, buffer); - id converted = - TurboModuleConvertUtils::convertJSIValueToObjCObject(*rt, facebook::jsi::Value(*rt, arrayBuffer), nullptr); + RCTArrayBuffer *converted = nil; + { + facebook::jsi::ArrayBuffer arrayBuffer(*rt, buffer); + id result = + TurboModuleConvertUtils::convertJSIValueToObjCObject(*rt, facebook::jsi::Value(*rt, arrayBuffer), nullptr, NO, NO); + XCTAssertTrue([result isKindOfClass:[RCTArrayBuffer class]]); + converted = (RCTArrayBuffer *)result; + } - XCTAssertTrue([converted isKindOfClass:[NSData class]]); - NSData *data = (NSData *)converted; - XCTAssertEqual(data.length, (NSUInteger)kBufferSize); - XCTAssertEqual(*static_cast(data.bytes), 0xAB); + XCTAssertTrue(converted.isOwningBytes, @"A native-backed buffer must be safe to retain"); + XCTAssertEqual(converted.length, (NSUInteger)kBufferSize); + XCTAssertEqual(converted.mutableBytes, (void *)sourceBytes, @"Bytes must be aliased, not copied"); - // Independent copy: mutating the source MutableBuffer must not write through to - // the NSData. + // Writes through the source are visible, and vice versa: one shared allocation. *buffer->data() = 0xCD; - XCTAssertEqual(*static_cast(data.bytes), 0xAB, @"NSData must not alias the source buffer"); + XCTAssertEqual(*static_cast(converted.mutableBytes), 0xCD); + *static_cast(converted.mutableBytes) = 0xEF; + XCTAssertEqual(*buffer->data(), 0xEF); + + // The RCTArrayBuffer holds the last reference, so the bytes stay valid. + XCTAssertGreaterThan(buffer.use_count(), 1L); + buffer.reset(); + XCTAssertEqual(*static_cast(converted.mutableBytes), 0xEF); } @end diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h index f170c5e0e6ac..62a82b7e61f0 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.h @@ -75,7 +75,8 @@ class JSI_EXPORT ObjCInteropTurboModule : public ObjCTurboModule { const jsi::Value &arg, size_t i, NSInvocation *inv, - NSMutableArray *retainedObjectsForInvocation) override; + NSMutableArray *retainedObjectsForInvocation, + bool mustCopyBytes) override; private: std::vector methodDescriptors_; diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm index b4c11eafaa59..c18326ae45d8 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm @@ -363,7 +363,8 @@ T RCTConvertTo(SEL selector, id json) const jsi::Value &jsiArg, size_t index, NSInvocation *inv, - NSMutableArray *retainedObjectsForInvocation) + NSMutableArray *retainedObjectsForInvocation, + [[maybe_unused]] bool mustCopyBytes) { NSString *methodName = @(methodNameCStr); std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h index d3fc87e6f148..fcd3231c3833 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h @@ -35,7 +35,8 @@ id convertJSIValueToObjCObject( jsi::Runtime &runtime, const jsi::Value &value, const std::shared_ptr &jsInvoker, - BOOL useNSNull = NO); + BOOL useNSNull = NO, + BOOL mustCopyBytes = YES); } // namespace TurboModuleConvertUtils template <> @@ -113,6 +114,9 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { * ObjCInteropTurboModule relies heavily on RCTConvert to convert arguments from JavaScript values to Objective C * values. ObjCTurboModule tries to minimize reliance on RCTConvert: RCTConvert uses the RCT_EXPORT_METHOD macros, * which we want to remove long term from React Native. + * + * mustCopyBytes says whether the invocation may outlive the JS call, in which case ArrayBuffer arguments must be + * copied rather than aliased. */ virtual void setInvocationArg( jsi::Runtime &runtime, @@ -121,7 +125,8 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { const jsi::Value &arg, size_t i, NSInvocation *inv, - NSMutableArray *retainedObjectsForInvocation); + NSMutableArray *retainedObjectsForInvocation, + bool mustCopyBytes); private: // Does the NativeModule dispatch async methods to the JS thread? @@ -137,11 +142,13 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { NSDictionary *> *methodArgumentTypeNames_; bool isMethodSync(TurboModuleMethodValueKind returnType); + bool mustCopyJSHeapArrayBufferBytes(TurboModuleMethodValueKind returnType); BOOL hasMethodArgConversionSelector(NSString *methodName, size_t argIndex); SEL getMethodArgConversionSelector(NSString *methodName, size_t argIndex); NSInvocation *createMethodInvocation( jsi::Runtime &runtime, bool isSync, + bool mustCopyBytes, const char *methodName, SEL selector, const jsi::Value *args, diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm index ac0753934520..61f25e2ac7a6 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm @@ -7,6 +7,7 @@ #import "RCTTurboModule.h" +#import #import #import #import @@ -89,35 +90,42 @@ static int32_t getUniqueId() return result; } -static jsi::ArrayBuffer convertNSMutableDataToJSIArrayBuffer(jsi::Runtime &runtime, NSMutableData *value) +static jsi::ArrayBuffer convertRCTArrayBufferToJSIArrayBuffer(jsi::Runtime &runtime, RCTArrayBuffer *value) { - class NSMutableDataBuffer final : public jsi::MutableBuffer { + class RCTArrayBufferMutableBuffer final : public jsi::MutableBuffer { public: - explicit NSMutableDataBuffer(NSMutableData *data) : data_(data) {} + explicit RCTArrayBufferMutableBuffer(RCTArrayBuffer *buffer) + : buffer_(buffer), data_(static_cast(buffer.mutableBytes)), size_(buffer.length) + { + } size_t size() const override { - return data_.length; + return size_; } uint8_t *data() override { - return static_cast(data_.mutableBytes); + return data_; } private: - NSMutableData *data_; + RCTArrayBuffer *buffer_; + uint8_t *data_; + size_t size_; }; - // A nil NSMutableData would silently yield a NULL `mutableBytes` pointer - // (ObjC nil-messaging) and corrupt the resulting ArrayBuffer. Substitute a - // fresh empty NSMutableData so the wrapped pointer is always well-defined. + // nil would yield a NULL pointer via ObjC nil-messaging and corrupt the ArrayBuffer. if (value == nil) { - RCTLogWarn(@"convertNSMutableDataToJSIArrayBuffer: received nil NSMutableData; returning empty ArrayBuffer"); - value = [NSMutableData data]; + RCTLogWarn(@"convertRCTArrayBufferToJSIArrayBuffer: received nil RCTArrayBuffer; returning empty ArrayBuffer"); + value = [RCTArrayBuffer arrayBufferWithLength:0]; + } + + if (!value.isOwningBytes) { + value = [RCTArrayBuffer arrayBufferWithCopiedBytes:value.mutableBytes length:value.length]; } - auto buffer = std::make_shared(value); - return {runtime, std::move(buffer)}; + + return {runtime, std::make_shared(value)}; } jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) @@ -133,8 +141,8 @@ size_t size() const override return convertNSDictionaryToJSIObject(runtime, (NSDictionary *)value); } else if ([value isKindOfClass:[NSArray class]]) { return convertNSArrayToJSIArray(runtime, (NSArray *)value); - } else if ([value isKindOfClass:[NSMutableData class]]) { - return convertNSMutableDataToJSIArrayBuffer(runtime, (NSMutableData *)value); + } else if ([value isKindOfClass:[RCTArrayBuffer class]]) { + return convertRCTArrayBufferToJSIArrayBuffer(runtime, (RCTArrayBuffer *)value); } else if (value == (id)kCFNull) { return jsi::Value::null(); } @@ -199,22 +207,40 @@ size_t size() const override }; } -// Copy the ArrayBuffer's bytes into an immutable NSData. An inbound buffer is -// owned by the caller, not the native module, so NSData (not NSMutableData) is -// the correct read-only contract. Copying makes the NSData self-contained and -// safe to retain in a block, store, or dispatch to another thread, regardless of -// whether the bytes were owned by JS (valid only for this callstack) or by a -// native MutableBuffer (which the JS ArrayBuffer may GC concurrently). -static NSData *convertJSIArrayBufferToNSData(jsi::Runtime &rt, const jsi::ArrayBuffer &value) +// Native-backed buffers are aliased and keep their backing store alive, so they stay valid for +// as long as the module holds them. JS-heap buffers have nothing to retain — their bytes are +// freed when the ArrayBuffer is collected or detached — so `mustCopyBytes` is set whenever the +// invocation may outlive the JS call, and they are aliased only when it cannot. +static RCTArrayBuffer * +convertJSIArrayBufferToRCTArrayBuffer(jsi::Runtime &rt, const jsi::ArrayBuffer &arrayBuffer, BOOL mustCopyBytes) { - return [NSData dataWithBytes:value.data(rt) length:value.size(rt)]; + if (auto nativeBuffer = arrayBuffer.tryGetMutableBuffer(rt)) { + auto keepAlive = std::move(nativeBuffer); + return [RCTArrayBuffer arrayBufferWithOwnedBytes:keepAlive->data() + length:keepAlive->size() + cleanup:^{ + (void)keepAlive; + }]; + } + + void *bytes = arrayBuffer.data(rt); + size_t size = arrayBuffer.size(rt); + + // The bytes belong to the JS heap -> async call. + if (mustCopyBytes) { + return [RCTArrayBuffer arrayBufferWithCopiedBytes:bytes length:size]; + } + + // The bytes belong to the JS heap -> sync call. + return [RCTArrayBuffer arrayBufferWithUnownedBytes:bytes length:size]; } id convertJSIValueToObjCObject( jsi::Runtime &runtime, const jsi::Value &value, const std::shared_ptr &jsInvoker, - BOOL useNSNull) + BOOL useNSNull, + BOOL mustCopyBytes) { if (value.isUndefined() || (value.isNull() && !useNSNull)) { return nil; @@ -240,7 +266,7 @@ id convertJSIValueToObjCObject( return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker); } if (o.isArrayBuffer(runtime)) { - return convertJSIArrayBufferToNSData(runtime, o.getArrayBuffer(runtime)); + return convertJSIArrayBufferToRCTArrayBuffer(runtime, o.getArrayBuffer(runtime), mustCopyBytes); } return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull); } @@ -570,11 +596,11 @@ TraceSection s( break; } case ArrayBufferKind: { - if (result != nil && ![result isKindOfClass:[NSMutableData class]]) { - RCTLogError(@"convertReturnIdToJSIValue: expected NSMutableData for ArrayBufferKind, got %@", [result class]); + if (result != nil && ![result isKindOfClass:[RCTArrayBuffer class]]) { + RCTLogError(@"convertReturnIdToJSIValue: expected RCTArrayBuffer for ArrayBufferKind, got %@", [result class]); break; } - returnValue = convertNSMutableDataToJSIArrayBuffer(runtime, (NSMutableData *)result); + returnValue = convertRCTArrayBufferToJSIArrayBuffer(runtime, (RCTArrayBuffer *)result); break; } case FunctionKind: @@ -651,7 +677,8 @@ TraceSection s( const jsi::Value &arg, size_t i, NSInvocation *inv, - NSMutableArray *retainedObjectsForInvocation) + NSMutableArray *retainedObjectsForInvocation, + bool mustCopyBytes) { if (arg.isBool()) { bool v = arg.getBool(); @@ -694,7 +721,8 @@ TraceSection s( * Convert arg to ObjC objects. */ BOOL enableModuleArgumentNSNullConversionIOS = ReactNativeFeatureFlags::enableModuleArgumentNSNullConversionIOS(); - id objCArg = convertJSIValueToObjCObject(runtime, arg, jsInvoker_, enableModuleArgumentNSNullConversionIOS); + id objCArg = + convertJSIValueToObjCObject(runtime, arg, jsInvoker_, enableModuleArgumentNSNullConversionIOS, mustCopyBytes); if (objCArg != nullptr) { NSString *methodNameNSString = @(methodName); @@ -755,6 +783,7 @@ TraceSection s( NSInvocation *ObjCTurboModule::createMethodInvocation( jsi::Runtime &runtime, bool isSync, + bool mustCopyBytes, const char *methodName, SEL selector, const jsi::Value *args, @@ -783,7 +812,7 @@ TraceSection s( for (size_t i = 0; i < count; i++) { const jsi::Value &arg = args[i]; const std::string objCArgType = [methodSignature getArgumentTypeAtIndex:i + 2]; - setInvocationArg(runtime, methodName, objCArgType, arg, i, inv, retainedObjectsForInvocation); + setInvocationArg(runtime, methodName, objCArgType, arg, i, inv, retainedObjectsForInvocation, mustCopyBytes); } if (isSync) { @@ -804,6 +833,12 @@ TraceSection s( return returnType != VoidKind && returnType != PromiseKind; } +bool ObjCTurboModule::mustCopyJSHeapArrayBufferBytes(TurboModuleMethodValueKind returnType) +{ + // Void always dispatches via invokeAsync and promise may. + return returnType == VoidKind || returnType == PromiseKind; +} + ObjCTurboModule::ObjCTurboModule(const InitParams ¶ms) : TurboModule(params.moduleName, params.jsInvoker), instance_(params.instance), @@ -824,6 +859,7 @@ TraceSection s( const char *methodName = methodNameStr.c_str(); bool isSyncInvocation = isMethodSync(returnType); + bool mustCopyBytes = mustCopyJSHeapArrayBufferBytes(returnType); if (isSyncInvocation) { TurboModulePerfLogger::syncMethodCallStart(moduleName, methodName); @@ -833,7 +869,7 @@ TraceSection s( NSMutableArray *retainedObjectsForInvocation = [NSMutableArray arrayWithCapacity:count + 2]; NSInvocation *inv = createMethodInvocation( - runtime, isSyncInvocation, methodName, selector, args, count, retainedObjectsForInvocation); + runtime, isSyncInvocation, mustCopyBytes, methodName, selector, args, count, retainedObjectsForInvocation); jsi::Value returnValue = jsi::Value::undefined(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm index aba2946a7f44..d1081238cc82 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm @@ -6,6 +6,8 @@ */ #import "RCTSampleTurboModule.h" + +#import #import "RCTSampleTurboModulePlugin.h" #import @@ -145,21 +147,23 @@ - (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)runtime }; } -// Arguments arrive as an immutable NSData, but an ArrayBuffer return must be -// NSMutableData: it is handed to JS as a jsi::MutableBuffer, whose data() is -// non-const. Echoing the argument back therefore needs a mutable copy. -RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSMutableData *, getArrayBuffer : (NSData *)buffer) +// The argument aliases the JS ArrayBuffer's bytes, so mutating in place is visible to JS. +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(RCTArrayBuffer *, getArrayBuffer : (RCTArrayBuffer *)buffer) { - return [buffer mutableCopy]; + auto *bytes = static_cast(buffer.mutableBytes); + for (NSUInteger i = 0; i < buffer.length; ++i) { + bytes[i] = static_cast(bytes[i] * 2); + } + return buffer; } -RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSMutableData *, createNativeBuffer : (double)size) +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(RCTArrayBuffer *, createNativeBuffer : (double)size) { - return [NSMutableData dataWithLength:(NSUInteger)size]; + return [RCTArrayBuffer arrayBufferWithLength:(NSUInteger)size]; } RCT_EXPORT_METHOD( - processAsyncBuffer : (NSData *)payload resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock) + processAsyncBuffer : (RCTArrayBuffer *)payload resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock) reject) { resolve(@(payload.length)); diff --git a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm index 156af187618d..749530c2ccfd 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm +++ b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm @@ -7,6 +7,7 @@ #import +#import #import #import #import @@ -29,14 +30,14 @@ return facebook::hermes::makeHermesRuntime(::hermes::vm::RuntimeConfig::Builder().withMicrotaskQueue(true).build()); } -NSMutableData *createIntegerSequenceData(NSUInteger size) +RCTArrayBuffer *createIntegerSequenceBuffer(NSUInteger size) { - NSMutableData *data = [NSMutableData dataWithLength:size]; - auto *bytes = static_cast(data.mutableBytes); + RCTArrayBuffer *buffer = [RCTArrayBuffer arrayBufferWithLength:size]; + auto *bytes = static_cast(buffer.mutableBytes); for (NSUInteger i = 0; i < size; ++i) { bytes[i] = static_cast(i); } - return data; + return buffer; } std::vector bytesFromData(NSData *data) @@ -98,6 +99,8 @@ void flushQueue() @interface RCTTestArrayBufferTurboModule : NSObject @property (nonatomic, copy) NSData *lastReceivedPayload; +@property (nonatomic, assign) BOOL sawAliasedBytes; +@property (nonatomic, assign) BOOL sawUnownedBytes; @end @@ -105,25 +108,43 @@ @implementation RCTTestArrayBufferTurboModule RCT_EXPORT_MODULE() -RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSMutableData *, testMethodWhichTransformsArrayBuffer : (NSData *)buffer) +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(RCTArrayBuffer *, testMethodWhichTransformsArrayBuffer : (RCTArrayBuffer *)buffer) { - NSMutableData *result = [buffer mutableCopy]; - auto *bytes = static_cast(result.mutableBytes); - for (NSUInteger i = 0; i < result.length; ++i) { - bytes[i] = static_cast((i + 1) * 10); + RCTArrayBuffer *result = [RCTArrayBuffer arrayBufferWithLength:buffer.length]; + auto *destinationBytes = static_cast(result.mutableBytes); + for (NSUInteger i = 0; i < buffer.length; ++i) { + destinationBytes[i] = static_cast((i + 1) * 10); } return result; } -RCT_EXPORT_METHOD(testMethodWhichStoresArrayBuffer : (NSData *)payload) +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(RCTArrayBuffer *, testMethodWhichReturnsItsArgument : (RCTArrayBuffer *)buffer) { - self.lastReceivedPayload = [payload copy]; + auto *bytes = static_cast(buffer.mutableBytes); + for (NSUInteger i = 0; i < buffer.length && i < 3; ++i) { + bytes[i] = static_cast(99 - i); + } + return buffer; +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, testMethodWhichChecksArrayBufferAliasing : (RCTArrayBuffer *)buffer) +{ + // An observable in-place write proves the bytes were aliased, not copied on the way in. + auto *bytes = static_cast(buffer.mutableBytes); + bytes[0] = 77; + self.sawAliasedBytes = buffer.length == 3 && bytes[0] == 77; + self.sawUnownedBytes = !buffer.isOwningBytes; + return @(YES); } -RCT_EXPORT_METHOD(testMethodWhichStoresNestedArrayBuffer : (NSDictionary *)payload) +RCT_EXPORT_METHOD(testMethodWhichStoresArrayBuffer : (RCTArrayBuffer *)payload) { - NSDictionary *nestedPayload = payload[@"nested"]; - self.lastReceivedPayload = [nestedPayload[@"buffer"] copy]; + self.lastReceivedPayload = [NSData dataWithBytes:payload.mutableBytes length:payload.length]; +} + +RCT_EXPORT_METHOD(testMethodWhichCallsBackWithArrayBuffer : (double)size callback : (RCTResponseSenderBlock)callback) +{ + callback(@[ createIntegerSequenceBuffer(static_cast(size)) ]); } RCT_EXPORT_METHOD( @@ -134,7 +155,7 @@ @implementation RCTTestArrayBufferTurboModule return; } - resolve(createIntegerSequenceData(static_cast(size))); + resolve(createIntegerSequenceBuffer(static_cast(size))); } @end @@ -186,18 +207,18 @@ - (void)testSyncArrayBufferRoundTrip XCTAssertEqual(returnedBytes[2], 30); } -- (void)testAsyncJSBackedArrayBufferIsCopied +// The sync argument is a live alias: an in-place write lands on the JS ArrayBuffer itself. +- (void)testJSBackedArrayBufferIsNotCopiedDuringTheCall { auto hermesRuntime = createHermesRuntime(); facebook::jsi::Runtime *rt = hermesRuntime.get(); - auto nativeInvoker = std::make_shared(); auto *instance = [RCTTestArrayBufferTurboModule new]; ObjCTurboModule::InitParams params = { .moduleName = "TestModule", .instance = instance, .jsInvoker = nullptr, - .nativeMethodCallInvoker = nativeInvoker, + .nativeMethodCallInvoker = std::make_shared(), .isSyncModule = false, }; ObjCTurboModule module(params); @@ -210,23 +231,93 @@ - (void)testAsyncJSBackedArrayBufferIsCopied facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, sourceBuffer)}; module.invokeObjCMethod( - *rt, VoidKind, "testMethodWhichStoresArrayBuffer", @selector(testMethodWhichStoresArrayBuffer:), args, 1); + *rt, + BooleanKind, + "testMethodWhichChecksArrayBufferAliasing", + @selector(testMethodWhichChecksArrayBufferAliasing:), + args, + 1); - auto *sourceBytes = sourceBuffer.data(*rt); - sourceBytes[0] = 9; - sourceBytes[1] = 8; - sourceBytes[2] = 7; + XCTAssertTrue(instance.sawAliasedBytes, @"The argument must alias the JS ArrayBuffer's bytes"); + XCTAssertTrue(instance.sawUnownedBytes, @"A JS-heap argument to a sync method must not own its bytes"); + XCTAssertEqual(bytesFromArrayBuffer(*rt, sourceBuffer)[0], 77, @"The native write must land on the JS ArrayBuffer"); +} - nativeInvoker->flushQueue(); +// Returning the argument must hand JS the mutated bytes: it has to stay valid through the +// return-value conversion. +- (void)testSyncMethodCanReturnItsOwnArgument +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; - auto receivedBytes = bytesFromData(instance.lastReceivedPayload); - XCTAssertEqual(receivedBytes.size(), 3u); - XCTAssertEqual(receivedBytes[0], 1); - XCTAssertEqual(receivedBytes[1], 2); - XCTAssertEqual(receivedBytes[2], 3); + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto sourceBuffer = rt->global() + .getPropertyAsFunction(*rt, "eval") + .call(*rt, "new Uint8Array([1, 2, 3]).buffer") + .asObject(*rt) + .getArrayBuffer(*rt); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, sourceBuffer)}; + + auto result = module.invokeObjCMethod( + *rt, + ArrayBufferKind, + "testMethodWhichReturnsItsArgument", + @selector(testMethodWhichReturnsItsArgument:), + args, + 1); + + XCTAssertTrue(result.isObject()); + XCTAssertTrue(result.asObject(*rt).isArrayBuffer(*rt)); + auto returnedBytes = bytesFromArrayBuffer(*rt, result.asObject(*rt).getArrayBuffer(*rt)); + XCTAssertEqual(returnedBytes.size(), 3u, @"Returning the argument must not yield an empty buffer"); + XCTAssertEqual(returnedBytes[0], 99); + XCTAssertEqual(returnedBytes[1], 98); + XCTAssertEqual(returnedBytes[2], 97); } -- (void)testAsyncNestedJSBackedArrayBufferIsCopied +// A zero-length ArrayBuffer must round-trip, NULL bytes pointer and all. +- (void)testEmptyArrayBufferRoundTrip +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto sourceBuffer = + rt->global().getPropertyAsFunction(*rt, "eval").call(*rt, "new ArrayBuffer(0)").asObject(*rt).getArrayBuffer(*rt); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, sourceBuffer)}; + + auto result = module.invokeObjCMethod( + *rt, + ArrayBufferKind, + "testMethodWhichTransformsArrayBuffer", + @selector(testMethodWhichTransformsArrayBuffer:), + args, + 1); + + XCTAssertTrue(result.isObject()); + XCTAssertTrue(result.asObject(*rt).isArrayBuffer(*rt)); + XCTAssertEqual(result.asObject(*rt).getArrayBuffer(*rt).size(*rt), 0u); +} + +- (void)testAsyncJSBackedArrayBufferIsCopied { auto hermesRuntime = createHermesRuntime(); facebook::jsi::Runtime *rt = hermesRuntime.get(); @@ -242,24 +333,15 @@ - (void)testAsyncNestedJSBackedArrayBufferIsCopied }; ObjCTurboModule module(params); - auto sourceObject = rt->global() + auto sourceBuffer = rt->global() .getPropertyAsFunction(*rt, "eval") - .call(*rt, "({nested: {buffer: new Uint8Array([1, 2, 3]).buffer}})") - .asObject(*rt); - auto sourceBuffer = sourceObject.getProperty(*rt, "nested") - .asObject(*rt) - .getProperty(*rt, "buffer") + .call(*rt, "new Uint8Array([1, 2, 3]).buffer") .asObject(*rt) .getArrayBuffer(*rt); - facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, sourceObject)}; + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, sourceBuffer)}; module.invokeObjCMethod( - *rt, - VoidKind, - "testMethodWhichStoresNestedArrayBuffer", - @selector(testMethodWhichStoresNestedArrayBuffer:), - args, - 1); + *rt, VoidKind, "testMethodWhichStoresArrayBuffer", @selector(testMethodWhichStoresArrayBuffer:), args, 1); auto *sourceBytes = sourceBuffer.data(*rt); sourceBytes[0] = 9; @@ -312,6 +394,56 @@ - (void)testAsyncNativeBackedArrayBufferRetainsBackingStore XCTAssertEqual(receivedBytes[3], 7); } +// A buffer handed to a callback is delivered to JS after the native call has returned. +- (void)testCallbackDeliversArrayBuffer +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto jsInvoker = std::make_shared(*rt); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = jsInvoker, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + std::vector callbackBytes; + auto onCallback = facebook::jsi::Function::createFromHostFunction( + *rt, + facebook::jsi::PropNameID::forAscii(*rt, "onCallback"), + 1, + [&callbackBytes]( + facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &, + const facebook::jsi::Value *callbackArgs, + size_t count) -> facebook::jsi::Value { + if (count == 1 && callbackArgs[0].isObject() && callbackArgs[0].asObject(runtime).isArrayBuffer(runtime)) { + callbackBytes = bytesFromArrayBuffer(runtime, callbackArgs[0].asObject(runtime).getArrayBuffer(runtime)); + } + return facebook::jsi::Value::undefined(); + }); + facebook::jsi::Value args[2] = {facebook::jsi::Value(3.0), facebook::jsi::Value(*rt, onCallback)}; + + module.invokeObjCMethod( + *rt, + VoidKind, + "testMethodWhichCallsBackWithArrayBuffer", + @selector(testMethodWhichCallsBackWithArrayBuffer:callback:), + args, + 2); + + jsInvoker->flushQueue(); + + XCTAssertEqual(callbackBytes.size(), 3u); + XCTAssertEqual(callbackBytes[0], 0); + XCTAssertEqual(callbackBytes[1], 1); + XCTAssertEqual(callbackBytes[2], 2); +} + - (void)testPromiseResolvesArrayBuffer { auto hermesRuntime = createHermesRuntime(true); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 143a3c05573e..08ed01d20f97 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -656,6 +656,16 @@ interface RCTAppearance : public RCTEventEmitter { public virtual instancetype init(); } +interface RCTArrayBuffer : public NSObject { + public @property (assign, readonly) BOOL owningBytes; + public @property (assign, readonly) NSUInteger length; + public @property (assign, readonly) void* mutableBytes NS_RETURNS_INNER_POINTER; + public virtual static instancetype arrayBufferWithCopiedBytes:length:(_Nullable const void* bytes, NSUInteger length); + public virtual static instancetype arrayBufferWithLength:(NSUInteger length); + public virtual static instancetype arrayBufferWithOwnedBytes:length:cleanup:(_Nullable void* bytes, NSUInteger length, _Nullable void(^)(void) cleanup); + public virtual static instancetype arrayBufferWithUnownedBytes:length:(_Nullable void* bytes, NSUInteger length); +} + interface RCTBackedTextFieldDelegateAdapter : public NSObject { public virtual instancetype initWithTextField:(UITextField* backedTextInputView); public virtual void selectedTextRangeWasSet(); @@ -2502,19 +2512,19 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual NSDictionary* getObjectThrows:(NSDictionary* arg); public virtual NSDictionary* getUnsafeObject:(NSDictionary* arg); public virtual NSDictionary* getValue:y:z:(double x, NSString* y, NSDictionary* z); - public virtual NSMutableData* createNativeBuffer:(double size); - public virtual NSMutableData* getArrayBuffer:(NSData* buffer); public virtual NSNumber* getBool:(BOOL arg); public virtual NSNumber* getEnum:(double arg); public virtual NSNumber* getNumber:(double arg); public virtual NSNumber* getRootTag:(double arg); public virtual NSString* getString:(NSString* arg); + public virtual RCTArrayBuffer* createNativeBuffer:(double size); + public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); public virtual void getImageUrl:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getValueWithCallback:(RCTResponseSenderBlock callback); public virtual void getValueWithPromise:resolve:reject:(BOOL error, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); - public virtual void processAsyncBuffer:resolve:reject:(NSData* payload, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); + public virtual void processAsyncBuffer:resolve:reject:(RCTArrayBuffer* payload, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void promiseAssert:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void promiseThrows:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void voidFunc(); @@ -6132,7 +6142,7 @@ class facebook::react::ObjCInteropTurboModule : public facebook::react::ObjCTurb protected virtual NSString* getArgumentTypeName(facebook::jsi::Runtime& runtime, NSString* methodName, int argIndex) override; protected virtual facebook::jsi::Value convertReturnIdToJSIValue(facebook::jsi::Runtime& runtime, const char* methodName, facebook::react::TurboModuleMethodValueKind returnType, id result) override; protected virtual facebook::jsi::Value create(facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName) override; - protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation) override; + protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation, bool mustCopyBytes) override; public ObjCInteropTurboModule(const facebook::react::ObjCTurboModule::InitParams& params); public virtual std::vector getPropertyNames(facebook::jsi::Runtime& runtime) override; } @@ -6147,7 +6157,7 @@ struct facebook::react::ObjCInteropTurboModule::MethodDescriptor { class facebook::react::ObjCTurboModule : public facebook::react::TurboModule { protected virtual NSString* getArgumentTypeName(facebook::jsi::Runtime& runtime, NSString* methodName, int argIndex); protected virtual facebook::jsi::Value convertReturnIdToJSIValue(facebook::jsi::Runtime& runtime, const char* methodName, facebook::react::TurboModuleMethodValueKind returnType, id result); - protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation); + protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation, bool mustCopyBytes); protected void setEventEmitterCallback(facebook::react::EventEmitterCallback eventEmitterCallback); protected void setMethodArgConversionSelector(NSString* methodName, size_t argIndex, NSString* fnName); public ObjCTurboModule(const facebook::react::ObjCTurboModule::InitParams& params); @@ -13514,7 +13524,7 @@ struct facebook::react::dom::RNMeasureRect { facebook::jsi::Value facebook::react::TurboModuleConvertUtils::convertObjCObjectToJSIValue(facebook::jsi::Runtime& runtime, id value); -id facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& value, const std::shared_ptr& jsInvoker, BOOL useNSNull = NO); +id facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& value, const std::shared_ptr& jsInvoker, BOOL useNSNull = NO, BOOL mustCopyBytes = YES); static const facebook::react::Color facebook::react::HostPlatformColor::UndefinedColor; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 3e868ec9f2f5..d17a3eaf02c7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -654,6 +654,16 @@ interface RCTAppearance : public RCTEventEmitter { public virtual instancetype init(); } +interface RCTArrayBuffer : public NSObject { + public @property (assign, readonly) BOOL owningBytes; + public @property (assign, readonly) NSUInteger length; + public @property (assign, readonly) void* mutableBytes NS_RETURNS_INNER_POINTER; + public virtual static instancetype arrayBufferWithCopiedBytes:length:(_Nullable const void* bytes, NSUInteger length); + public virtual static instancetype arrayBufferWithLength:(NSUInteger length); + public virtual static instancetype arrayBufferWithOwnedBytes:length:cleanup:(_Nullable void* bytes, NSUInteger length, _Nullable void(^)(void) cleanup); + public virtual static instancetype arrayBufferWithUnownedBytes:length:(_Nullable void* bytes, NSUInteger length); +} + interface RCTBackedTextFieldDelegateAdapter : public NSObject { public virtual instancetype initWithTextField:(UITextField* backedTextInputView); public virtual void selectedTextRangeWasSet(); @@ -2491,19 +2501,19 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual NSDictionary* getObjectThrows:(NSDictionary* arg); public virtual NSDictionary* getUnsafeObject:(NSDictionary* arg); public virtual NSDictionary* getValue:y:z:(double x, NSString* y, NSDictionary* z); - public virtual NSMutableData* createNativeBuffer:(double size); - public virtual NSMutableData* getArrayBuffer:(NSData* buffer); public virtual NSNumber* getBool:(BOOL arg); public virtual NSNumber* getEnum:(double arg); public virtual NSNumber* getNumber:(double arg); public virtual NSNumber* getRootTag:(double arg); public virtual NSString* getString:(NSString* arg); + public virtual RCTArrayBuffer* createNativeBuffer:(double size); + public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); public virtual void getImageUrl:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getValueWithCallback:(RCTResponseSenderBlock callback); public virtual void getValueWithPromise:resolve:reject:(BOOL error, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); - public virtual void processAsyncBuffer:resolve:reject:(NSData* payload, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); + public virtual void processAsyncBuffer:resolve:reject:(RCTArrayBuffer* payload, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void promiseAssert:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void promiseThrows:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void voidFunc(); @@ -6014,7 +6024,7 @@ class facebook::react::ObjCInteropTurboModule : public facebook::react::ObjCTurb protected virtual NSString* getArgumentTypeName(facebook::jsi::Runtime& runtime, NSString* methodName, int argIndex) override; protected virtual facebook::jsi::Value convertReturnIdToJSIValue(facebook::jsi::Runtime& runtime, const char* methodName, facebook::react::TurboModuleMethodValueKind returnType, id result) override; protected virtual facebook::jsi::Value create(facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName) override; - protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation) override; + protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation, bool mustCopyBytes) override; public ObjCInteropTurboModule(const facebook::react::ObjCTurboModule::InitParams& params); public virtual std::vector getPropertyNames(facebook::jsi::Runtime& runtime) override; } @@ -6029,7 +6039,7 @@ struct facebook::react::ObjCInteropTurboModule::MethodDescriptor { class facebook::react::ObjCTurboModule : public facebook::react::TurboModule { protected virtual NSString* getArgumentTypeName(facebook::jsi::Runtime& runtime, NSString* methodName, int argIndex); protected virtual facebook::jsi::Value convertReturnIdToJSIValue(facebook::jsi::Runtime& runtime, const char* methodName, facebook::react::TurboModuleMethodValueKind returnType, id result); - protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation); + protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation, bool mustCopyBytes); protected void setEventEmitterCallback(facebook::react::EventEmitterCallback eventEmitterCallback); protected void setMethodArgConversionSelector(NSString* methodName, size_t argIndex, NSString* fnName); public ObjCTurboModule(const facebook::react::ObjCTurboModule::InitParams& params); @@ -13199,7 +13209,7 @@ struct facebook::react::dom::RNMeasureRect { facebook::jsi::Value facebook::react::TurboModuleConvertUtils::convertObjCObjectToJSIValue(facebook::jsi::Runtime& runtime, id value); -id facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& value, const std::shared_ptr& jsInvoker, BOOL useNSNull = NO); +id facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& value, const std::shared_ptr& jsInvoker, BOOL useNSNull = NO, BOOL mustCopyBytes = YES); static const facebook::react::Color facebook::react::HostPlatformColor::UndefinedColor; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 67d36ab67fba..9255278bfc5d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -656,6 +656,16 @@ interface RCTAppearance : public RCTEventEmitter { public virtual instancetype init(); } +interface RCTArrayBuffer : public NSObject { + public @property (assign, readonly) BOOL owningBytes; + public @property (assign, readonly) NSUInteger length; + public @property (assign, readonly) void* mutableBytes NS_RETURNS_INNER_POINTER; + public virtual static instancetype arrayBufferWithCopiedBytes:length:(_Nullable const void* bytes, NSUInteger length); + public virtual static instancetype arrayBufferWithLength:(NSUInteger length); + public virtual static instancetype arrayBufferWithOwnedBytes:length:cleanup:(_Nullable void* bytes, NSUInteger length, _Nullable void(^)(void) cleanup); + public virtual static instancetype arrayBufferWithUnownedBytes:length:(_Nullable void* bytes, NSUInteger length); +} + interface RCTBackedTextFieldDelegateAdapter : public NSObject { public virtual instancetype initWithTextField:(UITextField* backedTextInputView); public virtual void selectedTextRangeWasSet(); @@ -2502,19 +2512,19 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual NSDictionary* getObjectThrows:(NSDictionary* arg); public virtual NSDictionary* getUnsafeObject:(NSDictionary* arg); public virtual NSDictionary* getValue:y:z:(double x, NSString* y, NSDictionary* z); - public virtual NSMutableData* createNativeBuffer:(double size); - public virtual NSMutableData* getArrayBuffer:(NSData* buffer); public virtual NSNumber* getBool:(BOOL arg); public virtual NSNumber* getEnum:(double arg); public virtual NSNumber* getNumber:(double arg); public virtual NSNumber* getRootTag:(double arg); public virtual NSString* getString:(NSString* arg); + public virtual RCTArrayBuffer* createNativeBuffer:(double size); + public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); public virtual void getImageUrl:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getValueWithCallback:(RCTResponseSenderBlock callback); public virtual void getValueWithPromise:resolve:reject:(BOOL error, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); - public virtual void processAsyncBuffer:resolve:reject:(NSData* payload, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); + public virtual void processAsyncBuffer:resolve:reject:(RCTArrayBuffer* payload, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void promiseAssert:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void promiseThrows:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void voidFunc(); @@ -6129,7 +6139,7 @@ class facebook::react::ObjCInteropTurboModule : public facebook::react::ObjCTurb protected virtual NSString* getArgumentTypeName(facebook::jsi::Runtime& runtime, NSString* methodName, int argIndex) override; protected virtual facebook::jsi::Value convertReturnIdToJSIValue(facebook::jsi::Runtime& runtime, const char* methodName, facebook::react::TurboModuleMethodValueKind returnType, id result) override; protected virtual facebook::jsi::Value create(facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName) override; - protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation) override; + protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation, bool mustCopyBytes) override; public ObjCInteropTurboModule(const facebook::react::ObjCTurboModule::InitParams& params); public virtual std::vector getPropertyNames(facebook::jsi::Runtime& runtime) override; } @@ -6144,7 +6154,7 @@ struct facebook::react::ObjCInteropTurboModule::MethodDescriptor { class facebook::react::ObjCTurboModule : public facebook::react::TurboModule { protected virtual NSString* getArgumentTypeName(facebook::jsi::Runtime& runtime, NSString* methodName, int argIndex); protected virtual facebook::jsi::Value convertReturnIdToJSIValue(facebook::jsi::Runtime& runtime, const char* methodName, facebook::react::TurboModuleMethodValueKind returnType, id result); - protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation); + protected virtual void setInvocationArg(facebook::jsi::Runtime& runtime, const char* methodName, const std::string& objCArgType, const facebook::jsi::Value& arg, size_t i, NSInvocation* inv, NSMutableArray* retainedObjectsForInvocation, bool mustCopyBytes); protected void setEventEmitterCallback(facebook::react::EventEmitterCallback eventEmitterCallback); protected void setMethodArgConversionSelector(NSString* methodName, size_t argIndex, NSString* fnName); public ObjCTurboModule(const facebook::react::ObjCTurboModule::InitParams& params); @@ -13377,7 +13387,7 @@ struct facebook::react::dom::RNMeasureRect { facebook::jsi::Value facebook::react::TurboModuleConvertUtils::convertObjCObjectToJSIValue(facebook::jsi::Runtime& runtime, id value); -id facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& value, const std::shared_ptr& jsInvoker, BOOL useNSNull = NO); +id facebook::react::TurboModuleConvertUtils::convertJSIValueToObjCObject(facebook::jsi::Runtime& runtime, const facebook::jsi::Value& value, const std::shared_ptr& jsInvoker, BOOL useNSNull = NO, BOOL mustCopyBytes = YES); static const facebook::react::Color facebook::react::HostPlatformColor::UndefinedColor;