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.ptr.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 extern "C" fn free_shared(rt: *mut qjs::JSRuntime, opaque: *mut c_void, _ptr: *mut c_void) {
90 // `opaque` is guaranteed non-null: the only QuickJS code path
91 // that loses it is `.transfer()`, which is blocked by the
92 // immutability flag we set below.
93 unsafe {
94 let boxed = Box::from_raw(opaque as *mut qjs::JSValue);
95 qjs::JS_FreeValueRT(rt, *boxed);
96 }
97 }
98
99 let view = unsafe {
100 let val = qjs::JS_NewArrayBuffer(
101 ctx_ptr,
102 ptr,
103 len as _,
104 Some(free_shared),
105 opaque,
106 /*is_shared=*/ false,
107 );
108 if qjs::JS_IsException(val) {
109 // QuickJS didn't take ownership of `opaque`; drop it ourselves.
110 let boxed = Box::from_raw(opaque as *mut qjs::JSValue);
111 qjs::JS_FreeValueRT(rt, *boxed);
112 return Err(ctx.throw(ctx.catch()));
113 }
114 let value = Value::from_raw(ctx.clone(), val);
115 ArrayBuffer::from_value(value)
116 .ok_or_else(|| Exception::throw_type(ctx, "expected ArrayBuffer"))?
117 };
118
119 set_immutable(&view);
120 Ok(view)
121}