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}