Skip to main content

ArrayBuffers (ArrayBuffer)

Array Buffers allow highly efficient access to shared raw binary data from both JS and native. Passing an ArrayBuffer between JS and native is zero-copy.

import { type HybridObject, iOS } from 'nitro-core'

@iOS(...)
export declare class Image extends HybridObject {
getData(): ArrayBuffer
}
note

The ArrayBuffer type is built-in in JavaScript.

ArrayBuffer is a value type in C++ and Swift. Copying the handle shares its backing storage; use ArrayBuffer.copy(...) to copy the bytes. In C++, access it with buffer.data() and buffer.size().

Ownership​

There's two kinds of ArrayBuffers, owning and non-owning:

Owning​

An ArrayBuffer that was created on the native side is owning (isOwner = true), which means you can safely access its data as long as the ArrayBuffer reference is alive. It can be safely held strong for longer, e.g. as a class property/member, and accessed from different Threads.

func doSomething() -> ArrayBuffer {
let buffer = ArrayBuffer.allocate(1024 * 10)
print(buffer.isOwner) // <-- ✅ true
let data = buffer.data // <-- ✅ safe to do because we own it!
self.buffer = buffer // <-- ✅ safe to use it later!
DispatchQueue.global().async {
let data = buffer.data // <-- ✅ also safe because we own it!
}
return buffer
}

Non-owning​

A buffer received from JS may borrow storage owned by the JS runtime (isOwner = false). Access its bytes only during the synchronous native call on that runtime's thread. Before retaining it or passing it to another thread, call asOwning() while the JS storage is still valid.

func doSomething(buffer: ArrayBuffer) {
print(buffer.isOwner) // <-- ❌ false
let data = buffer.data // <-- ✅ safe to do because we're still sync
DispatchQueue.global().async {
let data = buffer.data // <-- ❌ NOT safe
}
}

If you need a non-owning buffer's data for longer, copy it first:

func doSomething(buffer: ArrayBuffer) {
let copy = buffer.isOwner
? buffer
: ArrayBuffer.copy(of: buffer)
let data = copy.data // <-- ✅ safe now because we have an owning copy
DispatchQueue.global().async {
let data = copy.data // <-- ✅ still safe now because we have an owning copy
}
}
note

Not every ArrayBuffer received from JS is non-owning, eg if the buffer was created in native and then did a JS-roundtrip it is still owning!

Always check the isOwner property to prevent unnecessary copies.

Swift's data property is optional, including for empty buffers. Prefer withUnsafeBytes or withUnsafeMutableBytes for scoped access, and do not let their pointers escape:

let buffer = ArrayBuffer.allocate(4096)
buffer.withUnsafeMutableBytes { bytes in
bytes[0] = 42
}

Threading​

An ArrayBuffer can be accessed from both JS and native, and even from multiple Threads at once, but they are not thread-safe. To prevent race conditions or garbage-data from being read, make sure to not read from- and write to- the ArrayBuffer at the same time.

Creating Buffers​

Buffers can either be created from native (owning), or from JS (non-owning).

From native​

On the native side, an owning ArrayBuffer can either wrap-, or copy- an existing buffer:

let myData = UnsafeMutablePointer<UInt8>.allocate(capacity: 4096)

// wrap (no copy)
let wrappingArrayBuffer = ArrayBuffer.wrap(dataWithoutCopy: myData,
size: 4096,
onDelete: { myData.deallocate() })
// copy
let copiedArrayBuffer = ArrayBuffer.copy(wrappingArrayBuffer)
// new blank buffer
let newArrayBuffer = ArrayBuffer.allocate(4096)

Language-native buffer types​

ArrayBuffers also provide helper and conversion methods for the language-native conventional buffer types:

Swift often uses Data to represent Data.

let data = Data(capacity: 1024)
let buffer = try ArrayBuffer.copy(data: data)
let dataAgain = buffer.toData(copyIfNeeded: true)

From JS​

From JS, a non-owning ArrayBuffer can be created via the ArrayBuffer web APIs, and viewed or edited using the typed array APIs (e.g. Uint8Array).

const arrayBuffer = new ArrayBuffer(4096)
const view = new Uint8Array(arrayBuffer)
view[0] = 64
view[1] = 128
view[2] = 255

Creating a native buffer from JS​

To create an owning ArrayBuffer from JS, you can use the createNativeArrayBuffer(size) helper:

const arrayBuffer = NitroModules.createNativeArrayBuffer(4096)

In contrast to using the default JS ArrayBuffer constructor, this allocates a native ArrayBuffer using Nitro's implementation, allowing you to use this buffer in native, without performing any copies as it already is owning.