diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index efb710aaae0..e07177899af 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -273,8 +273,8 @@ function translateFunctionParamToJavaType( imports.add('com.facebook.react.bridge.Callback'); return wrapOptional('Callback', isRequired); case 'ArrayBufferTypeAnnotation': - imports.add('java.nio.ByteBuffer'); - return wrapOptional('ByteBuffer', isRequired); + imports.add('com.facebook.react.bridge.ArrayBuffer'); + return wrapOptional('ArrayBuffer', isRequired); default: realTypeAnnotation.type as 'MixedTypeAnnotation'; throw new Error(createErrorMessage(realTypeAnnotation.type)); @@ -370,8 +370,8 @@ function translateFunctionReturnTypeToJavaType( imports.add('com.facebook.react.bridge.WritableArray'); return wrapOptional('WritableArray', isRequired); case 'ArrayBufferTypeAnnotation': - imports.add('java.nio.ByteBuffer'); - return wrapOptional('ByteBuffer', isRequired); + imports.add('com.facebook.react.bridge.ArrayBuffer'); + return wrapOptional('ArrayBuffer', isRequired); default: realTypeAnnotation.type as 'MixedTypeAnnotation'; throw new Error(createErrorMessage(realTypeAnnotation.type)); diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index 56bc6f94180..7c088461107 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -311,7 +311,7 @@ function translateParamTypeToJniType( case 'FunctionTypeAnnotation': return 'Lcom/facebook/react/bridge/Callback;'; case 'ArrayBufferTypeAnnotation': - return 'Ljava/nio/ByteBuffer;'; + return 'Lcom/facebook/react/bridge/ArrayBuffer;'; default: realTypeAnnotation.type as 'MixedTypeAnnotation'; throw new Error( @@ -397,7 +397,7 @@ function translateReturnTypeToJniType( case 'ArrayTypeAnnotation': return 'Lcom/facebook/react/bridge/WritableArray;'; case 'ArrayBufferTypeAnnotation': - return 'Ljava/nio/ByteBuffer;'; + return 'Lcom/facebook/react/bridge/ArrayBuffer;'; default: realTypeAnnotation.type as 'MixedTypeAnnotation'; throw new Error( 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 a5a12496118..8d05e95c90f 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 @@ -58,11 +58,11 @@ Map { package com.facebook.fbreact.specs; import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.ArrayBuffer; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.turbomodule.core.interfaces.TurboModule; -import java.nio.ByteBuffer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -80,15 +80,15 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo @ReactMethod(isBlockingSynchronousMethod = true) @DoNotStrip - public abstract ByteBuffer getArrayBuffer(); + public abstract ArrayBuffer getArrayBuffer(); @ReactMethod @DoNotStrip - public abstract void voidArrayBuffer(ByteBuffer arg); + public abstract void voidArrayBuffer(ArrayBuffer arg); @ReactMethod @DoNotStrip - public abstract void voidNullableArrayBuffer(@Nullable ByteBuffer arg); + public abstract void voidNullableArrayBuffer(@Nullable ArrayBuffer arg); } ", } 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 801fc744dac..547344fda40 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 @@ -53,17 +53,17 @@ namespace facebook::react { static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { static jmethodID cachedMethodId = nullptr; - return static_cast(turboModule).invokeJavaMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", \\"()Ljava/nio/ByteBuffer;\\", args, count, cachedMethodId); + return static_cast(turboModule).invokeJavaMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", \\"()Lcom/facebook/react/bridge/ArrayBuffer;\\", args, count, cachedMethodId); } static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { static jmethodID cachedMethodId = nullptr; - return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidArrayBuffer\\", \\"(Ljava/nio/ByteBuffer;)V\\", args, count, cachedMethodId); + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId); } static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { static jmethodID cachedMethodId = nullptr; - return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Ljava/nio/ByteBuffer;)V\\", args, count, cachedMethodId); + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId); } NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ArrayBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ArrayBuffer.kt new file mode 100644 index 00000000000..f409cc3cdea --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ArrayBuffer.kt @@ -0,0 +1,120 @@ +/* + * 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 +import java.nio.ByteBuffer + +/** + * A fixed-length byte buffer for TurboModule `ArrayBuffer` arguments and return values. + * + * @property isOwningBytes: + * - `true` — safe to retain and return to JS. Synchronize externally if JS may + * touch the same memory concurrently. + * - `false` — bytes are borrowed from a JS `ArrayBuffer` for the current + * synchronous call only. Copy with [arrayBufferWithCopiedBytes] to keep them. + */ +@DoNotStrip +public class ArrayBuffer : HybridClassBase { + + private val buffer: ByteBuffer + + /** Whether this buffer owns its bytes. See the class documentation. */ + public val isOwningBytes: Boolean + + @DoNotStrip + private constructor(buffer: ByteBuffer, isOwningBytes: Boolean) : super() { + this.buffer = buffer + this.isOwningBytes = isOwningBytes + } + + /** @param size number of zero-filled bytes to allocate */ + public constructor(size: Int) : this(allocateDirect(size), true) { + initHybrid(buffer) + } + + public val bytes: ByteBuffer + get() = buffer + + public val size: Int + get() = buffer.capacity() + + private external fun initHybrid(buffer: ByteBuffer) + + public companion object { + init { + ReactNativeJniCommonSoLoader.staticInit() + } + + /** @param size number of zero-filled bytes to allocate. Same as `ArrayBuffer(size)`. */ + @JvmStatic + @DoNotStrip + public fun arrayBufferWithLength(size: Int): ArrayBuffer = ArrayBuffer(size) + + /** @param bytes copied into a new owning buffer */ + @JvmStatic + @DoNotStrip + public fun arrayBufferWithCopiedBytes(bytes: ByteArray): ArrayBuffer { + val buffer = ArrayBuffer(bytes.size) + if (bytes.isNotEmpty()) { + buffer.bytes.put(bytes) + buffer.bytes.rewind() + } + return buffer + } + + /** @param source remaining bytes are copied into a new owning buffer */ + @JvmStatic + @DoNotStrip + public fun arrayBufferWithCopiedBytes(source: ByteBuffer): ArrayBuffer { + val length = source.remaining() + val buffer = ArrayBuffer(length) + if (length > 0) { + buffer.bytes.put(source.duplicate()) + buffer.bytes.rewind() + } + return buffer + } + + /** + * @param source copied into a new owning buffer. Use to keep bytes from a non-owning argument + * after the call returns. + */ + @JvmStatic + @DoNotStrip + public fun arrayBufferWithCopiedBytes(source: ArrayBuffer): ArrayBuffer { + val length = source.size + val buffer = ArrayBuffer(length) + if (length > 0) { + val src = source.bytes.duplicate() + src.position(0) + src.limit(length) + buffer.bytes.put(src) + buffer.bytes.rewind() + } + return buffer + } + + /** + * @param buffer direct [ByteBuffer] to alias without copying. The caller must keep it valid for + * as long as this [ArrayBuffer] lives. + */ + @JvmStatic + @DoNotStrip + public fun arrayBufferWithOwnedBytes(buffer: ByteBuffer): ArrayBuffer { + require(buffer.isDirect) { "arrayBufferWithOwnedBytes requires a direct ByteBuffer" } + return ArrayBuffer(buffer, true).apply { initHybrid(buffer) } + } + + private fun allocateDirect(size: Int): ByteBuffer { + require(size >= 0) { "ArrayBuffer size must not be negative, got $size" } + return ByteBuffer.allocateDirect(size) + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt index 8bd7ef473c8..ee4912f976b 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt @@ -23,6 +23,7 @@ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-ut add_library( reactnativejni_common OBJECT + JArrayBuffer.cpp JDynamicNative.cpp JReactMarker.cpp NativeArray.cpp 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 new file mode 100644 index 00000000000..33f31247d6c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.cpp @@ -0,0 +1,116 @@ +/* + * 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 "JArrayBuffer.h" + +#include +#include +#include + +#include "JByteBufferMutableBuffer.h" + +namespace facebook::react { + +namespace { + +// Holds a copy of bytes borrowed from a JS ArrayBuffer. +class OwnedBytesBuffer final : public jsi::MutableBuffer { + public: + explicit OwnedBytesBuffer(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_; +}; + +} // namespace + +void JArrayBuffer::registerNatives() +{ + registerHybrid({ + makeNativeMethod("initHybrid", JArrayBuffer::initHybrid), + }); +} + +void JArrayBuffer::initHybrid( + jni::alias_ref jobj, + jni::alias_ref buffer) +{ + setCxxInstance(jobj, std::make_shared(buffer), true); +} + +jni::local_ref JArrayBuffer::create( + jni::local_ref byteBuffer, + std::shared_ptr buffer, + bool owningBytes) +{ + auto cxxPart = std::make_unique(std::move(buffer), owningBytes); + auto javaPart = newObjectJavaArgs(byteBuffer, owningBytes); + setNativePointer(javaPart, std::move(cxxPart)); + return javaPart; +} + +jni::local_ref JArrayBuffer::createOwning( + std::shared_ptr buffer) +{ + // NewDirectByteBuffer rejects a null address, which is what an empty + // jsi::ArrayBuffer reports, so empty buffers get an allocation of their own. + if (buffer->size() == 0) { + return create(jni::JByteBuffer::allocateDirect(0), std::move(buffer), true); + } + + 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) +{ + if (size == 0) { + return createOwned(nullptr, 0); + } + + 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(const void *bytes, size_t size) +{ + auto byteBuffer = jni::JByteBuffer::allocateDirect(static_cast(size)); + if (size > 0 && bytes != nullptr) { + // @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy + std::memcpy(byteBuffer->getDirectBytes(), bytes, size); + } + + auto buffer = std::make_shared(byteBuffer); + return create(std::move(byteBuffer), std::move(buffer), true); +} + +std::shared_ptr JArrayBuffer::toJSBuffer(jni::alias_ref arrayBuffer) +{ + auto *self = arrayBuffer->cthis(); + if (self->owningBytes_) { + return self->buffer_; + } + + // Borrowed bytes still belong to the inbound JS ArrayBuffer; copy them before + // handing a new buffer back to JS. + auto *data = self->buffer_->data(); + auto size = self->buffer_->size(); + return std::make_shared(std::vector(data, data + size)); +} + +} // 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 new file mode 100644 index 00000000000..8e014a9192d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBuffer.h @@ -0,0 +1,65 @@ +/* + * 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 +#include + +namespace facebook::react { + +// JNI side of com.facebook.react.bridge.ArrayBuffer. +// +// When owningBytes_ is true, the module may retain the buffer and return it to +// JS without copying. When false, the bytes were borrowed from a JS-heap +// ArrayBuffer for a synchronous call only. +class JArrayBuffer : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/ArrayBuffer;"; + + static void registerNatives(); + + // 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); + + // 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); + + // Convert a module return value for rt.createArrayBuffer. Owning buffers pass + // through; borrowed ones are copied because createArrayBuffer needs its own + // backing store. + static std::shared_ptr toJSBuffer(jni::alias_ref arrayBuffer); + + JArrayBuffer(std::shared_ptr buffer, bool owningBytes) noexcept + : buffer_(std::move(buffer)), owningBytes_(owningBytes) + { + } + + private: + friend HybridBase; + + static void initHybrid(jni::alias_ref jobj, jni::alias_ref buffer); + + static jni::local_ref create( + jni::local_ref byteBuffer, + std::shared_ptr buffer, + bool owningBytes); + + std::shared_ptr buffer_; + bool owningBytes_; +}; + +} // 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 293d2ffc8ab..8c6787bdde6 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 @@ -6,6 +6,7 @@ */ #include +#include "JArrayBuffer.h" #include "JCallback.h" #include "JDynamicNative.h" #include "JReactMarker.h" @@ -18,6 +19,7 @@ namespace facebook::react { extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return facebook::jni::initialize(vm, [] { + JArrayBuffer::registerNatives(); JCxxCallbackImpl::registerNatives(); JDynamicNative::registerNatives(); JReactMarker::registerNatives(); 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 944b59c4f49..8430640bc41 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 @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include #include @@ -315,6 +315,8 @@ JNIArgs convertJSIArgsToJNIArgs( auto& jargs = jniArgs.args; auto& globalRefs = jniArgs.globalRefs; + auto isSyncInvocation = valueKind != VoidKind && valueKind != PromiseKind; + auto makeGlobalIfNecessary = [&](jobject obj) { if (valueKind == VoidKind || valueKind == PromiseKind) { jobject globalObj = env->NewGlobalRef(obj); @@ -438,7 +440,7 @@ JNIArgs convertJSIArgsToJNIArgs( auto dynamicFromValue = jsi::dynamicFromValue(rt, *arg); auto jParams = JDynamicNative::newObjectCxxArgs(dynamicFromValue); jarg->l = makeGlobalIfNecessary(jParams.release()); - } else if (type == "Ljava/nio/ByteBuffer;") { + } else if (type == "Lcom/facebook/react/bridge/ArrayBuffer;") { if (!(arg->isObject() && arg->getObject(rt).isArrayBuffer(rt))) { throw JavaTurboModuleArgumentConversionException( "ArrayBuffer", argIndex, methodName, arg, &rt); @@ -457,18 +459,24 @@ JNIArgs convertJSIArgsToJNIArgs( rt, "JavaTurboModule::convertJSIArgsToJNIArgs: ArrayBuffer exceeds maximum size."); } - auto data = arrayBuffer.data(rt); - // ArrayBuffer arguments are always copied into a Java-owned direct - // ByteBuffer, so Java fully owns the bytes. Borrowing the JS bytes is - // never safe — even on a synchronous call the module may retain the - // buffer or hand it to an async method, and JS may garbage-collect the - // source ArrayBuffer, leaving Java with a dangling view. - auto buffer = jni::JByteBuffer::allocateDirect(static_cast(size)); - if (size > 0) { - // @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy - std::memcpy(buffer->getDirectBytes(), data, size); - } - jarg->l = makeGlobalIfNecessary(buffer.release()); + + auto jArrayBuffer = [&]() { + // 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 (auto mutableBuffer = arrayBuffer.tryGetMutableBuffer(rt)) { + return JArrayBuffer::createOwning(std::move(mutableBuffer)); + } + + // JS heap bytes on a synchronous call: lend them for the duration of + // the call. + if (isSyncInvocation) { + return JArrayBuffer::createUnowned(arrayBuffer.data(rt), size); + } + + // JS heap bytes that outlive the call: copy. + return JArrayBuffer::createOwned(arrayBuffer.data(rt), size); + }(); + jarg->l = makeGlobalIfNecessary(jArrayBuffer.release()); } else { throw JavaTurboModuleInvalidArgumentTypeException( type, argIndex, methodName); @@ -1008,22 +1016,12 @@ jsi::Value JavaTurboModule::invokeJavaMethod( jsi::Value returnValue = jsi::Value::null(); if (returnObject != nullptr) { - auto jByteBuffer = jni::adopt_local( - static_cast(returnObject)); - - if (!jByteBuffer->isDirect()) { - throw jsi::JSError( - runtime, - "Only direct ByteBuffers (ByteBuffer.allocateDirect) can be returned from a TurboModule."); - } - // Zero-copy: JByteBufferMutableBuffer takes a global reference that - // pins the ByteBuffer's memory for the lifetime of the JS ArrayBuffer, - // and its destructor attaches the current thread before releasing that - // ref, so JS GC finalization on any thread is safe. - auto nativeBuffer = - std::make_shared(jByteBuffer); + auto jArrayBuffer = jni::adopt_local( + static_cast(returnObject)); returnValue = { - runtime, jsi::ArrayBuffer{runtime, std::move(nativeBuffer)}}; + runtime, + jsi::ArrayBuffer{ + runtime, JArrayBuffer::toJSBuffer(jArrayBuffer)}}; } TMPL::syncMethodCallReturnConversionEnd(moduleName, methodName); 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 c8a36acc32b..28767bacfe4 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 @@ -14,6 +14,7 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.bridge.ArrayBuffer import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Callback import com.facebook.react.bridge.Promise @@ -27,7 +28,6 @@ import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings -import java.nio.ByteBuffer import java.util.UUID @DoNotStrip @@ -155,28 +155,35 @@ public class SampleTurboModule(private val context: ReactApplicationContext) : return map } + // Mutating the argument updates the JS ArrayBuffer in place. @DoNotStrip @Suppress("unused") - override fun getArrayBuffer(buffer: ByteBuffer?): ByteBuffer? { + override fun getArrayBuffer(buffer: ArrayBuffer?): ArrayBuffer? { + if (buffer != null) { + val bytes = buffer.bytes + for (i in 0 until bytes.capacity()) { + bytes.put(i, (bytes.get(i) * 2).toByte()) + } + } log("getArrayBuffer", buffer, buffer) return buffer } @DoNotStrip @Suppress("unused") - override fun createNativeBuffer(size: Double): ByteBuffer { + override fun createNativeBuffer(size: Double): ArrayBuffer { require(size.isFinite() && size >= 0.0 && size <= Int.MAX_VALUE.toDouble()) { "createNativeBuffer: size must be a finite value in [0, ${Int.MAX_VALUE}], got $size" } - val buffer = ByteBuffer.allocateDirect(size.toInt()) + val buffer = ArrayBuffer(size.toInt()) log("createNativeBuffer", size, buffer) return buffer } @DoNotStrip @Suppress("unused") - override fun processAsyncBuffer(payload: ByteBuffer?, promise: Promise) { - promise.resolve((payload?.capacity() ?: 0).toDouble()) + override fun processAsyncBuffer(payload: ArrayBuffer?, promise: Promise) { + promise.resolve((payload?.size ?: 0).toDouble()) } @DoNotStrip diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 5588b49856f..9f61ad231f9 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2792,6 +2792,16 @@ class facebook::react::IntersectionObserverState { public static facebook::react::IntersectionObserverState NotIntersecting(); } +class facebook::react::JArrayBuffer : public jni::HybridClass { + public JArrayBuffer(std::shared_ptr buffer, bool owningBytes) noexcept; + 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 std::shared_ptr toJSBuffer(jni::alias_ref arrayBuffer); + public static void registerNatives(); +} + class facebook::react::JBindingsInstaller : public jni::HybridClass, public facebook::react::BindingsInstaller { public static constexpr auto kJavaDescriptor; public ~JBindingsInstaller(); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 3f4f51314fa..2b17df88d15 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2750,6 +2750,16 @@ class facebook::react::IntersectionObserverState { public static facebook::react::IntersectionObserverState NotIntersecting(); } +class facebook::react::JArrayBuffer : public jni::HybridClass { + public JArrayBuffer(std::shared_ptr buffer, bool owningBytes) noexcept; + 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 std::shared_ptr toJSBuffer(jni::alias_ref arrayBuffer); + public static void registerNatives(); +} + class facebook::react::JBindingsInstaller : public jni::HybridClass, public facebook::react::BindingsInstaller { public static constexpr auto kJavaDescriptor; public ~JBindingsInstaller(); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index d3a8f199349..ee84c173bc8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2789,6 +2789,16 @@ class facebook::react::IntersectionObserverState { public static facebook::react::IntersectionObserverState NotIntersecting(); } +class facebook::react::JArrayBuffer : public jni::HybridClass { + public JArrayBuffer(std::shared_ptr buffer, bool owningBytes) noexcept; + 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 std::shared_ptr toJSBuffer(jni::alias_ref arrayBuffer); + public static void registerNatives(); +} + class facebook::react::JBindingsInstaller : public jni::HybridClass, public facebook::react::BindingsInstaller { public static constexpr auto kJavaDescriptor; public ~JBindingsInstaller();