Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/react-native/React/Base/RCTArrayBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 13 additions & 21 deletions packages/react-native/React/Base/RCTArrayBuffer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> _copiedBytes;
}

@synthesize mutableBytes = _bytes;
@synthesize length = _length;
@synthesize owningBytes = _owningBytes;

#pragma mark - Initializers

- (instancetype)initWithBytesNoCopy:(void *)bytes
Expand All @@ -38,12 +39,17 @@ - (instancetype)initWithBytesNoCopy:(void *)bytes
cleanup:(void (^)(void))cleanup
{
if (bytes == NULL && length != 0) {
// The caller handed ownership of `bytes` to `cleanup`; nothing else would release them.
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 `length` is 0.
_bytes = length == 0 ? NULL : bytes;
_length = length;
_owningBytes = owningBytes;
_cleanup = [cleanup copy];
Expand All @@ -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;
Expand Down Expand Up @@ -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 = %@>",
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
#include "JArrayBuffer.h"

#include <cstring>
#include <span>
#include <stdexcept>
#include <utility>
#include <vector>

#include <react/bridging/ArrayBuffer.h>

Expand Down Expand Up @@ -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<jsi::MutableBuffer>& JArrayBuffer::mutableBuffer() const {
if (!hasBytes()) {
throw std::runtime_error(kRevokedBorrowMessage);
}
return buffer_;
}

jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::create(
jni::local_ref<jni::JByteBuffer> byteBuffer,
std::shared_ptr<jsi::MutableBuffer> buffer,
Expand All @@ -72,22 +57,13 @@ jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::create(
return javaPart;
}

jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwning(
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createWithOwnedBytes(
std::shared_ptr<jsi::MutableBuffer> buffer) {
auto byteBuffer = jni::JByteBuffer::wrapBytes(buffer->data(), buffer->size());
return create(std::move(byteBuffer), std::move(buffer), true);
}

jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createUnowned(
void* bytes,
size_t size) {
auto byteBuffer =
jni::JByteBuffer::wrapBytes(static_cast<uint8_t*>(bytes), size);
auto buffer = std::make_shared<JByteBufferMutableBuffer>(byteBuffer);
return create(std::move(byteBuffer), std::move(buffer), false);
}

jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwned(
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createWithCopiedBytes(
const void* bytes,
size_t size) {
auto byteBuffer = jni::JByteBuffer::allocateDirect(static_cast<jint>(size));
Expand All @@ -100,6 +76,15 @@ jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwned(
return create(std::move(byteBuffer), std::move(buffer), true);
}

jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createWithUnownedBytes(
void* bytes,
size_t size) {
auto byteBuffer =
jni::JByteBuffer::wrapBytes(static_cast<uint8_t*>(bytes), size);
auto buffer = std::make_shared<JByteBufferMutableBuffer>(byteBuffer);
return create(std::move(byteBuffer), std::move(buffer), false);
}

std::shared_ptr<jsi::MutableBuffer> JArrayBuffer::toJSBuffer(
jsi::Runtime& runtime,
jni::alias_ref<javaobject> arrayBuffer) {
Expand All @@ -115,15 +100,26 @@ std::shared_ptr<jsi::MutableBuffer> 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<uint8_t>(buffer->data(), buffer->size());
return std::make_shared<detail::OwnedBytesBuffer>(
std::vector<uint8_t>(bytes.begin(), bytes.end()));
return detail::copyToOwnedBuffer(buffer->data(), buffer->size());
}

const std::shared_ptr<jsi::MutableBuffer>& JArrayBuffer::mutableBuffer() const {
if (!hasBytes()) {
throw std::runtime_error(kRevokedBorrowMessage);
}
return buffer_;
}

void JArrayBuffer::invalidate() noexcept {
if (!owningBytes_) {
buffer_.reset();
}
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,23 @@ class JArrayBuffer : public jni::HybridClass<JArrayBuffer> {

// JS ArrayBuffer with a native MutableBuffer (tryGetMutableBuffer). Retain
// the owner so the bytes stay valid after the call.
static jni::local_ref<javaobject> createOwning(std::shared_ptr<jsi::MutableBuffer> buffer);

// JS-heap bytes passed to a synchronous call. Zero-copy for the call only;
// do not retain the result.
static jni::local_ref<javaobject> createUnowned(void *bytes, size_t size);
static jni::local_ref<javaobject> createWithOwnedBytes(std::shared_ptr<jsi::MutableBuffer> 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<javaobject> createOwned(const void *bytes, size_t size);
static jni::local_ref<javaobject> 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<javaobject> 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
// backing store. Raises a jsi::JSError if the buffer has no native peer or
// its borrow has been revoked.
static std::shared_ptr<jsi::MutableBuffer> toJSBuffer(jsi::Runtime &runtime, jni::alias_ref<javaobject> 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<jsi::MutableBuffer> &mutableBuffer() const;
Expand All @@ -66,6 +61,12 @@ class JArrayBuffer : public jni::HybridClass<JArrayBuffer> {
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<jsi::MutableBuffer> buffer, bool owningBytes) noexcept
: buffer_(std::move(buffer)), owningBytes_(owningBytes)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

#include <react/bridging/ArrayBuffer.h>
#include <react/jni/JArrayBuffer.h>
#include <react/jni/JByteBufferMutableBuffer.h>

Expand All @@ -19,22 +20,9 @@ namespace facebook::react {

namespace {

class TestBuffer final : public jsi::MutableBuffer {
public:
explicit TestBuffer(std::vector<uint8_t> bytes) noexcept
: bytes_(std::move(bytes)) {}

size_t size() const override {
return bytes_.size();
}

uint8_t* data() override {
return bytes_.data();
}

private:
std::vector<uint8_t> bytes_;
};
std::shared_ptr<jsi::MutableBuffer> makeBuffer(std::vector<uint8_t> bytes) {
return std::make_shared<detail::OwnedBytesBuffer>(std::move(bytes));
}

} // namespace

Expand All @@ -47,15 +35,15 @@ class TestBuffer final : public jsi::MutableBuffer {
*/

TEST(JArrayBufferTest, owningBufferExposesItsBytes) {
auto buffer = std::make_shared<TestBuffer>(std::vector<uint8_t>{1, 2, 3});
auto buffer = makeBuffer({1, 2, 3});
JArrayBuffer arrayBuffer{buffer, true};

EXPECT_TRUE(arrayBuffer.isOwningBytes());
EXPECT_EQ(arrayBuffer.mutableBuffer(), buffer);
}

TEST(JArrayBufferTest, borrowedBufferExposesItsBytesBeforeInvalidation) {
auto buffer = std::make_shared<TestBuffer>(std::vector<uint8_t>{1, 2, 3});
auto buffer = makeBuffer({1, 2, 3});
JArrayBuffer arrayBuffer{buffer, false};

EXPECT_FALSE(arrayBuffer.isOwningBytes());
Expand All @@ -69,7 +57,7 @@ TEST(JArrayBufferTest, borrowedBufferExposesItsBytesBeforeInvalidation) {
* occupies that memory.
*/
TEST(JArrayBufferTest, invalidateRevokesABorrow) {
auto buffer = std::make_shared<TestBuffer>(std::vector<uint8_t>{1, 2, 3});
auto buffer = makeBuffer({1, 2, 3});
JArrayBuffer arrayBuffer{buffer, false};

arrayBuffer.invalidate();
Expand All @@ -78,7 +66,7 @@ TEST(JArrayBufferTest, invalidateRevokesABorrow) {
}

TEST(JArrayBufferTest, invalidateLeavesAnOwningBufferUsable) {
auto buffer = std::make_shared<TestBuffer>(std::vector<uint8_t>{1, 2, 3});
auto buffer = makeBuffer({1, 2, 3});
JArrayBuffer arrayBuffer{buffer, true};

arrayBuffer.invalidate();
Expand All @@ -87,7 +75,7 @@ TEST(JArrayBufferTest, invalidateLeavesAnOwningBufferUsable) {
}

TEST(JArrayBufferTest, invalidateIsIdempotent) {
auto buffer = std::make_shared<TestBuffer>(std::vector<uint8_t>{1, 2, 3});
auto buffer = makeBuffer({1, 2, 3});
JArrayBuffer arrayBuffer{buffer, false};

arrayBuffer.invalidate();
Expand All @@ -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<TestBuffer>(std::vector<uint8_t>{1, 2, 3});
auto buffer = makeBuffer({1, 2, 3});
std::weak_ptr<jsi::MutableBuffer> weakBuffer = buffer;
JArrayBuffer arrayBuffer{std::move(buffer), false};

Expand Down
51 changes: 51 additions & 0 deletions packages/react-native/ReactCommon/react/bridging/ArrayBuffer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 <react/bridging/ArrayBuffer.h>

#include <atomic>
#include <span>
#include <string>

namespace facebook::react::detail {

void throwIfDetached(
jsi::Runtime& rt,
const jsi::ArrayBuffer& buffer,
const char* callerName) {
// Process-wide rather than per-runtime: every runtime in a process shares one
// engine build, so one rejection means none of them support the check.
static std::atomic<bool> 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<jsi::MutableBuffer> copyToOwnedBuffer(
const uint8_t* bytes,
size_t size) {
if (size == 0) {
return std::make_shared<OwnedBytesBuffer>(std::vector<uint8_t>{});
}
auto span = std::span<const uint8_t>(bytes, size);
return std::make_shared<OwnedBytesBuffer>(
std::vector<uint8_t>(span.begin(), span.end()));
}

} // namespace facebook::react::detail
Loading
Loading