Skip to content

Expose a way to back an ArrayBuffer/SharedArrayBuffer with embedder-owned memory #5447

Description

@Shine-neko

Feature request: expose a way to back an ArrayBuffer/SharedArrayBuffer with embedder-owned memory

Summary

When embedding Boa, there is currently no public API to create an ArrayBuffer (or SharedArrayBuffer) whose bytes are owned by the
embedder
rather than allocated by Boa. Every construction path (JsArrayBuffer::from_byte_block, JsArrayBuffer::new,
JsSharedArrayBuffer::new) allocates a fresh, Boa-owned backing store and copies into it.

This makes it impossible to share a byte region between JavaScript and native host code with zero copies — the JS side and the native side each hold their own allocation, so writes on one side are not visible on the other.

Motivation / use case

I'm embedding Boa as the engine of a small JS runtime. A common need is to hand JavaScript a live view over memory that lives outside the engine. For example
a WebAssembly linear memory, an mmap'd file, or a GPU-mapped buffer. The
canonical shape in other engines is V8's
ArrayBuffer::New(data_ptr, byte_length, ...) / backing-store API.

Concretely: to run napi-rs WebAssembly addons (@napi-rs/wasm-runtime + @emnapi/core), the JS runtime must expose WebAssembly.Memory.prototype.buffer as an ArrayBuffer whose bytes are the wasm linear memory. emnapi does
new Uint8Array(wasmMemory.buffer).set(bytes, ptr) to pass data into wasm, and relies on those writes being visible to the running module. With a Boa-owned, copied buffer they are not, so the addon silently fails to receive its inputs.

What already exists and almost works

Boa is actually very close:

  • JsArrayBuffer::data_mut() hands out a stable &mut [u8] over the internal AlignedVec. I verified the base pointer is stable across re-borrows, and that a raw write through that pointer is observable from the JS side and vice-versa so the aliasing model itself works.
  • SharedArrayBuffer already stores its bytes in Arc<Inner> with an AlignedBox<[AtomicU8]> backing and an internal as_ptr().

The only missing piece is a public way to either:

  1. construct an ArrayBuffer/SharedArrayBuffer from an embedder-supplied backing (a pointer + length, or a type implementing a
    small BackingStore trait), the embedder guaranteeing the region outlives the buffer; or
  2. failing that, make SharedArrayBuffer::as_ptr() / bytes() (currently pub(crate)) public, so an embedder can at least read/write the existing backing in place and mirror it though (1) is what actually enables true zero-copy sharing.

Proposed API sketch (illustrative, not prescriptive)

// On the JS wrapper:
impl JsArrayBuffer {
    /// Create an ArrayBuffer that aliases `len` bytes at `ptr`.
    ///
    /// # Safety
    /// The caller must keep the region valid and unmoved for the buffer's
    /// lifetime, and must not alias it mutably from Rust while JS may run.
    pub unsafe fn from_external_ptr(
        ptr: *mut u8,
        len: usize,
        context: &mut Context,
    ) -> JsResult<Self>;
}

An Arc-based BackingStore handle (à la V8) would be even nicer for lifetime management, but a documented unsafe pointer constructor would already unblock the use case.

Environment

  • boa_engine from git+https://github.com/boa-dev/boa.git?branch=main
    at commit 5718cc8.

Notes

Happy to prototype a PR if the maintainers agree on the shape. The main design questions are lifetime/ownership (raw pointer + unsafe vs. an Arc backing handle) and whether detach/resize should be disallowed for externally-backed buffers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions