Skip to main content

ferrijs_std/utils/
array_buffer.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3//! Zero-copy `ArrayBuffer` helpers built on QuickJS-NG primitives.
4//!
5//! `rquickjs` doesn't yet ship safe wrappers for QuickJS-NG's
6//! [immutable ArrayBuffer](https://tc39.es/proposal-immutable-arraybuffer/)
7//! support, so we go through `rquickjs::qjs::*` directly. The two
8//! capabilities exposed here are:
9//!
10//! * [`shared_array_buffer_view`] — create a fresh `ArrayBuffer` that
11//!   borrows the bytes of an existing one (no memcpy), kept alive via a
12//!   dup'd `JSValue` reference. The view is marked immutable, which is
13//!   both a correctness guarantee (consumer mutations can't leak into the
14//!   source) and a hard safety rail (the only QuickJS code path that
15//!   would lose our refcount handle is `.transfer()`, which immutability
16//!   blocks at the JS layer).
17//! * [`set_immutable`] — flip the immutable flag on an existing
18//!   `ArrayBuffer` (used when the source is a freshly-allocated buffer
19//!   we own and want to seal before handing out).
20//!
21//! These are used by `Blob.stream()` / `Blob.slice()` and by fetch's
22//! `Response.body` / `Request.body` getters to hand out aliased,
23//! transfer-safe views into producer-owned storage.
24
25use std::ffi::c_void;
26
27use rquickjs::{qjs, ArrayBuffer, Ctx, Exception, Result, Value};
28
29/// Mark an `ArrayBuffer` as immutable: subsequent writes through any
30/// `Uint8Array` / `DataView` view silently fail (or `TypeError` in strict
31/// mode), and `.transfer()` throws `TypeError: ArrayBuffer is immutable`.
32///
33/// Calling this on an already-immutable buffer is a no-op. Calling it on
34/// a detached buffer is a no-op (QuickJS returns -1 internally). The flag
35/// is checked at write/transfer time, not at create time, so the buffer
36/// can be initialised with bytes before being sealed.
37pub fn set_immutable(ab: &ArrayBuffer<'_>) {
38    // Safety: the JSValue is owned by `ab`; we only flip a boolean flag
39    // on the underlying `JSArrayBuffer` struct.
40    unsafe {
41        qjs::JS_SetImmutableArrayBuffer(ab.as_value().as_raw(), true);
42    }
43}
44
45/// Create a fresh, **immutable** `ArrayBuffer` that shares storage with
46/// `source` at `[offset..offset+len]` without copying any bytes. The
47/// returned buffer holds a dup'd reference to the source's `JSValue`, so
48/// the backing allocation stays alive exactly as long as any view (or
49/// transferred descendant of it) is reachable.
50///
51/// Immutability is what makes this sound:
52///
53///   * Writes through `Uint8Array` / `DataView` views silently no-op
54///     (strict mode: `TypeError`) — aliased consumers can't corrupt the
55///     source.
56///   * `buffer.transfer()` throws `TypeError: ArrayBuffer is immutable`
57///     — so a consumer can't detach the view and drop the `opaque`
58///     pointer that keeps the source alive. Without this guard the
59///     `free_func` would later fire with `opaque=NULL` (QuickJS strips
60///     `opaque` on transfer; see `js_array_buffer_constructor3`) and
61///     panic in `Box::from_raw(null)`. Because immutability blocks
62///     transfer at the JS layer, that path is unreachable.
63///
64/// If a future caller wants a *mutable* shared view, they need a
65/// different cleanup strategy (ptr-keyed side table, upstream QuickJS
66/// patch, or accepting a per-transfer leak).
67pub fn shared_array_buffer_view<'js>(
68    ctx: &Ctx<'js>,
69    source: &ArrayBuffer<'js>,
70    offset: usize,
71    len: usize,
72) -> Result<ArrayBuffer<'js>> {
73    let raw = source
74        .as_raw()
75        .ok_or_else(|| Exception::throw_type(ctx, "cannot view a detached ArrayBuffer"))?;
76    debug_assert!(
77        offset.checked_add(len).is_some_and(|e| e <= raw.len()),
78        "shared_array_buffer_view: slice out of range"
79    );
80    let ptr = unsafe { raw.cast::<u8>().as_ptr().add(offset) };
81
82    // Dup the source's JSValue. The returned ArrayBuffer's free-callback
83    // (below) will drop this reference.
84    let ctx_ptr = ctx.as_raw().as_ptr();
85    let rt = unsafe { qjs::JS_GetRuntime(ctx_ptr) };
86    let source_val = unsafe { qjs::JS_DupValueRT(rt, source.as_value().as_raw()) };
87    let opaque = Box::into_raw(Box::new(source_val)) as *mut c_void;
88
89    // QuickJS manages this block through one realloc-shaped callback:
90    // `size == 0` means free and return null, anything else is a resize
91    // request. The buffer is created non-resizable (`max_len` 0) and
92    // immutable, so only the free case can arrive; a resize is refused
93    // by returning null, which QuickJS reads as "cannot", leaving the
94    // block valid at its current size.
95    unsafe extern "C" fn realloc_shared(
96        rt: *mut qjs::JSRuntime,
97        opaque: *mut c_void,
98        _ptr: *mut c_void,
99        size: qjs::size_t,
100    ) -> *mut c_void {
101        if size != 0 {
102            return std::ptr::null_mut();
103        }
104        // `opaque` is guaranteed non-null: the only QuickJS code path
105        // that loses it is `.transfer()`, which is blocked by the
106        // immutability flag we set below.
107        unsafe {
108            let boxed = Box::from_raw(opaque as *mut qjs::JSValue);
109            qjs::JS_FreeValueRT(rt, *boxed);
110        }
111        std::ptr::null_mut()
112    }
113
114    let view = unsafe {
115        let val = qjs::JS_NewArrayBuffer(
116            ctx_ptr,
117            ptr,
118            len as _,
119            /*max_len=*/ 0,
120            Some(realloc_shared),
121            opaque,
122            /*is_shared=*/ false,
123        );
124        if qjs::JS_IsException(val) {
125            // QuickJS didn't take ownership of `opaque`; drop it ourselves.
126            let boxed = Box::from_raw(opaque as *mut qjs::JSValue);
127            qjs::JS_FreeValueRT(rt, *boxed);
128            return Err(ctx.throw(ctx.catch()));
129        }
130        let value = Value::from_raw(ctx.clone(), val);
131        ArrayBuffer::from_value(value)
132            .ok_or_else(|| Exception::throw_type(ctx, "expected ArrayBuffer"))?
133    };
134
135    set_immutable(&view);
136    Ok(view)
137}