Skip to content

Commit eb9740a

Browse files
committed
feat: add zero-copy ArrayBuffer for Java TM
1 parent ebfcd88 commit eb9740a

6 files changed

Lines changed: 86 additions & 16 deletions

File tree

packages/react-native/ReactAndroid/src/main/jni/react/jni/JByteBufferMutableBuffer.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class JByteBufferMutableBuffer final : public jsi::MutableBuffer {
3939
JByteBufferMutableBuffer(JByteBufferMutableBuffer &&) = delete;
4040
JByteBufferMutableBuffer &operator=(JByteBufferMutableBuffer &&) = delete;
4141

42+
jni::local_ref<jni::JByteBuffer> getJavaByteBuffer() const
43+
{
44+
return jni::make_local(byteBuffer_);
45+
}
46+
4247
size_t size() const override
4348
{
4449
return byteBuffer_->getDirectSize();

packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
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

6465
namespace {
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+
6676
struct 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
}

packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,15 @@ public class SampleTurboModule(private val context: ReactApplicationContext) :
155155
return map
156156
}
157157

158+
// Mutating the argument updates the JS ArrayBuffer in place.
158159
@DoNotStrip
159160
@Suppress("unused")
160161
override fun getArrayBuffer(buffer: ByteBuffer?): ByteBuffer? {
162+
if (buffer != null) {
163+
for (i in 0 until buffer.capacity()) {
164+
buffer.put(i, (buffer.get(i) * 2).toByte())
165+
}
166+
}
161167
log("getArrayBuffer", buffer, buffer)
162168
return buffer
163169
}

scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2803,6 +2803,7 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB
28032803
public JByteBufferMutableBuffer(jni::alias_ref<jni::JByteBuffer> byteBuffer);
28042804
public facebook::react::JByteBufferMutableBuffer& operator=(const facebook::react::JByteBufferMutableBuffer&) = delete;
28052805
public facebook::react::JByteBufferMutableBuffer& operator=(facebook::react::JByteBufferMutableBuffer&&) = delete;
2806+
public jni::local_ref<jni::JByteBuffer> getJavaByteBuffer() const;
28062807
public virtual size_t size() const override;
28072808
public virtual uint8_t* data() override;
28082809
public ~JByteBufferMutableBuffer() override;

scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2761,6 +2761,7 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB
27612761
public JByteBufferMutableBuffer(jni::alias_ref<jni::JByteBuffer> byteBuffer);
27622762
public facebook::react::JByteBufferMutableBuffer& operator=(const facebook::react::JByteBufferMutableBuffer&) = delete;
27632763
public facebook::react::JByteBufferMutableBuffer& operator=(facebook::react::JByteBufferMutableBuffer&&) = delete;
2764+
public jni::local_ref<jni::JByteBuffer> getJavaByteBuffer() const;
27642765
public virtual size_t size() const override;
27652766
public virtual uint8_t* data() override;
27662767
public ~JByteBufferMutableBuffer() override;

scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,7 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB
28002800
public JByteBufferMutableBuffer(jni::alias_ref<jni::JByteBuffer> byteBuffer);
28012801
public facebook::react::JByteBufferMutableBuffer& operator=(const facebook::react::JByteBufferMutableBuffer&) = delete;
28022802
public facebook::react::JByteBufferMutableBuffer& operator=(facebook::react::JByteBufferMutableBuffer&&) = delete;
2803+
public jni::local_ref<jni::JByteBuffer> getJavaByteBuffer() const;
28032804
public virtual size_t size() const override;
28042805
public virtual uint8_t* data() override;
28052806
public ~JByteBufferMutableBuffer() override;

0 commit comments

Comments
 (0)