2020#include < ReactCommon/TurboModule.h>
2121#include < ReactCommon/TurboModulePerfLogger.h>
2222#include < jsi/JSIDynamic.h>
23+ #include < react/bridging/ArrayBuffer.h>
2324#include < react/bridging/Bridging.h>
2425#include < react/debug/react_native_assert.h>
2526#include < react/featureflags/ReactNativeFeatureFlags.h>
@@ -63,6 +64,15 @@ JavaTurboModule::~JavaTurboModule() {
6364
6465namespace {
6566
67+ // Aliased ArrayBuffer arg passed without copying. nativeOwner keeps native
68+ // bytes alive for the invocation, or is null when JS heap bytes were borrowed
69+ // for a synchronous call.
70+ struct AliasedArrayBuffer {
71+ const uint8_t * bytes{nullptr };
72+ size_t size{0 };
73+ std::shared_ptr<jsi::MutableBuffer> nativeOwner;
74+ };
75+
6676struct JNIArgs {
6777 JNIArgs (size_t count) : args(count) {}
6878 JNIArgs (const JNIArgs&) = delete ;
@@ -73,6 +83,7 @@ struct JNIArgs {
7383
7484 std::vector<jvalue> args;
7585 std::vector<jobject> globalRefs;
86+ std::vector<AliasedArrayBuffer> aliasedArrayBuffers;
7687
7788 ~JNIArgs () {
7889 JNIEnv* env = jni::Environment::current ();
@@ -315,6 +326,8 @@ JNIArgs convertJSIArgsToJNIArgs(
315326 auto & jargs = jniArgs.args ;
316327 auto & globalRefs = jniArgs.globalRefs ;
317328
329+ auto isSyncInvocation = valueKind != VoidKind && valueKind != PromiseKind;
330+
318331 auto makeGlobalIfNecessary = [&](jobject obj) {
319332 if (valueKind == VoidKind || valueKind == PromiseKind) {
320333 jobject globalObj = env->NewGlobalRef (obj);
@@ -457,17 +470,42 @@ JNIArgs convertJSIArgsToJNIArgs(
457470 rt,
458471 " JavaTurboModule::convertJSIArgsToJNIArgs: ArrayBuffer exceeds maximum size." );
459472 }
460- auto data = arrayBuffer.data (rt);
461- // ArrayBuffer arguments are always copied into a Java-owned direct
462- // ByteBuffer, so Java fully owns the bytes. Borrowing the JS bytes is
463- // never safe — even on a synchronous call the module may retain the
464- // buffer or hand it to an async method, and JS may garbage-collect the
465- // source ArrayBuffer, leaving Java with a dangling view.
466- auto buffer = jni::JByteBuffer::allocateDirect (static_cast <jint>(size));
467- if (size > 0 ) {
468- // @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy
469- std::memcpy (buffer->getDirectBytes (), data, size);
473+
474+ // Empty buffers cannot use NewDirectByteBuffer; allocate an empty direct buffer.
475+ if (size == 0 ) {
476+ jarg->l = makeGlobalIfNecessary (
477+ jni::JByteBuffer::allocateDirect (0 ).release ());
478+ continue ;
479+ }
480+
481+ // Native-backed: alias and retain MutableBuffer for the invocation.
482+ if (auto mutableBuffer = arrayBuffer.tryGetMutableBuffer (rt)) {
483+ // Round-trip a Java-origin buffer as the same ByteBuffer object.
484+ auto * javaBacked =
485+ dynamic_cast <JByteBufferMutableBuffer*>(mutableBuffer.get ());
486+ auto byteBuffer = javaBacked != nullptr
487+ ? javaBacked->getJavaByteBuffer ()
488+ : jni::JByteBuffer::wrapBytes (mutableBuffer->data (), size);
489+ jniArgs.aliasedArrayBuffers .push_back (
490+ {byteBuffer->getDirectBytes (), size, std::move (mutableBuffer)});
491+ jarg->l = makeGlobalIfNecessary (byteBuffer.release ());
492+ continue ;
493+ }
494+
495+ // JS heap bytes: borrow for sync calls only.
496+ if (isSyncInvocation) {
497+ auto byteBuffer =
498+ jni::JByteBuffer::wrapBytes (arrayBuffer.data (rt), size);
499+ jniArgs.aliasedArrayBuffers .push_back (
500+ {byteBuffer->getDirectBytes (), size, nullptr });
501+ jarg->l = makeGlobalIfNecessary (byteBuffer.release ());
502+ continue ;
470503 }
504+
505+ // Async: copy so bytes outlive the JS call.
506+ auto buffer = jni::JByteBuffer::allocateDirect (static_cast <jint>(size));
507+ // @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy
508+ std::memcpy (buffer->getDirectBytes (), arrayBuffer.data (rt), size);
471509 jarg->l = makeGlobalIfNecessary (buffer.release ());
472510 } else {
473511 throw JavaTurboModuleInvalidArgumentTypeException (
@@ -1016,12 +1054,30 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
10161054 runtime,
10171055 " Only direct ByteBuffers (ByteBuffer.allocateDirect) can be returned from a TurboModule." );
10181056 }
1019- // Zero-copy: JByteBufferMutableBuffer takes a global reference that
1020- // pins the ByteBuffer's memory for the lifetime of the JS ArrayBuffer,
1021- // and its destructor attaches the current thread before releasing that
1022- // ref, so JS GC finalization on any thread is safe.
1023- auto nativeBuffer =
1024- std::make_shared<JByteBufferMutableBuffer>(jByteBuffer);
1057+ auto * returnedBytes = jByteBuffer->getDirectBytes ();
1058+ auto returnedSize = jByteBuffer->getDirectSize ();
1059+ std::shared_ptr<jsi::MutableBuffer> nativeBuffer;
1060+
1061+ // Exact argument match: reuse nativeOwner or copy borrowed bytes.
1062+ for (const auto & aliased : jniArgs.aliasedArrayBuffers ) {
1063+ if (aliased.bytes != returnedBytes || aliased.size != returnedSize) {
1064+ continue ;
1065+ }
1066+ if (aliased.nativeOwner != nullptr ) {
1067+ nativeBuffer = aliased.nativeOwner ;
1068+ } else {
1069+ nativeBuffer =
1070+ std::make_shared<detail::OwnedBytesBuffer>(std::vector<uint8_t >(
1071+ returnedBytes, returnedBytes + returnedSize));
1072+ }
1073+ break ;
1074+ }
1075+
1076+ if (nativeBuffer == nullptr ) {
1077+ nativeBuffer =
1078+ std::make_shared<JByteBufferMutableBuffer>(jByteBuffer);
1079+ }
1080+
10251081 returnValue = {
10261082 runtime, jsi::ArrayBuffer{runtime, std::move (nativeBuffer)}};
10271083 }
0 commit comments