From d5dac1cba77a01f9efc94c7a5fa0d172d5f6e412 Mon Sep 17 00:00:00 2001 From: Facebook Employee Date: Wed, 19 Aug 2026 07:39:03 -0700 Subject: [PATCH 1/4] Copy ArrayBuffer arguments when an ObjC TurboModule method takes a block (#57983) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57983 A JS-heap `ArrayBuffer` argument was aliased, not copied, whenever the method's return kind let the call complete synchronously. That is unsafe if the method also takes a block: the module can hand the buffer to the callback, and JS drains it after the call has returned, by which point the bytes may be gone. The copy decision now also looks at the ObjC method signature, and copies whenever a parameter is a block. Also migrates the macOS sample TurboModule to `RCTArrayBuffer`; it still used `NSData`/`NSMutableData`, which no longer matches the generated spec. Changelog: [iOS][Fixed] - Copy ArrayBuffer arguments in ObjC TurboModules when the method also takes a callback Differential Revision: D115767439 --- .../react-native/React/Base/RCTArrayBuffer.h | 6 +- .../ios/ReactCommon/RCTTurboModule.mm | 29 +++++++- .../RCTTurboModuleArrayBufferTests.mm | 73 +++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/packages/react-native/React/Base/RCTArrayBuffer.h b/packages/react-native/React/Base/RCTArrayBuffer.h index c8d80878819..d60b5cc82f7 100644 --- a/packages/react-native/React/Base/RCTArrayBuffer.h +++ b/packages/react-native/React/Base/RCTArrayBuffer.h @@ -17,7 +17,11 @@ NS_ASSUME_NONNULL_BEGIN * - `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. + * `arrayBufferWithCopiedBytes:length:` to keep the bytes. Such a buffer must not be captured in + * a block or handed to a callback or promise resolve block, which deliver after the call + * returns. The TurboModule framework copies rather than aliases whenever the method signature + * exposes a block parameter, and buffers nested inside `NSArray` or `NSDictionary` arguments + * always own their bytes. */ @interface RCTArrayBuffer : NSObject 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 7270187b414..165226e3952 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 @@ -210,11 +210,29 @@ size_t size() const override }; } +// A block parameter means the module can hand an argument to a callback it invokes after the call +// returns, so nothing passed to such a method may alias the JS heap. +static BOOL methodSignatureTakesBlock(NSMethodSignature *methodSignature) +{ + const char *BLOCK_TYPE = @encode( + __typeof__(^{ + })); + + for (NSUInteger i = 2; i < methodSignature.numberOfArguments; i++) { + const std::string objCArgType = [methodSignature getArgumentTypeAtIndex:i]; + if (objCArgType == BLOCK_TYPE) { + return YES; + } + } + return NO; +} + // 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. Engines that -// don't hand out the backing MutableBuffer fall back to those JS-heap rules for every buffer. +// invocation may outlive the JS call (an async return kind, or a block parameter the module can +// call later), and they are aliased only when it cannot. Engines that don't hand out the backing +// MutableBuffer fall back to those JS-heap rules for every buffer. static RCTArrayBuffer * convertJSIArrayBufferToRCTArrayBuffer(jsi::Runtime &rt, const jsi::ArrayBuffer &arrayBuffer, BOOL mustCopyBytes) { @@ -813,10 +831,15 @@ TraceSection s( NSInvocation *inv = [NSInvocation invocationWithMethodSignature:methodSignature]; [inv setSelector:selector]; + // A block argument can outlive the call, and JS drains it asynchronously, so an ArrayBuffer the + // module hands to it must own its bytes even when the method itself returns synchronously. + BOOL mustCopyArrayBufferBytes = mustCopyBytes || methodSignatureTakesBlock(methodSignature); + 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, mustCopyBytes); + setInvocationArg( + runtime, methodName, objCArgType, arg, i, inv, retainedObjectsForInvocation, mustCopyArrayBufferBytes); } if (isSync) { diff --git a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm index 14f32b48e84..951668d45d2 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm +++ b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm @@ -14,6 +14,7 @@ #import #import +#import #import #import @@ -142,6 +143,15 @@ - (void)testMethodWhichStoresArrayBuffer:(RCTArrayBuffer *)payload self.lastReceivedPayload = [NSData dataWithBytes:payload.mutableBytes length:payload.length]; } +- (NSNumber *)testMethodWhichPassesArrayBufferToCallback:(RCTArrayBuffer *)buffer + callback:(RCTResponseSenderBlock)callback +{ + // The buffer escapes into a callback JS drains after this method returns, so the framework must + // have handed us a copy rather than an alias of the JS heap. + callback(@[ buffer ]); + return @(buffer.isOwningBytes); +} + - (void)testMethodWhichCallsBackWithArrayBuffer:(double)size callback:(RCTResponseSenderBlock)callback { callback(@[ createIntegerSequenceBuffer(static_cast(size)) ]); @@ -444,6 +454,69 @@ - (void)testCallbackDeliversArrayBuffer XCTAssertEqual(callbackBytes[2], 2); } +// A sync method that hands its argument to a callback lets the buffer escape the call, so the +// argument must be copied even though the return kind would otherwise allow aliasing. +- (void)testArrayBufferIsCopiedWhenAMethodTakesACallback +{ + 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); + + auto sourceBuffer = rt->global() + .getPropertyAsFunction(*rt, "eval") + .call(*rt, "new Uint8Array([1, 2, 3]).buffer") + .asObject(*rt) + .getArrayBuffer(*rt); + + 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) { + const auto &callbackArg = *callbackArgs; + if (callbackArg.isObject() && callbackArg.asObject(runtime).isArrayBuffer(runtime)) { + callbackBytes = bytesFromArrayBuffer(runtime, callbackArg.asObject(runtime).getArrayBuffer(runtime)); + } + } + return facebook::jsi::Value::undefined(); + }); + std::array args = { + facebook::jsi::Value(*rt, sourceBuffer), facebook::jsi::Value(*rt, onCallback)}; + + auto result = module.invokeObjCMethod( + *rt, + BooleanKind, + "testMethodWhichPassesArrayBufferToCallback", + @selector(testMethodWhichPassesArrayBufferToCallback:callback:), + args.data(), + 2); + + XCTAssertTrue(result.getBool(), @"A buffer that can escape into a callback must own its bytes"); + + jsInvoker->flushQueue(); + + XCTAssertEqual(callbackBytes.size(), 3u); + XCTAssertEqual(callbackBytes[0], 1); + XCTAssertEqual(callbackBytes[1], 2); + XCTAssertEqual(callbackBytes[2], 3); +} + - (void)testPromiseResolvesArrayBuffer { auto hermesRuntime = createHermesRuntime(true); From 65f82a4214588be03b205451fa37d3cf74a69361 Mon Sep 17 00:00:00 2001 From: Facebook Employee Date: Wed, 19 Aug 2026 07:45:10 -0700 Subject: [PATCH 2/4] Deduplicate the ArrayBuffer helpers and align the ObjC, C++ and JNI implementations Summary: Follow-up cleanup on the `ArrayBuffer` TurboModule plumbing. No behaviour change except the leak fix below. - Move `detail::throwIfDetached` out of the header into a new `react/bridging/ArrayBuffer.cpp`, and drop the `AsyncArrayBuffer::throwIfDetached` wrapper that only forwarded to it. The JNI path now calls `detail::throwIfDetached` directly instead of going through `AsyncArrayBuffer`. - Add `detail::copyToOwnedBuffer`, so the C++ bridging path and the tests build an owning buffer the same way instead of each hand-rolling a `std::vector` copy. The header no longer needs ``/``. - Rename the `JArrayBuffer` factories to say what they do about ownership: `createOwning`/`createOwned`/`createUnowned` become `createWithOwnedBytes`/`createWithCopiedBytes`/`createWithUnownedBytes`, matching the `RCTArrayBuffer` naming. Move `invalidate()` next to the other private members. - `RCTArrayBuffer` uses `synthesize` rather than three hand-written accessors, and normalizes `mutableBytes` to NULL for a zero-length buffer so the documented "NULL exactly when empty" invariant holds for every factory. - Run the caller's `cleanup` block before the designated initializer raises on a NULL/non-zero-length mismatch. Nothing else would ever release those bytes, so raising first leaked them. - Treat only `std::logic_error` from `tryGetMutableBuffer` as "this runtime has no native buffer" on the JNI path, and log it once, instead of swallowing every `std::exception`. - Keep the macOS mirror of `RCTArrayBuffer` byte-identical to the iOS one. Changelog: [General][Changed] - Rename the internal JArrayBuffer factories and deduplicate the ArrayBuffer helpers Differential Revision: D116358593 --- .../react-native/React/Base/RCTArrayBuffer.h | 3 +- .../react-native/React/Base/RCTArrayBuffer.mm | 34 +++++------ .../src/main/jni/react/jni/JArrayBuffer.cpp | 56 +++++++++---------- .../src/main/jni/react/jni/JArrayBuffer.h | 25 +++++---- .../jni/react/jni/test/JArrayBufferTest.cpp | 32 ++++------- .../react/bridging/ArrayBuffer.cpp | 52 +++++++++++++++++ .../ReactCommon/react/bridging/ArrayBuffer.h | 44 +++++++-------- .../core/iostests/RCTTurboModuleTests.mm | 20 +------ .../android/ReactCommon/JavaTurboModule.cpp | 34 +++++++---- .../RCTTurboModuleArrayBufferTests.mm | 19 +++++++ .../api-snapshots/ReactAndroidDebugCxx.api | 9 +-- .../api-snapshots/ReactAndroidNewarchCxx.api | 9 +-- .../api-snapshots/ReactAndroidReleaseCxx.api | 9 +-- .../api-snapshots/ReactAppleDebugCxx.api | 3 +- .../api-snapshots/ReactAppleNewarchCxx.api | 3 +- .../api-snapshots/ReactAppleReleaseCxx.api | 3 +- .../api-snapshots/ReactCommonDebugCxx.api | 3 +- .../api-snapshots/ReactCommonNewarchCxx.api | 3 +- .../api-snapshots/ReactCommonReleaseCxx.api | 3 +- 19 files changed, 206 insertions(+), 158 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/bridging/ArrayBuffer.cpp diff --git a/packages/react-native/React/Base/RCTArrayBuffer.h b/packages/react-native/React/Base/RCTArrayBuffer.h index d60b5cc82f7..0a9140b7bab 100644 --- a/packages/react-native/React/Base/RCTArrayBuffer.h +++ b/packages/react-native/React/Base/RCTArrayBuffer.h @@ -26,7 +26,8 @@ NS_ASSUME_NONNULL_BEGIN @interface RCTArrayBuffer : NSObject /** - * NULL when `length` is 0, non-NULL otherwise, matching `NSData.bytes`. + * NULL when `length` is 0, non-NULL otherwise, matching `NSData.bytes`. Holds for every factory: a + * zero-length buffer normalizes its pointer to NULL. */ @property (nonatomic, readonly, nullable) void *mutableBytes NS_RETURNS_INNER_POINTER; diff --git a/packages/react-native/React/Base/RCTArrayBuffer.mm b/packages/react-native/React/Base/RCTArrayBuffer.mm index c4270b927be..bdb6cf76405 100644 --- a/packages/react-native/React/Base/RCTArrayBuffer.mm +++ b/packages/react-native/React/Base/RCTArrayBuffer.mm @@ -23,13 +23,14 @@ - (instancetype)initWithCopiedBytes:(const void *_Nullable)bytes length:(NSUInte @end @implementation RCTArrayBuffer { - void *_bytes; - NSUInteger _length; - BOOL _owningBytes; void (^_cleanup)(void); std::vector _copiedBytes; } +@synthesize mutableBytes = _bytes; +@synthesize length = _length; +@synthesize owningBytes = _owningBytes; + #pragma mark - Initializers - (instancetype)initWithBytesNoCopy:(void *)bytes @@ -38,12 +39,17 @@ - (instancetype)initWithBytesNoCopy:(void *)bytes cleanup:(void (^)(void))cleanup { if (bytes == NULL && length != 0) { + // Nothing else will ever release whatever `bytes` was meant to be once this fails. + if (cleanup != nil) { + cleanup(); + } [NSException raise:NSInvalidArgumentException format:@"RCTArrayBuffer: NULL bytes with length %lu", (unsigned long)length]; } - if (self = [super init]) { - _bytes = bytes; + if ((self = [super init]) != nil) { + // `mutableBytes` is documented as NULL exactly when empty. + _bytes = length == 0 ? NULL : bytes; _length = length; _owningBytes = owningBytes; _cleanup = [cleanup copy]; @@ -63,7 +69,7 @@ - (instancetype)initWithCopiedBytes:(const void *)bytes length:(NSUInteger)lengt } // Moving a vector hands over its heap buffer, so `data()` stays valid in `_copiedBytes`. - if (self = [self initWithBytesNoCopy:copy.data() length:length owningBytes:YES cleanup:nil]) { + if ((self = [self initWithBytesNoCopy:copy.data() length:length owningBytes:YES cleanup:nil]) != nil) { _copiedBytes = std::move(copy); } return self; @@ -93,21 +99,6 @@ + (instancetype)arrayBufferWithUnownedBytes:(void *)bytes length:(NSUInteger)len #pragma mark - Accessors -- (void *)mutableBytes -{ - return _bytes; -} - -- (NSUInteger)length -{ - return _length; -} - -- (BOOL)isOwningBytes -{ - return _owningBytes; -} - - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p; length = %lu; owningBytes = %@>", @@ -119,6 +110,7 @@ - (NSString *)description - (void)dealloc { + // Runs on whichever thread drops the last reference, so cleanup blocks must be thread-agnostic. if (_cleanup != nil) { _cleanup(); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.cpp index 241f032b9e6..b67a531c8e4 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.cpp @@ -8,10 +8,8 @@ #include "JArrayBuffer.h" #include -#include #include #include -#include #include @@ -49,19 +47,6 @@ jboolean JArrayBuffer::isBytesValid() { return hasBytes() ? JNI_TRUE : JNI_FALSE; } -void JArrayBuffer::invalidate() noexcept { - if (!owningBytes_) { - buffer_.reset(); - } -} - -const std::shared_ptr& JArrayBuffer::mutableBuffer() const { - if (!hasBytes()) { - throw std::runtime_error(kRevokedBorrowMessage); - } - return buffer_; -} - jni::local_ref JArrayBuffer::create( jni::local_ref byteBuffer, std::shared_ptr buffer, @@ -72,22 +57,13 @@ jni::local_ref JArrayBuffer::create( return javaPart; } -jni::local_ref JArrayBuffer::createOwning( +jni::local_ref JArrayBuffer::createWithOwnedBytes( std::shared_ptr buffer) { auto byteBuffer = jni::JByteBuffer::wrapBytes(buffer->data(), buffer->size()); return create(std::move(byteBuffer), std::move(buffer), true); } -jni::local_ref JArrayBuffer::createUnowned( - void* bytes, - size_t size) { - auto byteBuffer = - jni::JByteBuffer::wrapBytes(static_cast(bytes), size); - auto buffer = std::make_shared(byteBuffer); - return create(std::move(byteBuffer), std::move(buffer), false); -} - -jni::local_ref JArrayBuffer::createOwned( +jni::local_ref JArrayBuffer::createWithCopiedBytes( const void* bytes, size_t size) { auto byteBuffer = jni::JByteBuffer::allocateDirect(static_cast(size)); @@ -100,6 +76,15 @@ jni::local_ref JArrayBuffer::createOwned( return create(std::move(byteBuffer), std::move(buffer), true); } +jni::local_ref JArrayBuffer::createWithUnownedBytes( + void* bytes, + size_t size) { + auto byteBuffer = + jni::JByteBuffer::wrapBytes(static_cast(bytes), size); + auto buffer = std::make_shared(byteBuffer); + return create(std::move(byteBuffer), std::move(buffer), false); +} + std::shared_ptr JArrayBuffer::toJSBuffer( jsi::Runtime& runtime, jni::alias_ref arrayBuffer) { @@ -115,15 +100,26 @@ std::shared_ptr JArrayBuffer::toJSBuffer( } const auto& buffer = self->mutableBuffer(); - if (self->owningBytes_) { + if (self->isOwningBytes()) { return buffer; } // Borrowed bytes still belong to the inbound JS ArrayBuffer; copy them before // handing a new buffer back to JS. - auto bytes = std::span(buffer->data(), buffer->size()); - return std::make_shared( - std::vector(bytes.begin(), bytes.end())); + return detail::copyToOwnedBuffer(buffer->data(), buffer->size()); +} + +const std::shared_ptr& JArrayBuffer::mutableBuffer() const { + if (!hasBytes()) { + throw std::runtime_error(kRevokedBorrowMessage); + } + return buffer_; +} + +void JArrayBuffer::invalidate() noexcept { + if (!owningBytes_) { + buffer_.reset(); + } } } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.h b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.h index 23029d35e75..8e34db0cb17 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.h @@ -28,15 +28,16 @@ class JArrayBuffer : public jni::HybridClass { // JS ArrayBuffer with a native MutableBuffer (tryGetMutableBuffer). Retain // the owner so the bytes stay valid after the call. - static jni::local_ref createOwning(std::shared_ptr buffer); - - // JS-heap bytes passed to a synchronous call. Zero-copy for the call only; - // do not retain the result. - static jni::local_ref createUnowned(void *bytes, size_t size); + static jni::local_ref createWithOwnedBytes(std::shared_ptr buffer); // Copy JS-heap bytes into a new owned buffer. Used for async/promise calls // and anywhere the module needs its own copy of the data. - static jni::local_ref createOwned(const void *bytes, size_t size); + static jni::local_ref createWithCopiedBytes(const void *bytes, size_t size); + + // JS-heap bytes passed to a synchronous call. Zero-copy for the call only; + // do not retain the result. No Kotlin equivalent by design: only the + // TurboModule bridge may lend out JS-heap bytes. + static jni::local_ref createWithUnownedBytes(void *bytes, size_t size); // Convert a module return value for rt.createArrayBuffer. Owning buffers pass // through; borrowed ones are copied because createArrayBuffer needs its own @@ -44,12 +45,6 @@ class JArrayBuffer : public jni::HybridClass { // its borrow has been revoked. static std::shared_ptr toJSBuffer(jsi::Runtime &runtime, jni::alias_ref arrayBuffer); - // Revokes access to borrowed bytes. Called when the call frame that lent the - // bytes unwinds, so a module that retained a non-owning ArrayBuffer gets an - // exception instead of reading memory the JS heap has moved or freed. Owning - // buffers are unaffected. - void invalidate() noexcept; - // The bytes this buffer was created over. Throws if a borrow has since been // revoked by invalidate(). const std::shared_ptr &mutableBuffer() const; @@ -66,6 +61,12 @@ class JArrayBuffer : public jni::HybridClass { return owningBytes_; } + // Revokes access to borrowed bytes. Called when the call frame that lent the + // bytes unwinds, so a module that retained a non-owning ArrayBuffer gets an + // exception instead of reading memory the JS heap has moved or freed. Owning + // buffers are unaffected. + void invalidate() noexcept; + JArrayBuffer(std::shared_ptr buffer, bool owningBytes) noexcept : buffer_(std::move(buffer)), owningBytes_(owningBytes) { diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/test/JArrayBufferTest.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/test/JArrayBufferTest.cpp index 8eebde8b2ec..55ff651700b 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/test/JArrayBufferTest.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/test/JArrayBufferTest.cpp @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#include #include #include @@ -19,22 +20,9 @@ namespace facebook::react { namespace { -class TestBuffer final : public jsi::MutableBuffer { - public: - explicit TestBuffer(std::vector bytes) noexcept - : bytes_(std::move(bytes)) {} - - size_t size() const override { - return bytes_.size(); - } - - uint8_t* data() override { - return bytes_.data(); - } - - private: - std::vector bytes_; -}; +std::shared_ptr makeBuffer(std::vector bytes) { + return std::make_shared(std::move(bytes)); +} } // namespace @@ -47,7 +35,7 @@ class TestBuffer final : public jsi::MutableBuffer { */ TEST(JArrayBufferTest, owningBufferExposesItsBytes) { - auto buffer = std::make_shared(std::vector{1, 2, 3}); + auto buffer = makeBuffer({1, 2, 3}); JArrayBuffer arrayBuffer{buffer, true}; EXPECT_TRUE(arrayBuffer.isOwningBytes()); @@ -55,7 +43,7 @@ TEST(JArrayBufferTest, owningBufferExposesItsBytes) { } TEST(JArrayBufferTest, borrowedBufferExposesItsBytesBeforeInvalidation) { - auto buffer = std::make_shared(std::vector{1, 2, 3}); + auto buffer = makeBuffer({1, 2, 3}); JArrayBuffer arrayBuffer{buffer, false}; EXPECT_FALSE(arrayBuffer.isOwningBytes()); @@ -69,7 +57,7 @@ TEST(JArrayBufferTest, borrowedBufferExposesItsBytesBeforeInvalidation) { * occupies that memory. */ TEST(JArrayBufferTest, invalidateRevokesABorrow) { - auto buffer = std::make_shared(std::vector{1, 2, 3}); + auto buffer = makeBuffer({1, 2, 3}); JArrayBuffer arrayBuffer{buffer, false}; arrayBuffer.invalidate(); @@ -78,7 +66,7 @@ TEST(JArrayBufferTest, invalidateRevokesABorrow) { } TEST(JArrayBufferTest, invalidateLeavesAnOwningBufferUsable) { - auto buffer = std::make_shared(std::vector{1, 2, 3}); + auto buffer = makeBuffer({1, 2, 3}); JArrayBuffer arrayBuffer{buffer, true}; arrayBuffer.invalidate(); @@ -87,7 +75,7 @@ TEST(JArrayBufferTest, invalidateLeavesAnOwningBufferUsable) { } TEST(JArrayBufferTest, invalidateIsIdempotent) { - auto buffer = std::make_shared(std::vector{1, 2, 3}); + auto buffer = makeBuffer({1, 2, 3}); JArrayBuffer arrayBuffer{buffer, false}; arrayBuffer.invalidate(); @@ -100,7 +88,7 @@ TEST(JArrayBufferTest, invalidateIsIdempotent) { // aliasing adapter (and the JNI global ref inside it) is torn down with the // call frame rather than at the whim of the Java GC. TEST(JArrayBufferTest, invalidateReleasesTheBorrowedBuffer) { - auto buffer = std::make_shared(std::vector{1, 2, 3}); + auto buffer = makeBuffer({1, 2, 3}); std::weak_ptr weakBuffer = buffer; JArrayBuffer arrayBuffer{std::move(buffer), false}; diff --git a/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.cpp b/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.cpp new file mode 100644 index 00000000000..9177a110d6d --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.cpp @@ -0,0 +1,52 @@ +/* + * 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. + */ + +#include + +#include +#include +#include + +namespace facebook::react::detail { + +void throwIfDetached( + jsi::Runtime& rt, + const jsi::ArrayBuffer& buffer, + const char* callerName) { + // Latched the first time a runtime rejects the check, so the property get and + // the thrown-and-caught exception are paid once rather than per conversion. + // Process-wide because every runtime in a process shares one engine build. + static std::atomic unsupported{false}; + if (unsupported.load(std::memory_order_relaxed)) { + return; + } + + bool detached = false; + try { + detached = buffer.detached(rt); + } catch (const jsi::JSINativeException&) { + unsupported.store(true, std::memory_order_relaxed); + return; + } + if (detached) { + throw jsi::JSError( + rt, std::string(callerName) + ": ArrayBuffer is detached"); + } +} + +std::shared_ptr copyToOwnedBuffer( + const uint8_t* bytes, + size_t size) { + if (size == 0) { + return std::make_shared(std::vector{}); + } + auto span = std::span(bytes, size); + return std::make_shared( + std::vector(span.begin(), span.end())); +} + +} // namespace facebook::react::detail diff --git a/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.h b/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.h index 8739fb2881c..8ff9a92a6c3 100644 --- a/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.h +++ b/packages/react-native/ReactCommon/react/bridging/ArrayBuffer.h @@ -7,8 +7,8 @@ #pragma once -#include -#include +#include +#include #include @@ -40,6 +40,20 @@ class OwnedBytesBuffer final : public jsi::MutableBuffer { std::vector bytes_; }; +/** + * Best-effort detached check. jsi::ArrayBuffer::detached relies on the JS-level + * `detached` property, which some runtimes (e.g. Hermes) don't implement and + * signal by throwing. In that case we silently skip the check rather than + * surfacing a confusing error from an unrelated method, and remember that the + * runtime rejected it so later calls skip it for free. + */ +void throwIfDetached(jsi::Runtime &rt, const jsi::ArrayBuffer &buffer, const char *callerName); + +/** + * Copies bytes into a new owning buffer. `bytes` may be null when `size` is 0. + */ +std::shared_ptr copyToOwnedBuffer(const uint8_t *bytes, size_t size); + } // namespace detail template <> @@ -77,7 +91,7 @@ class AsyncArrayBuffer { // Zero-copy if the input has a native MutableBuffer; copies otherwise. static AsyncArrayBuffer acquire(jsi::Runtime &rt, const jsi::ArrayBuffer &buffer) { - throwIfDetached(rt, buffer, "AsyncArrayBuffer::acquire"); + detail::throwIfDetached(rt, buffer, "AsyncArrayBuffer::acquire"); if (auto mutableBuf = buffer.tryGetMutableBuffer(rt)) { return AsyncArrayBuffer{std::move(mutableBuf)}; } @@ -87,7 +101,7 @@ class AsyncArrayBuffer { // Zero-copy. Throws if the input has no native MutableBuffer. static AsyncArrayBuffer borrow(jsi::Runtime &rt, const jsi::ArrayBuffer &buffer) { - throwIfDetached(rt, buffer, "AsyncArrayBuffer::borrow"); + detail::throwIfDetached(rt, buffer, "AsyncArrayBuffer::borrow"); auto mutableBuf = buffer.tryGetMutableBuffer(rt); if (!mutableBuf) { throw jsi::JSError( @@ -101,7 +115,7 @@ class AsyncArrayBuffer { // Always copies. static AsyncArrayBuffer copy(jsi::Runtime &rt, const jsi::ArrayBuffer &buffer) { - throwIfDetached(rt, buffer, "AsyncArrayBuffer::copy"); + detail::throwIfDetached(rt, buffer, "AsyncArrayBuffer::copy"); return copyBytes(rt, buffer); } @@ -140,30 +154,12 @@ class AsyncArrayBuffer { return buffer_; } - // Best-effort detached check. jsi::ArrayBuffer::detached relies on the JS-level - // `detached` property, which some runtimes (e.g. Hermes) don't implement and - // signal by throwing. In that case we silently skip the check rather than - // surfacing a confusing error from an unrelated method. - static void throwIfDetached(jsi::Runtime &rt, const jsi::ArrayBuffer &buffer, const char *callerName) - { - bool detached = false; - try { - detached = buffer.detached(rt); - } catch (const jsi::JSINativeException &) { - return; - } - if (detached) { - throw jsi::JSError(rt, std::string(callerName) + ": ArrayBuffer is detached"); - } - } - private: explicit AsyncArrayBuffer(std::shared_ptr buffer) noexcept : buffer_{std::move(buffer)} {} static AsyncArrayBuffer copyBytes(jsi::Runtime &rt, const jsi::ArrayBuffer &buffer) { - auto bytes = std::span(buffer.data(rt), buffer.size(rt)); - return wrap(std::vector(bytes.begin(), bytes.end())); + return AsyncArrayBuffer{detail::copyToOwnedBuffer(buffer.data(rt), buffer.size(rt))}; } std::shared_ptr buffer_; 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 0000965280c..ce1e19463e6 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/iostests/RCTTurboModuleTests.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import #import @@ -32,23 +33,6 @@ @implementation RCTTestTurboModule @end -// Minimal concrete MutableBuffer that owns its bytes, used to observe lifetime. -class TestMutableBuffer : public facebook::jsi::MutableBuffer { - public: - explicit TestMutableBuffer(size_t size) : bytes_(size, 0) {} - size_t size() const override - { - return bytes_.size(); - } - uint8_t *data() override - { - return bytes_.data(); - } - - private: - std::vector bytes_; -}; - // `jsi::Runtime::tryGetMutableBuffer` is optional — the Hermes branch linked into apps returns // nullptr for every ArrayBuffer — so decorate the runtime to answer it for the buffers created // through it. That keeps the coverage of the native-backed path independent of the engine. @@ -168,7 +152,7 @@ - (void)testNativeBackedArrayBufferIsAliasedAndKeepsBackingStoreAlive auto hermesRuntime = facebook::hermes::makeHermesRuntime(); MutableBufferAwareRuntime runtime(*hermesRuntime); - auto buffer = std::make_shared(kBufferSize); + auto buffer = std::make_shared(std::vector(kBufferSize, 0)); *buffer->data() = 0xAB; const uint8_t *sourceBytes = buffer->data(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index 0db4927a633..12c4975c805 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -466,7 +468,7 @@ JNIArgs convertJSIArgsToJNIArgs( } auto arrayBuffer = arg->getObject(rt).getArrayBuffer(rt); - AsyncArrayBuffer::throwIfDetached( + detail::throwIfDetached( rt, arrayBuffer, "JavaTurboModule::convertJSIArgsToJNIArgs"); auto size = arrayBuffer.size(rt); @@ -476,16 +478,21 @@ JNIArgs convertJSIArgsToJNIArgs( "JavaTurboModule::convertJSIArgsToJNIArgs: ArrayBuffer exceeds maximum size."); } - // Runtimes without a native buffer for this ArrayBuffer return nullptr, - // but the Static Hermes tracing runtime throws instead, and argument - // conversion runs outside any std::exception handler. Treat a failed - // probe as "no native buffer" so a traced session copies the bytes rather - // than aborting the process. + // Runtimes without a native buffer return nullptr, but the Static Hermes + // tracing runtime signals that by throwing std::logic_error. Treat a + // failed probe as "no native buffer" so a traced session copies rather + // than aborting; anything else is a real failure and propagates. std::shared_ptr mutableBuffer; try { mutableBuffer = arrayBuffer.tryGetMutableBuffer(rt); - } catch (const std::exception&) { - mutableBuffer = nullptr; + } catch (const std::logic_error& e) { + static std::once_flag warnOnce; + std::call_once(warnOnce, [&] { + LOG(WARNING) + << "JavaTurboModule::convertJSIArgsToJNIArgs: tryGetMutableBuffer is unsupported by this runtime (" + << e.what() + << "); ArrayBuffer arguments will be copied instead of aliased"; + }); } bool borrowsJSBytes = false; @@ -493,18 +500,21 @@ JNIArgs convertJSIArgsToJNIArgs( // Backed by a native buffer: alias it and retain its owner, so the // bytes stay valid for as long as the module holds the ArrayBuffer. if (mutableBuffer) { - return JArrayBuffer::createOwning(std::move(mutableBuffer)); + return JArrayBuffer::createWithOwnedBytes(std::move(mutableBuffer)); } // JS heap bytes on a synchronous call: lend them for the duration of - // the call. + // the call. Unlike iOS, which copies whenever the method signature + // exposes a block, a module that retains the buffer here gets an + // IllegalStateException from the revocation below. if (isSyncInvocation) { borrowsJSBytes = true; - return JArrayBuffer::createUnowned(arrayBuffer.data(rt), size); + return JArrayBuffer::createWithUnownedBytes( + arrayBuffer.data(rt), size); } // JS heap bytes that outlive the call: copy. - return JArrayBuffer::createOwned(arrayBuffer.data(rt), size); + return JArrayBuffer::createWithCopiedBytes(arrayBuffer.data(rt), size); }(); if (borrowsJSBytes) { diff --git a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm index 951668d45d2..657f325ccf4 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm +++ b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm @@ -571,4 +571,23 @@ - (void)testPromiseResolvesArrayBuffer XCTAssertEqual(resolvedBytes[3], 3); } +// +arrayBufferWithOwnedBytes:length:cleanup: takes ownership of `bytes` via `cleanup`, so raising +// without running the block leaks the caller's allocation. +- (void)testOwnedBytesCleanupRunsBeforeRaisingOnNullBytes +{ + __block BOOL cleanupRan = NO; + + XCTAssertThrowsSpecificNamed( + [RCTArrayBuffer arrayBufferWithOwnedBytes:NULL + length:4 + cleanup:^{ + cleanupRan = YES; + }], + // NOLINTNEXTLINE(misc-throw-by-value-catch-by-reference) - XCTest catches this class as a pointer + NSException, + NSInvalidArgumentException, + @"NULL bytes with a non-zero length must raise"); + XCTAssertTrue(cleanupRan, @"The cleanup block must run before the initializer raises, or its bytes leak"); +} + @end diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 5eb6faa7d0f..7678150d953 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -1657,7 +1657,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -2804,9 +2803,9 @@ class facebook::react::JArrayBuffer : public jni::HybridClass& mutableBuffer() const; public static constexpr auto kJavaDescriptor; - public static jni::local_ref createOwned(const void* bytes, size_t size); - public static jni::local_ref createOwning(std::shared_ptr buffer); - public static jni::local_ref createUnowned(void* bytes, size_t size); + public static jni::local_ref createWithCopiedBytes(const void* bytes, size_t size); + public static jni::local_ref createWithOwnedBytes(std::shared_ptr buffer); + public static jni::local_ref createWithUnownedBytes(void* bytes, size_t size); public static std::shared_ptr toJSBuffer(facebook::jsi::Runtime& runtime, jni::alias_ref arrayBuffer); public static void registerNatives(); public void invalidate() noexcept; @@ -10097,6 +10096,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -10119,6 +10119,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 33d2a212b96..f41c5ee0f9e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -1651,7 +1651,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -2762,9 +2761,9 @@ class facebook::react::JArrayBuffer : public jni::HybridClass& mutableBuffer() const; public static constexpr auto kJavaDescriptor; - public static jni::local_ref createOwned(const void* bytes, size_t size); - public static jni::local_ref createOwning(std::shared_ptr buffer); - public static jni::local_ref createUnowned(void* bytes, size_t size); + public static jni::local_ref createWithCopiedBytes(const void* bytes, size_t size); + public static jni::local_ref createWithOwnedBytes(std::shared_ptr buffer); + public static jni::local_ref createWithUnownedBytes(void* bytes, size_t size); public static std::shared_ptr toJSBuffer(facebook::jsi::Runtime& runtime, jni::alias_ref arrayBuffer); public static void registerNatives(); public void invalidate() noexcept; @@ -9710,6 +9709,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -9732,6 +9732,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index f47b1abfdbb..ce226ef3688 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -1655,7 +1655,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -2801,9 +2800,9 @@ class facebook::react::JArrayBuffer : public jni::HybridClass& mutableBuffer() const; public static constexpr auto kJavaDescriptor; - public static jni::local_ref createOwned(const void* bytes, size_t size); - public static jni::local_ref createOwning(std::shared_ptr buffer); - public static jni::local_ref createUnowned(void* bytes, size_t size); + public static jni::local_ref createWithCopiedBytes(const void* bytes, size_t size); + public static jni::local_ref createWithOwnedBytes(std::shared_ptr buffer); + public static jni::local_ref createWithUnownedBytes(void* bytes, size_t size); public static std::shared_ptr toJSBuffer(facebook::jsi::Runtime& runtime, jni::alias_ref arrayBuffer); public static void registerNatives(); public void invalidate() noexcept; @@ -9941,6 +9940,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -9963,6 +9963,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index e1c2982613a..4f8b3d6c84a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -4259,7 +4259,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -11986,6 +11985,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -12008,6 +12008,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 95c65c5ed1b..77caf262fb2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -4246,7 +4246,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -11665,6 +11664,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -11687,6 +11687,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 0f9ef238b70..1cabf9b1f1a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -4257,7 +4257,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -11840,6 +11839,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -11862,6 +11862,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 411d5c80a1c..b2e02592a61 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -981,7 +981,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -7097,6 +7096,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -7119,6 +7119,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 1d027d31af2..674898438c0 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -976,7 +976,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -6921,6 +6920,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -6943,6 +6943,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 937203e41aa..463c6c13d92 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -979,7 +979,6 @@ class facebook::react::AsyncArrayBuffer { public static facebook::react::AsyncArrayBuffer copy(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer); public static facebook::react::AsyncArrayBuffer wrap(std::shared_ptr buffer) noexcept; public static facebook::react::AsyncArrayBuffer wrap(std::vector bytes); - public static void throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); public std::shared_ptr getMutableBuffer() const noexcept; public uint8_t* data() noexcept; public ~AsyncArrayBuffer() = default; @@ -7088,6 +7087,7 @@ constexpr uint8_t facebook::react::detail::clampAlpha(std::optional alpha constexpr uint8_t facebook::react::detail::hexToNumeric(std::string_view hex, facebook::react::detail::HexColorType hexType); float facebook::react::detail::normalizeHue(float hue); std::optional facebook::react::detail::normalizeHueComponent(const std::variant& component); +std::shared_ptr facebook::react::detail::copyToOwnedBuffer(const uint8_t* bytes, size_t size); std::tuple facebook::react::detail::hslToRgb(float h, float s, float l); std::tuple facebook::react::detail::hwbToRgb(float h, float w, float b); template @@ -7110,6 +7110,7 @@ template C facebook::react::detail::memberFunctionClass(T C::*); template constexpr std::optional facebook::react::detail::normalizeComponent(const std::variant& component, float baseValue); +void facebook::react::detail::throwIfDetached(facebook::jsi::Runtime& rt, const facebook::jsi::ArrayBuffer& buffer, const char* callerName); class facebook::react::detail::OwnedBytesBuffer : public facebook::jsi::MutableBuffer { public OwnedBytesBuffer(std::vector bytes) noexcept; From 2e8f9675686e9930f8b5cd4e60c1d83ac4f24ef6 Mon Sep 17 00:00:00 2001 From: Facebook Employee Date: Wed, 19 Aug 2026 09:03:06 -0700 Subject: [PATCH 3/4] Generated from a GitHub Pull Request. Run 'jf sync' on this diff to load the correct commit data. Differential Revision: D116345031 --- .../modules/GenerateModuleJavaSpec.js | 11 +- .../modules/GenerateModuleJniCpp.js | 36 ++- .../GenerateModuleObjCpp/serializeMethod.js | 6 - .../src/generators/modules/Utils.js | 42 ---- .../modules/__test_fixtures__/fixtures.js | 38 ++-- .../__tests__/GenerateModuleHObjCpp-test.js | 40 ---- .../__tests__/GenerateModuleJavaSpec-test.js | 35 --- .../__tests__/GenerateModuleJniCpp-test.js | 39 ---- .../GenerateModuleH-test.js.snap | 45 +--- .../GenerateModuleHObjCpp-test.js.snap | 46 +--- .../GenerateModuleJavaSpec-test.js.snap | 11 +- .../GenerateModuleJniCpp-test.js.snap | 40 +--- .../GenerateModuleJniH-test.js.snap | 59 ----- .../GenerateModuleMm-test.js.snap | 36 ++- .../bridge/CxxArrayBufferCallbackImpl.kt | 44 ++++ .../main/jni/react/jni/JArrayBufferCallback.h | 58 +++++ .../src/main/jni/react/jni/OnLoad-common.cpp | 2 + .../bridge/CxxArrayBufferCallbackImplTest.kt | 95 ++++++++ .../android/ReactCommon/JavaTurboModule.cpp | 205 ++++++++++++++---- .../android/ReactCommon/JavaTurboModule.h | 3 +- .../platform/android/SampleTurboModule.kt | 20 ++ .../ios/ReactCommon/RCTSampleTurboModule.mm | 22 ++ .../modules/NativeSampleTurboModule.js | 1 + .../TurboModule/SampleTurboModuleExample.js | 5 + .../api-snapshots/ReactAndroidDebugCxx.api | 7 +- .../api-snapshots/ReactAndroidNewarchCxx.api | 7 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 7 +- .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + 30 files changed, 528 insertions(+), 435 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/CxxArrayBufferCallbackImplTest.kt diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index 9dc0b42ce8f..c038f166e31 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -25,11 +25,7 @@ import type {AliasResolver} from './Utils'; const {unwrapNullable} = require('../../parsers/parsers-commons'); const {wrapOptional} = require('../TypeUtils/Java'); const {parseValidUnionType, toPascalCase} = require('../Utils'); -const { - createAliasResolver, - getModules, - throwIfUnsupportedPromiseArrayBuffer, -} = require('./Utils'); +const {createAliasResolver, getModules} = require('./Utils'); type FilesOutput = Map; @@ -599,11 +595,6 @@ module.exports = { method.typeAnnotation, ); - throwIfUnsupportedPromiseArrayBuffer( - method.name, - methodTypeAnnotation.returnTypeAnnotation, - ); - // Handle return type const translatedReturnType = translateFunctionReturnTypeToJavaType( methodTypeAnnotation.returnTypeAnnotation, diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index 7c088461107..7e4e53ebd00 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -24,11 +24,7 @@ import type {AliasResolver} from './Utils'; const {unwrapNullable} = require('../../parsers/parsers-commons'); const {parseValidUnionType} = require('../Utils'); -const { - createAliasResolver, - getModules, - throwIfUnsupportedPromiseArrayBuffer, -} = require('./Utils'); +const {createAliasResolver, getModules} = require('./Utils'); type FilesOutput = Map; @@ -47,15 +43,22 @@ const HostFunctionTemplate = ({ propertyName, jniSignature, jsReturnType, + promiseResolveSupportsArrayBuffer, }: Readonly<{ hasteModuleName: string, propertyName: string, jniSignature: string, jsReturnType: JSReturnType, + promiseResolveSupportsArrayBuffer: boolean, }>) => { + // invokeJavaMethod defaults this to false; only pass it where it matters, so + // adding the parameter does not rewrite every generated JNI file. + const promiseResolveSupportsArrayBufferArg = promiseResolveSupportsArrayBuffer + ? ', true' + : ''; return `static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${propertyName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { static jmethodID cachedMethodId = nullptr; - return static_cast(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId); + return static_cast(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId${promiseResolveSupportsArrayBufferArg}); }`; }; @@ -406,6 +409,23 @@ function translateReturnTypeToJniType( } } +function doesPromiseResolveSupportArrayBuffer( + nullableReturnTypeAnnotation: Nullable, +): boolean { + const [returnTypeAnnotation] = + unwrapNullable( + nullableReturnTypeAnnotation, + ); + if (returnTypeAnnotation.type !== 'PromiseTypeAnnotation') { + return false; + } + + let elementType = returnTypeAnnotation.elementType; + [elementType] = unwrapNullable(elementType); + + return elementType.type === 'ArrayBufferTypeAnnotation'; +} + function translateMethodTypeToJniSignature( property: NativeModulePropertyShape, resolveAlias: AliasResolver, @@ -453,8 +473,6 @@ function translateMethodForImplementation( unwrapNullable(property.typeAnnotation); const {returnTypeAnnotation} = propertyTypeAnnotation; - throwIfUnsupportedPromiseArrayBuffer(property.name, returnTypeAnnotation); - if ( property.name === 'getConstants' && returnTypeAnnotation.type === 'ObjectTypeAnnotation' && @@ -468,6 +486,8 @@ function translateMethodForImplementation( propertyName: property.name, jniSignature: translateMethodTypeToJniSignature(property, resolveAlias), jsReturnType: translateReturnTypeToKind(returnTypeAnnotation, resolveAlias), + promiseResolveSupportsArrayBuffer: + doesPromiseResolveSupportArrayBuffer(returnTypeAnnotation), }); } 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 397516dc5c2..c22c45b9f4c 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -26,7 +26,6 @@ const { } = require('../../../parsers/parsers-commons'); const {wrapOptional} = require('../../TypeUtils/Objective-C'); const {capitalize, parseValidUnionType} = require('../../Utils'); -const {throwIfUnsupportedPromiseArrayBuffer} = require('../Utils'); const {getNamespacedStructName} = require('./Utils'); const invariant = require('invariant'); @@ -104,11 +103,6 @@ function serializeMethod( } }); - throwIfUnsupportedPromiseArrayBuffer( - methodName, - propertyTypeAnnotation.returnTypeAnnotation, - ); - // Unwrap returnTypeAnnotation, so we check if the return type is Promise // TODO(T76719514): Disallow nullable PromiseTypeAnnotations const [returnTypeAnnotation] = unwrapNullable( diff --git a/packages/react-native-codegen/src/generators/modules/Utils.js b/packages/react-native-codegen/src/generators/modules/Utils.js index 8cd8d37ff09..ce6b6339841 100644 --- a/packages/react-native-codegen/src/generators/modules/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/Utils.js @@ -13,7 +13,6 @@ import type { NativeModuleAliasMap, NativeModuleObjectTypeAnnotation, - NativeModuleReturnTypeAnnotation, NativeModuleSchema, NativeModuleTypeAnnotation, Nullable, @@ -78,50 +77,9 @@ function isArrayRecursiveMember( ); } -// Platform-native (Java/Kotlin and ObjC) TurboModules copy ArrayBuffer -// arguments and return ArrayBuffers zero-copy from synchronous methods, but -// `Promise` is not part of their contract. -// -// On Android it cannot work: the resolve path serializes through -// folly::dynamic, which cannot carry raw bytes. On iOS the resolve path is a -// direct ObjC->jsi conversion that would in fact produce an ArrayBuffer for an -// NSMutableData, so the limitation there is not technical — the guard is -// applied to ObjC as well to keep one cross-platform contract, so a spec that -// compiles for iOS cannot fail to build for Android. -// -// Reject `Promise` at codegen time for both native platforms so -// the unsupported case surfaces as a build error rather than a runtime failure -// or a silent iOS/Android divergence. -function throwIfUnsupportedPromiseArrayBuffer( - methodName: string, - nullableReturnTypeAnnotation: Nullable, -): void { - const [returnTypeAnnotation] = - unwrapNullable( - nullableReturnTypeAnnotation, - ); - if (returnTypeAnnotation.type !== 'PromiseTypeAnnotation') { - return; - } - let elementType = returnTypeAnnotation.elementType; - if (elementType.type === 'NullableTypeAnnotation') { - elementType = elementType.typeAnnotation; - } - if (elementType.type === 'ArrayBufferTypeAnnotation') { - throw new Error( - `Unsupported return type for method "${methodName}": Promise is not ` + - 'supported for Android (Java/Kotlin) or iOS (ObjC) TurboModules. Use a C++ ' + - '(Cxx) TurboModule, return the ArrayBuffer from a synchronous method, or resolve ' + - 'the Promise with a different type. ArrayBuffer is still supported as a method ' + - 'argument and as a synchronous return value on all platforms.', - ); - } -} - module.exports = { createAliasResolver, getModules, isDirectRecursiveMember, isArrayRecursiveMember, - throwIfUnsupportedPromiseArrayBuffer, }; diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 83cc98bef05..9ad6453a0d6 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -2661,25 +2661,6 @@ const ARRAY_BUFFER_NATIVE_MODULE: SchemaType = { ], }, }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, -}; - -// Promise is only supported by C++ (Cxx) TurboModules (see -// throwIfUnsupportedPromiseArrayBuffer), so this fixture is excluded on both -// Android and iOS. It keeps C++ codegen coverage for the async-return case. -const ARRAY_BUFFER_PROMISE_NATIVE_MODULE: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ { name: 'promiseArrayBuffer', optional: false, @@ -2694,10 +2675,26 @@ const ARRAY_BUFFER_PROMISE_NATIVE_MODULE: SchemaType = { params: [], }, }, + { + name: 'promiseNullableArrayBuffer', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + elementType: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + params: [], + }, + }, ], }, moduleName: 'SampleTurboModule', - excludedPlatforms: ['android', 'iOS'], }, }, }; @@ -2896,7 +2893,6 @@ const STRING_LITERALS: SchemaType = { module.exports = { array_buffer_native_module: ARRAY_BUFFER_NATIVE_MODULE, - array_buffer_promise_native_module: ARRAY_BUFFER_PROMISE_NATIVE_MODULE, complex_objects: COMPLEX_OBJECTS, two_modules_different_files: TWO_MODULES_DIFFERENT_FILES, empty_native_modules: EMPTY_NATIVE_MODULES, diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js index c151a4aa1cc..bb50ae55a1e 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleObjCpp'); @@ -33,42 +31,4 @@ describe('GenerateModuleHObjCpp', () => { ).toMatchSnapshot(); }); }); - - it('throws for a method returning Promise (unsupported on iOS)', () => { - const schema: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ - { - name: 'getAsyncBuffer', - optional: false, - typeAnnotation: { - type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], - }, - }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, - }; - expect(() => - generator.generate( - 'array_buffer_promise_throws', - schema, - 'com.facebook.fbreact.specs', - false, - ), - ).toThrow(/Promise is not supported/); - }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js index 3cbcf974717..45d7b0e5879 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleJavaSpec.js'); @@ -31,37 +29,4 @@ describe('GenerateModuleJavaSpec', () => { ).toMatchSnapshot(); }); }); - - it('throws for a method returning Promise (unsupported on Android)', () => { - const schema: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ - { - name: 'getAsyncBuffer', - optional: false, - typeAnnotation: { - type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], - }, - }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, - }; - expect(() => - generator.generate('array_buffer_promise_throws', schema), - ).toThrow(/Promise is not supported/); - }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js index 72e173904c6..0e1fae7402e 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleJniCpp.js'); @@ -31,41 +29,4 @@ describe('GenerateModuleJniCpp', () => { ).toMatchSnapshot(); }); }); - - it('throws for a method returning Promise (unsupported on Android)', () => { - const schema: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ - { - name: 'getAsyncBuffer', - optional: false, - typeAnnotation: { - type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], - }, - }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, - }; - expect(() => - generator.generate( - 'array_buffer_promise_throws', - schema, - 'com.facebook.fbreact.specs', - ), - ).toThrow(/Promise is not supported/); - }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap index f2b1d6ad49c..2e35e312030 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap @@ -67,6 +67,8 @@ protected: methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __getArrayBuffer}; methodMap_[\\"voidArrayBuffer\\"] = MethodMetadata {.argCount = 1, .invoker = __voidArrayBuffer}; methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {.argCount = 1, .invoker = __voidNullableArrayBuffer}; + methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseArrayBuffer}; + methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseNullableArrayBuffer}; } private: @@ -92,49 +94,20 @@ private: bridging::callFromJs(rt, &T::voidNullableArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), count <= 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).getArrayBuffer(rt)));return jsi::Value::undefined(); } -}; - -} // namespace facebook::react -", -} -`; - -exports[`GenerateModuleH can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "array_buffer_promise_native_moduleJSI.h" => "/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleH.js - */ - -#pragma once - -#include -#include - -namespace facebook::react { - - -template -class JSI_EXPORT NativeSampleTurboModuleCxxSpec : public TurboModule { -public: - static constexpr std::string_view kModuleName = \\"SampleTurboModule\\"; -protected: - NativeSampleTurboModuleCxxSpec(std::shared_ptr jsInvoker) : TurboModule(std::string{NativeSampleTurboModuleCxxSpec::kModuleName}, jsInvoker) { - methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseArrayBuffer}; - } - -private: static jsi::Value __promiseArrayBuffer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { static_assert( bridging::getParameterCount(&T::promiseArrayBuffer) == 1, \\"Expected promiseArrayBuffer(...) to have 1 parameters\\"); return bridging::callFromJs(rt, &T::promiseArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); } + + static jsi::Value __promiseNullableArrayBuffer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { + static_assert( + bridging::getParameterCount(&T::promiseNullableArrayBuffer) == 1, + \\"Expected promiseNullableArrayBuffer(...) to have 1 parameters\\"); + return bridging::callFromJs(rt, &T::promiseNullableArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); + } }; } // namespace facebook::react 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 4726cdcd0d9..327bf53723f 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 @@ -107,6 +107,10 @@ Map { - (RCTArrayBuffer *)getArrayBuffer; - (void)voidArrayBuffer:(RCTArrayBuffer *)arg; - (void)voidNullableArrayBuffer:(RCTArrayBuffer * _Nullable)arg; +- (void)promiseArrayBuffer:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; +- (void)promiseNullableArrayBuffer:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; @end @@ -134,48 +138,6 @@ namespace facebook::react { } `; -exports[`GenerateModuleHObjCpp can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "array_buffer_promise_native_module.h" => "/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleObjCpp - * - * We create an umbrella header (and corresponding implementation) here since - * Cxx compilation in BUCK has a limitation: source-code producing genrule()s - * must have a single output. More files => more genrule()s => slower builds. - */ - -#ifndef __cplusplus -#error This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm. -#endif - -// Avoid multiple includes of array_buffer_promise_native_module symbols -#ifndef array_buffer_promise_native_module_H -#define array_buffer_promise_native_module_H - -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import - - - -#endif // array_buffer_promise_native_module_H -", -} -`; - exports[`GenerateModuleHObjCpp can generate fixture complex_objects 1`] = ` Map { "complex_objects.h" => "/** diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap index 14722e2179f..0c74d565771 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap @@ -59,6 +59,7 @@ package com.facebook.fbreact.specs; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.ArrayBuffer; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -89,13 +90,19 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo @ReactMethod @DoNotStrip public abstract void voidNullableArrayBuffer(@Nullable ArrayBuffer arg); + + @ReactMethod + @DoNotStrip + public abstract void promiseArrayBuffer(Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void promiseNullableArrayBuffer(Promise promise); } ", } `; -exports[`GenerateModuleJavaSpec can generate fixture array_buffer_promise_native_module 1`] = `Map {}`; - exports[`GenerateModuleJavaSpec can generate fixture complex_objects 1`] = ` Map { "java/com/facebook/fbreact/specs/NativeSampleTurboModuleSpec.java" => " diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap index 547344fda40..ca8e30f8fbf 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap @@ -66,11 +66,23 @@ static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidNu return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId); } +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"promiseArrayBuffer\\", \\"(Lcom/facebook/react/bridge/Promise;)V\\", args, count, cachedMethodId, true); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"promiseNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/Promise;)V\\", args, count, cachedMethodId, true); +} + NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) : JavaTurboModule(params) { methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer}; methodMap_[\\"voidArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer}; methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer}; + methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer}; + methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer}; } std::shared_ptr array_buffer_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { @@ -85,34 +97,6 @@ std::shared_ptr array_buffer_native_module_ModuleProvider(const std } `; -exports[`GenerateModuleJniCpp can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "jni/array_buffer_promise_native_module-generated.cpp" => " -/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleJniCpp.js - */ - -#include \\"array_buffer_promise_native_module.h\\" - -namespace facebook::react { - - - -std::shared_ptr array_buffer_promise_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { - - return nullptr; -} - -} // namespace facebook::react -", -} -`; - exports[`GenerateModuleJniCpp can generate fixture complex_objects 1`] = ` Map { "jni/complex_objects-generated.cpp" => " diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap index fed0ac2033f..daa41686b1c 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap @@ -132,65 +132,6 @@ target_compile_reactnative_options(react_codegen_array_buffer_native_module PRIV } `; -exports[`GenerateModuleJniH can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "jni/array_buffer_promise_native_module.h" => " -/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleJniH.js - */ - -#pragma once - -#include -#include -#include - -namespace facebook::react { - - - -JSI_EXPORT -std::shared_ptr array_buffer_promise_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms); - -} // namespace facebook::react -", - "jni/CMakeLists.txt" => "# 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. - -cmake_minimum_required(VERSION 3.13) -set(CMAKE_VERBOSE_MAKEFILE on) - -file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/array_buffer_promise_native_module/*.cpp) - -add_library( - react_codegen_array_buffer_promise_native_module - OBJECT - \${react_codegen_SRCS} -) - -target_include_directories(react_codegen_array_buffer_promise_native_module PUBLIC . react/renderer/components/array_buffer_promise_native_module) - -target_link_libraries( - react_codegen_array_buffer_promise_native_module - fbjni - jsi - # We need to link different libraries based on whether we are building rncore or not, that's necessary - # because we want to break a circular dependency between react_codegen_rncore and reactnative - reactnative -) - -target_compile_reactnative_options(react_codegen_array_buffer_promise_native_module PRIVATE) -", -} -`; - exports[`GenerateModuleJniH can generate fixture complex_objects 1`] = ` Map { "jni/complex_objects.h" => " diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap index f70403ac89b..096243c34bc 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap @@ -82,6 +82,14 @@ namespace facebook::react { return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", @selector(voidNullableArrayBuffer:), args, count); } + static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, \\"promiseArrayBuffer\\", @selector(promiseArrayBuffer:reject:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, \\"promiseNullableArrayBuffer\\", @selector(promiseNullableArrayBuffer:reject:), args, count); + } + NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms) : ObjCTurboModule(params) { @@ -93,34 +101,18 @@ namespace facebook::react { methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer}; + + methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer}; + + + methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer}; + } } // namespace facebook::react ", } `; -exports[`GenerateModuleMm can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "array_buffer_promise_native_module-generated.mm" => "/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleObjCpp - * - * We create an umbrella header (and corresponding implementation) here since - * Cxx compilation in BUCK has a limitation: source-code producing genrule()s - * must have a single output. More files => more genrule()s => slower builds. - */ - -#import \\"array_buffer_promise_native_module.h\\" - - -", -} -`; - exports[`GenerateModuleMm can generate fixture complex_objects 1`] = ` Map { "complex_objects-generated.mm" => "/** diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt new file mode 100644 index 00000000000..b2060c08277 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package com.facebook.react.bridge + +import com.facebook.jni.HybridClassBase +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Resolve callback for a Promise that may be fulfilled with an [ArrayBuffer] or null. Created from + * C++ only where the JavaScript spec permits `Promise` or `Promise`. + * + * Unlike [CxxCallbackImpl], this does not serialize through folly::dynamic. An owning [ArrayBuffer] + * reaches JavaScript aliasing the same memory; null is forwarded explicitly. + * + * A module that resolves with anything else is misusing its spec. Rather than throwing on whichever + * thread called `Promise.resolve`, the problem is described to C++, which rejects the Promise with + * it. + */ +@DoNotStrip +internal class CxxArrayBufferCallbackImpl @DoNotStrip private constructor() : + HybridClassBase(), Callback { + + override fun invoke(vararg args: Any?) { + if (args.size > 1) { + nativeInvoke(null, "expected at most one argument, got ${args.size}") + return + } + when (val arg = args.firstOrNull()) { + null -> nativeInvoke(null, null) + is ArrayBuffer -> nativeInvoke(arg, null) + else -> nativeInvoke(null, "expected an ArrayBuffer or null, got ${arg.javaClass.name}") + } + } + + /** + * At most one of [arrayBuffer] and [error] is non-null. Both null resolves with JavaScript null. + */ + private external fun nativeInvoke(arrayBuffer: ArrayBuffer?, error: String?) +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h new file mode 100644 index 00000000000..b8618840e15 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h @@ -0,0 +1,58 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include + +#include "JArrayBuffer.h" +#include "JCallback.h" + +namespace facebook::react { + +// Resolve callback for a Promise that may be fulfilled with an ArrayBuffer or +// null. +// +// Created only where the JavaScript spec permits Promise or +// Promise. Does not use folly::dynamic; a non-null ArrayBuffer is +// handed to JavaScript without copying, and null is forwarded explicitly. +// +// The Java side validates what the module resolved with and reports a +// description of the problem through `error` instead of throwing, so that +// misuse rejects the Promise rather than escaping on the resolving thread. +class JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public: + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/bridge/CxxArrayBufferCallbackImpl;"; + + static void registerNatives() + { + registerHybrid({ + makeNativeMethod("nativeInvoke", JCxxArrayBufferCallbackImpl::invoke), + }); + } + + private: + friend HybridBase; + + // At most one of `arrayBuffer` and `error` is non-null. Both null resolves + // the Promise with JavaScript null. + using Callback = + std::function arrayBuffer, jni::alias_ref error)>; + + explicit JCxxArrayBufferCallbackImpl(Callback callback) : callback_(std::move(callback)) {} + + void invoke(jni::alias_ref arrayBuffer, jni::alias_ref error) + { + callback_(arrayBuffer, error); + } + + Callback callback_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp index 8c6787bdde6..cb8729cd9d1 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp @@ -7,6 +7,7 @@ #include #include "JArrayBuffer.h" +#include "JArrayBufferCallback.h" #include "JCallback.h" #include "JDynamicNative.h" #include "JReactMarker.h" @@ -20,6 +21,7 @@ namespace facebook::react { extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return facebook::jni::initialize(vm, [] { JArrayBuffer::registerNatives(); + JCxxArrayBufferCallbackImpl::registerNatives(); JCxxCallbackImpl::registerNatives(); JDynamicNative::registerNatives(); JReactMarker::registerNatives(); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/CxxArrayBufferCallbackImplTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/CxxArrayBufferCallbackImplTest.kt new file mode 100644 index 00000000000..87fd1f4c3c7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/CxxArrayBufferCallbackImplTest.kt @@ -0,0 +1,95 @@ +/* + * 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. + */ + +package com.facebook.react.bridge + +import com.facebook.testutils.shadows.ShadowArrayBuffer +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowSoLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + +/** + * Records what [CxxArrayBufferCallbackImpl] hands to its C++ peer, which Robolectric cannot load. + */ +@Implements(CxxArrayBufferCallbackImpl::class) +open class ShadowCxxArrayBufferCallbackImpl { + + @Implementation + fun nativeInvoke(arrayBuffer: ArrayBuffer?, error: String?) { + invocations.add(arrayBuffer to error) + } + + companion object { + val invocations: MutableList> = mutableListOf() + } +} + +@RunWith(RobolectricTestRunner::class) +@Config( + shadows = + [ + ShadowSoLoader::class, + ShadowNativeLoader::class, + ShadowArrayBuffer::class, + ShadowCxxArrayBufferCallbackImpl::class, + ], +) +class CxxArrayBufferCallbackImplTest { + + @Before + fun setUp() { + ShadowCxxArrayBufferCallbackImpl.invocations.clear() + } + + @Test + fun forwardsNullAsAResolutionWithNoError() { + newCallback().invoke(null) + + assertThat(ShadowCxxArrayBufferCallbackImpl.invocations).containsExactly(null to null) + } + + @Test + fun forwardsAnArrayBufferWithoutCopyingIt() { + val buffer = ArrayBuffer(4) + + newCallback().invoke(buffer) + + assertThat(ShadowCxxArrayBufferCallbackImpl.invocations).containsExactly(buffer to null) + } + + @Test + fun reportsAnErrorForAnUnsupportedResolutionType() { + newCallback().invoke("not a buffer") + + val (arrayBuffer, error) = ShadowCxxArrayBufferCallbackImpl.invocations.single() + assertThat(arrayBuffer).isNull() + assertThat(error).contains("expected an ArrayBuffer or null", "java.lang.String") + } + + @Test + fun reportsAnErrorForMoreThanOneArgument() { + newCallback().invoke(ArrayBuffer(1), ArrayBuffer(1)) + + val (arrayBuffer, error) = ShadowCxxArrayBufferCallbackImpl.invocations.single() + assertThat(arrayBuffer).isNull() + assertThat(error).contains("at most one argument") + } + + private fun newCallback(): CxxArrayBufferCallbackImpl = + CxxArrayBufferCallbackImpl::class + .java + .getDeclaredConstructor() + .apply { isAccessible = true } + .newInstance() +} diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index 12c4975c805..d1d0e97a031 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -5,10 +5,12 @@ * LICENSE file in the root directory of this source tree. */ +#include #include #include #include #include +#include #include #include @@ -26,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -136,20 +139,38 @@ jsi::Value createRejectionError(jsi::Runtime& rt, const folly::dynamic& args) { return jsError; } -auto createJavaCallback( - jsi::Runtime& rt, - jsi::Function&& function, - std::shared_ptr jsInvoker) { - std::optional> callback( - {rt, std::move(function), std::move(jsInvoker)}); - return JCxxCallbackImpl::newObjectCxxArgs( - [callback = std::move(callback)](folly::dynamic args) mutable { - if (!callback) { - LOG(FATAL) << "Callback arg cannot be called more than once"; - return; - } - callback->call([args = std::move(args)]( - jsi::Runtime& rt, jsi::Function& jsFunction) { +class OnceCallback { + std::optional> callback_; + + public: + OnceCallback( + jsi::Runtime& rt, + jsi::Function function, + std::shared_ptr jsInvoker) + : callback_( + AsyncCallback<>(rt, std::move(function), std::move(jsInvoker))) {} + + OnceCallback(const OnceCallback&) = delete; + OnceCallback& operator=(const OnceCallback&) = delete; + OnceCallback(OnceCallback&&) = default; + OnceCallback& operator=(OnceCallback&&) = default; + ~OnceCallback() = default; + + template + void call(const char* what, F&& invoke) { + if (!callback_) { + LOG(FATAL) << what << " cannot be called more than once"; + return; + } + callback_->call(std::forward(invoke)); + callback_ = std::nullopt; + } + + void callWithArgs(const char* what, folly::dynamic&& args) { + call( + what, + [args = std::move(args)]( + jsi::Runtime& rt, jsi::Function& jsFunction) mutable { std::vector jsArgs; jsArgs.reserve(args.size()); for (const auto& val : args) { @@ -157,27 +178,118 @@ auto createJavaCallback( } jsFunction.call(rt, (const jsi::Value*)jsArgs.data(), jsArgs.size()); }); - callback = std::nullopt; - }); + } +}; + +template +jni::local_ref makeJavaOnceCallback( + jsi::Runtime& rt, + jsi::Function function, + std::shared_ptr jsInvoker, + Handler handler) { + auto once = std::make_shared( + rt, std::move(function), std::move(jsInvoker)); + return jni::static_ref_cast( + JavaCallbackImpl::newObjectCxxArgs( + [once = std::move(once), + handler = std::move(handler)](auto&&... args) mutable { + handler(*once, std::forward(args)...); + })); } -auto createJavaRejectCallback( +jni::local_ref createJavaCallback( jsi::Runtime& rt, jsi::Function&& function, std::shared_ptr jsInvoker) { - std::optional> callback( - {rt, std::move(function), std::move(jsInvoker)}); - return JCxxCallbackImpl::newObjectCxxArgs( - [callback = std::move(callback)](folly::dynamic args) mutable { - if (!callback) { - LOG(FATAL) << "Callback arg cannot be called more than once"; + return makeJavaOnceCallback( + rt, + std::move(function), + std::move(jsInvoker), + [](OnceCallback& once, folly::dynamic args) { + once.callWithArgs("Callback arg", std::move(args)); + }); +} + +jni::local_ref createJavaArrayBufferCallback( + jsi::Runtime& rt, + jsi::Function&& resolveFunction, + jsi::Function&& rejectFunction, + std::shared_ptr jsInvoker) { + auto rejectMisuse = + std::make_shared(rt, std::move(rejectFunction), jsInvoker); + return makeJavaOnceCallback( + rt, + std::move(resolveFunction), + std::move(jsInvoker), + [rejectMisuse = std::move(rejectMisuse)]( + OnceCallback& once, + jni::alias_ref arrayBuffer, + jni::alias_ref error) { + auto reject = [&](const std::string& reason) { + rejectMisuse->call( + "Promise reject", + [message = "Invalid Promise resolution: " + reason]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call(rt, createJSRuntimeError(rt, message)); + }); + }; + + if (error) { + reject(error->toStdString()); return; } - callback->call([args = std::move(args)]( - jsi::Runtime& rt, jsi::Function& jsFunction) { - jsFunction.call(rt, createRejectionError(rt, args)); - }); - callback = std::nullopt; + + if (!arrayBuffer) { + once.call( + "Promise resolve", + [](jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call(rt, jsi::Value::null()); + }); + return; + } + + // toJSBuffer runs on the JS thread, where throwing would escape into + // the JS queue instead of rejecting. The states it rejects for are + // observable here, so check them while a rejection is still possible. + auto* peer = arrayBuffer->cthis(); + if (peer == nullptr || !peer->hasBytes()) { + reject( + "the ArrayBuffer has no bytes to hand over. A non-owning " + "ArrayBuffer borrows the bytes of a JS ArrayBuffer only for the " + "synchronous call it was passed to; copy them with " + "ArrayBuffer.arrayBufferWithCopiedBytes() to resolve with them " + "later."); + return; + } + + once.call( + "Promise resolve", + [globalBuffer = jni::make_global(arrayBuffer)]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call( + rt, + jsi::Value( + jsi::ArrayBuffer( + rt, JArrayBuffer::toJSBuffer(rt, globalBuffer)))); + }); + }); +} + +jni::local_ref createJavaRejectCallback( + jsi::Runtime& rt, + jsi::Function&& function, + std::shared_ptr jsInvoker) { + return makeJavaOnceCallback( + rt, + std::move(function), + std::move(jsInvoker), + [](OnceCallback& once, folly::dynamic args) { + once.call( + "Promise reject", + [args = std::move(args)]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call(rt, createRejectionError(rt, args)); + }); }); } @@ -406,14 +518,14 @@ JNIArgs convertJSIArgsToJNIArgs( "number", argIndex, methodName, arg, &rt); } jarg->l = makeGlobalIfNecessary( - jni::JFloat::valueOf(arg->getNumber()).release()); + jni::JFloat::valueOf(static_cast(arg->getNumber())).release()); } else if (type == "Ljava/lang/Integer;") { if (!arg->isNumber()) { throw JavaTurboModuleArgumentConversionException( "number", argIndex, methodName, arg, &rt); } jarg->l = makeGlobalIfNecessary( - jni::JInteger::valueOf(arg->getNumber()).release()); + jni::JInteger::valueOf(static_cast(arg->getNumber())).release()); } else if (type == "Ljava/lang/Boolean;") { if (!arg->isBool()) { throw JavaTurboModuleArgumentConversionException( @@ -605,7 +717,11 @@ jsi::Value JavaTurboModule::invokeJavaMethod( const std::string& methodSignature, const jsi::Value* args, size_t argCount, - jmethodID& methodID) { + jmethodID& methodID, + bool promiseResolveSupportsArrayBuffer) { + react_native_assert( + !promiseResolveSupportsArrayBuffer || valueKind == PromiseKind); + const char* methodName = methodNameStr.c_str(); const char* moduleName = name_.c_str(); @@ -653,7 +769,7 @@ jsi::Value JavaTurboModule::invokeJavaMethod( * number of alive LocalReferences is estimatedLocalRefCount smaller than * kJniLocalRefMax. */ - jni::JniLocalScope scope(env, estimatedLocalRefCount); + jni::JniLocalScope scope(env, static_cast(estimatedLocalRefCount)); auto checkJNIErrorForMethodCall = [&]() -> void { try { @@ -684,7 +800,8 @@ jsi::Value JavaTurboModule::invokeJavaMethod( TMPL::syncMethodCallArgConversionEnd(moduleName, methodName); TMPL::syncMethodCallExecutionStart(moduleName, methodName); - auto constantsMap = (jobject)env->CallObjectMethod(instance, methodID); + // getConstants takes no arguments, so the jvalue array is never read. + auto constantsMap = env->CallObjectMethodA(instance, methodID, nullptr); checkJNIErrorForMethodCall(); TMPL::syncMethodCallExecutionEnd(moduleName, methodName); @@ -828,8 +945,8 @@ jsi::Value JavaTurboModule::invokeJavaMethod( } } case StringKind: { - auto returnString = - (jstring)env->CallObjectMethodA(instance, methodID, jargs.data()); + auto returnString = static_cast( + env->CallObjectMethodA(instance, methodID, jargs.data())); checkJNIErrorForMethodCall(); TMPL::syncMethodCallExecutionEnd(moduleName, methodName); @@ -969,10 +1086,16 @@ jsi::Value JavaTurboModule::invokeJavaMethod( args[1].getObject(runtime).getFunction(runtime), jsInvoker_); - auto resolve = createJavaCallback( - runtime, - args[0].getObject(runtime).getFunction(runtime), - jsInvoker_); + auto resolve = promiseResolveSupportsArrayBuffer + ? createJavaArrayBufferCallback( + runtime, + args[0].getObject(runtime).getFunction(runtime), + args[1].getObject(runtime).getFunction(runtime), + jsInvoker_) + : createJavaCallback( + runtime, + args[0].getObject(runtime).getFunction(runtime), + jsInvoker_); auto reject = createJavaRejectCallback( runtime, args[1].getObject(runtime).getFunction(runtime), @@ -1117,12 +1240,12 @@ void JavaTurboModule::configureEventEmitterCallback() { .emit(args.size() > 1 ? std::move(args).at(1) : nullptr); }); - jvalue args[1]; + std::array args{}; args[0].l = callback.release(); // CallVoidMethod is replaced with CallVoidMethodA as it's unsafe on 32bit and // causes crashes https://github.com/facebook/react-native/issues/51628 - env->CallVoidMethodA(instance_.get(), cachedMethodId, args); + env->CallVoidMethodA(instance_.get(), cachedMethodId, args.data()); FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h index c42f1c7dbac..e2c8dd77791 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h @@ -45,7 +45,8 @@ class JSI_EXPORT JavaTurboModule : public TurboModule { const std::string &methodSignature, const jsi::Value *args, size_t argCount, - jmethodID &cachedMethodID); + jmethodID &cachedMethodID, + bool promiseResolveSupportsArrayBuffer = false); protected: void configureEventEmitterCallback(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt index 4c97e114015..0e4aa35589b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt @@ -186,6 +186,26 @@ public class SampleTurboModule(private val context: ReactApplicationContext) : promise.resolve((payload?.size ?: 0).toDouble()) } + @DoNotStrip + @Suppress("unused") + override fun getAsyncBuffer(size: Double, promise: Promise) { + if (!size.isFinite() || size < 0.0 || size > Int.MAX_VALUE.toDouble()) { + promise.reject( + "invalid_size", + "getAsyncBuffer: size must be a finite value in [0, ${Int.MAX_VALUE}], got $size", + ) + return + } + val capacity = size.toInt() + val buffer = ArrayBuffer(capacity) + val bytes = buffer.bytes + for (i in 0 until capacity) { + bytes.put(i, (i + 1).toByte()) + } + log("getAsyncBuffer", size, buffer) + promise.resolve(buffer) + } + @DoNotStrip @Suppress("unused") override fun getValueWithCallback(callback: Callback?) { 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 6ec73228f76..38c740cafcc 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 @@ -16,6 +16,7 @@ #import #import +#include #include using namespace facebook::react; @@ -176,6 +177,27 @@ - (void)processAsyncBuffer:(RCTArrayBuffer *)payload resolve(@(payload.length)); } +// Resolving a Promise with an RCTArrayBuffer hands JS the bytes without copying +// them; the object keeps them alive for as long as JS holds the ArrayBuffer. +- (void)getAsyncBuffer:(double)size resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + if (!std::isfinite(size) || size < 0 || size > (double)INT32_MAX) { + reject( + @"invalid_size", + [NSString stringWithFormat:@"getAsyncBuffer: size must be a finite value in [0, %d], got %g", INT32_MAX, size], + nil); + return; + } + + RCTArrayBuffer *buffer = [RCTArrayBuffer arrayBufferWithLength:(NSUInteger)size]; + std::span byteSpan(static_cast(buffer.mutableBytes), static_cast(buffer.length)); + uint8_t value = 1; + for (auto &byte : byteSpan) { + byte = value++; + } + resolve(buffer); +} + - (void)getValueWithCallback:(RCTResponseSenderBlock)callback { if (callback == nullptr) { diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js index c458c91a220..3f4b77c4801 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js @@ -54,6 +54,7 @@ export interface Spec extends TurboModule { readonly getArrayBuffer: (buffer: ArrayBuffer) => ArrayBuffer; readonly createNativeBuffer: (size: number) => ArrayBuffer; readonly processAsyncBuffer: (payload: ArrayBuffer) => Promise; + readonly getAsyncBuffer: (size: number) => Promise; readonly getValueWithCallback: (callback: (value: string) => void) => void; readonly getValueWithPromise: (error: boolean) => Promise; readonly voidFuncThrows?: () => void; diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index aab25aacd46..4b63a4cb073 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -49,6 +49,7 @@ type Examples = | 'getArrayBuffer' | 'createNativeBuffer' | 'processAsyncBuffer' + | 'getAsyncBuffer' | 'promise' | 'rejectPromise' | 'voidFunc' @@ -130,6 +131,10 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.processAsyncBuffer( new Uint8Array([1, 2, 3]).buffer, ).then(length => this._setResult('processAsyncBuffer', length)), + getAsyncBuffer: () => + NativeSampleTurboModule.getAsyncBuffer(4).then(buffer => + this._setResult('getAsyncBuffer', Array.from(new Uint8Array(buffer))), + ), }; // $FlowFixMe[missing-local-annot] diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 7678150d953..7029d1e8022 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2827,6 +2827,11 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB public ~JByteBufferMutableBuffer() override; } +class facebook::react::JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public static constexpr auto kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::JCxxCallbackImpl : public jni::HybridClass { public static constexpr auto kJavaDescriptor; public static void registerNatives(); @@ -3102,7 +3107,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule { protected void configureEventEmitterCallback(); protected void setEventEmitterCallback(jni::alias_ref); public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params); - public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID); + public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID, bool promiseResolveSupportsArrayBuffer = false); public virtual ~JavaTurboModule(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index f41c5ee0f9e..97557a55b37 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2785,6 +2785,11 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB public ~JByteBufferMutableBuffer() override; } +class facebook::react::JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public static constexpr auto kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::JCxxCallbackImpl : public jni::HybridClass { public static constexpr auto kJavaDescriptor; public static void registerNatives(); @@ -3021,7 +3026,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule { protected void configureEventEmitterCallback(); protected void setEventEmitterCallback(jni::alias_ref); public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params); - public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID); + public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID, bool promiseResolveSupportsArrayBuffer = false); public virtual ~JavaTurboModule(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index ce226ef3688..93dfd348e19 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2824,6 +2824,11 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB public ~JByteBufferMutableBuffer() override; } +class facebook::react::JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public static constexpr auto kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::JCxxCallbackImpl : public jni::HybridClass { public static constexpr auto kJavaDescriptor; public static void registerNatives(); @@ -3099,7 +3104,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule { protected void configureEventEmitterCallback(); protected void setEventEmitterCallback(jni::alias_ref); public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params); - public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID); + public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID, bool promiseResolveSupportsArrayBuffer = false); public virtual ~JavaTurboModule(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 4f8b3d6c84a..4fc3062117c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2521,6 +2521,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); + public virtual void getAsyncBuffer:resolve:reject:(double size, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); 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); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 77caf262fb2..f9e235d65e9 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -2514,6 +2514,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); + public virtual void getAsyncBuffer:resolve:reject:(double size, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); 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); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 1cabf9b1f1a..6f6873100ec 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2521,6 +2521,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); + public virtual void getAsyncBuffer:resolve:reject:(double size, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); 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); From c894a0dc8ad81b572583f48914b5116f07060f40 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Wed, 19 Aug 2026 09:05:52 -0700 Subject: [PATCH 4/4] Align SampleTurboModuleExample UI with NativeCxxModuleExampleExample (#57985) Summary: Pull Request resolved: https://github.com/react/react-native/pull/57985 The two RNTester TurboModule example screens had drifted apart, making it hard to compare TurboModule and C++ TurboModule behaviour side by side. Align `SampleTurboModuleExample` with the style already used by `NativeCxxModuleExampleExample`: - Order and group the entries in `_tests` the same way (callback, ArrayBuffer group, `get*` group, promises, `voidFunc`), so the buttons render in the same order on both screens. - Make the `Examples` union match the tests that actually exist; it still listed many entries copied from the C++ example that `SampleTurboModule` does not implement (`getCustomHostObject`, `getSet`, `setMenuItem`, ...) and was missing `getEnum`, `getRootTag` and `getUnsafeObject`. - Add the missing `installJSIBindings` entry to `ErrorExamples` and type `_renderResult` as `Examples | ErrorExamples`. - Surface rejected promises from the error tests in the UI instead of only logging them to the console, matching the other screen. - Fix `getUnsafeObject` to call `getUnsafeObject` instead of `getObject`. - Merge the duplicated `NativeSampleTurboModule` imports and drop a stale Flow suppression. Changelog: [Internal] Differential Revision: D116375214 --- .../TurboModule/SampleTurboModuleExample.js | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index 4b63a4cb073..f11aa856dc3 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -14,8 +14,9 @@ import RNTesterText from '../../components/RNTesterText'; import styles from './TurboModuleExampleCommon'; import * as React from 'react'; import {FlatList, RootTagContext, TouchableOpacity, View} from 'react-native'; -import NativeSampleTurboModule from 'react-native/Libraries/TurboModule/samples/NativeSampleTurboModule'; -import {EnumInt} from 'react-native/Libraries/TurboModule/samples/NativeSampleTurboModule'; +import NativeSampleTurboModule, { + EnumInt, +} from 'react-native/Libraries/TurboModule/samples/NativeSampleTurboModule'; type State = { testResults: { @@ -33,6 +34,7 @@ type Examples = | 'getArray' | 'getBool' | 'getConstants' + | 'getEnum' | 'getCustomEnum' | 'getCustomHostObject' | 'getBinaryTreeNode' @@ -42,9 +44,11 @@ type Examples = | 'getMap' | 'getNumber' | 'getObject' + | 'getRootTag' | 'getSet' | 'getString' | 'getUnion' + | 'getUnsafeObject' | 'getValue' | 'getArrayBuffer' | 'createNativeBuffer' @@ -63,7 +67,8 @@ type ErrorExamples = | 'promiseThrows' | 'voidFuncAssert' | 'getObjectAssert' - | 'promiseAssert'; + | 'promiseAssert' + | 'installJSIBindings'; class SampleTurboModuleExample extends React.Component<{}, State> { static contextType: React.Context = RootTagContext; @@ -80,38 +85,12 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.getValueWithCallback(callbackValue => this._setResult('callback', callbackValue), ), - promise: () => - NativeSampleTurboModule.getValueWithPromise(false).then(valuePromise => - this._setResult('promise', valuePromise), - ), - rejectPromise: () => - NativeSampleTurboModule.getValueWithPromise(true) - .then(() => {}) - .catch(e => { - this._setResult('rejectPromise', e.message); - }), - getConstants: () => NativeSampleTurboModule.getConstants(), - voidFunc: () => NativeSampleTurboModule.voidFunc(), - getBool: () => NativeSampleTurboModule.getBool(true), - getEnum: () => - NativeSampleTurboModule.getEnum - ? NativeSampleTurboModule.getEnum(EnumInt.A) - : null, - getNumber: () => NativeSampleTurboModule.getNumber(99.95), - getString: () => NativeSampleTurboModule.getString('Hello'), getArray: () => NativeSampleTurboModule.getArray([ {a: 1, b: 'foo'}, {a: 2, b: 'bar'}, null, ]), - getObject: () => - NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}), - getUnsafeObject: () => - NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}), - getRootTag: () => NativeSampleTurboModule.getRootTag(this.context), - getValue: () => - NativeSampleTurboModule.getValue(5, 'test', {a: 1, b: 'foo'}), getArrayBuffer: () => { const input = new Uint8Array([1, 2, 3, 4]); const result = NativeSampleTurboModule.getArrayBuffer(input.buffer); @@ -135,6 +114,30 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.getAsyncBuffer(4).then(buffer => this._setResult('getAsyncBuffer', Array.from(new Uint8Array(buffer))), ), + getBool: () => NativeSampleTurboModule.getBool(true), + getConstants: () => NativeSampleTurboModule.getConstants(), + getEnum: () => + NativeSampleTurboModule.getEnum + ? NativeSampleTurboModule.getEnum(EnumInt.A) + : null, + getNumber: () => NativeSampleTurboModule.getNumber(99.95), + getObject: () => + NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}), + getRootTag: () => NativeSampleTurboModule.getRootTag(this.context), + getString: () => NativeSampleTurboModule.getString('Hello'), + getUnsafeObject: () => + NativeSampleTurboModule.getUnsafeObject({a: 1, b: 'foo', c: null}), + getValue: () => + NativeSampleTurboModule.getValue(5, 'test', {a: 1, b: 'foo'}), + promise: () => + NativeSampleTurboModule.getValueWithPromise(false).then(valuePromise => + this._setResult('promise', valuePromise), + ), + rejectPromise: () => + NativeSampleTurboModule.getValueWithPromise(true) + .then(() => {}) + .catch(e => this._setResult('rejectPromise', e.message)), + voidFunc: () => NativeSampleTurboModule.voidFunc(), }; // $FlowFixMe[missing-local-annot] @@ -143,7 +146,6 @@ class SampleTurboModuleExample extends React.Component<{}, State> { try { NativeSampleTurboModule.voidFuncThrows?.(); } catch (e) { - console.error(e); return e.message; } }, @@ -151,22 +153,17 @@ class SampleTurboModuleExample extends React.Component<{}, State> { try { NativeSampleTurboModule.getObjectThrows?.({a: 1, b: 'foo', c: null}); } catch (e) { - console.error(e); return e.message; } }, - promiseThrows: () => { + promiseThrows: () => NativeSampleTurboModule.promiseThrows?.() .then(() => {}) - .catch(e => { - console.error(e); - }); - }, + .catch(e => this._setResult('promiseThrows', e.message)), voidFuncAssert: () => { try { NativeSampleTurboModule.voidFuncAssert?.(); } catch (e) { - console.error(e); return e.message; } }, @@ -174,20 +171,14 @@ class SampleTurboModuleExample extends React.Component<{}, State> { try { NativeSampleTurboModule.getObjectAssert?.({a: 1, b: 'foo', c: null}); } catch (e) { - console.error(e); return e.message; } }, - promiseAssert: () => { + promiseAssert: () => NativeSampleTurboModule.promiseAssert?.() .then(() => {}) - .catch(e => { - console.error(e); - }); - }, - installJSIBindings: () => { - return global.__SampleTurboModuleJSIBindings; - }, + .catch(e => this._setResult('promiseAssert', e.message)), + installJSIBindings: () => global.__SampleTurboModuleJSIBindings, }; _setResult( @@ -204,9 +195,6 @@ class SampleTurboModuleExample extends React.Component<{}, State> { | Array<$FlowFixMe>, ) { this.setState(({testResults}) => ({ - /* $FlowFixMe[cannot-spread-indexer] (>=0.122.0 site=react_native_fb) - * This comment suppresses an error found when Flow v0.122.0 was - * deployed. To see the error, delete this comment and run Flow. */ testResults: { ...testResults, /* $FlowFixMe[invalid-computed-prop] (>=0.111.0 site=react_native_fb) @@ -217,7 +205,7 @@ class SampleTurboModuleExample extends React.Component<{}, State> { })); } - _renderResult(name: string): React.Node { + _renderResult(name: Examples | ErrorExamples): React.Node { const result = this.state.testResults[name] || {}; return ( @@ -282,7 +270,7 @@ class SampleTurboModuleExample extends React.Component<{}, State> { ) }> - Run all tests + Run function call tests