diff --git a/core/engine/src/builtins/array_buffer/mod.rs b/core/engine/src/builtins/array_buffer/mod.rs index 5b8c5a1bfa5..cc968f024b4 100644 --- a/core/engine/src/builtins/array_buffer/mod.rs +++ b/core/engine/src/builtins/array_buffer/mod.rs @@ -16,9 +16,13 @@ pub(crate) mod utils; #[cfg(test)] mod tests; -use std::ops::{Deref, DerefMut}; +use std::{ + ops::{Deref, DerefMut}, + slice, +}; use aligned_vec::{ABox, AVec, ConstAlign}; +pub use portable_atomic::AtomicU8; pub use shared::SharedArrayBuffer; use std::sync::atomic::Ordering; @@ -36,7 +40,7 @@ use crate::{ }; use boa_gc::{Finalize, GcRef, GcRefMut, Trace}; -use self::utils::{SliceRef, SliceRefMut}; +use self::utils::{SliceRef, SliceRefMut, memcpy}; use super::{ Array, BuiltInBuilder, BuiltInConstructor, DataView, IntrinsicObject, typed_array::TypedArray, @@ -46,6 +50,32 @@ use super::{ pub type AlignedVec = AVec>; pub(crate) type AlignedBox = ABox>; +/// Minimum alignment required for a region of embedder-owned memory backing an +/// [`ArrayBuffer`] or a [`SharedArrayBuffer`]. +/// +/// Typed array views (`Float64Array`, `BigInt64Array`, ...) and the batched copy +/// routines perform aligned accesses of up to 8 bytes on the backing memory, so the +/// base address of an external region must satisfy the largest of those alignments. +pub(crate) const EXTERNAL_MEMORY_ALIGNMENT: usize = 8; + +/// Asserts that `data` is properly aligned to back an `ArrayBuffer` or a +/// `SharedArrayBuffer`. +/// +/// # Panics +/// +/// Panics if the region is non-empty and its base address is not aligned to +/// [`EXTERNAL_MEMORY_ALIGNMENT`] bytes. +pub(crate) fn assert_external_memory_alignment(data: &[AtomicU8]) { + assert!( + data.is_empty() + || data + .as_ptr() + .addr() + .is_multiple_of(EXTERNAL_MEMORY_ALIGNMENT), + "external buffer memory must be aligned to {EXTERNAL_MEMORY_ALIGNMENT} bytes", + ); +} + #[derive(Debug, Clone, Copy)] pub(crate) enum BufferRef { Buffer(B), @@ -60,7 +90,7 @@ where /// Gets the inner data of the buffer. pub(crate) fn bytes(&self, ordering: Ordering) -> Option> { match self { - Self::Buffer(buf) => buf.deref().bytes().map(SliceRef::Slice), + Self::Buffer(buf) => buf.deref().slice_ref(), Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().bytes(ordering))), } } @@ -72,7 +102,7 @@ where #[track_caller] pub(crate) fn bytes_with_len(&self, len: usize) -> Option> { match self { - Self::Buffer(buf) => buf.deref().bytes_with_len(len).map(SliceRef::Slice), + Self::Buffer(buf) => buf.deref().slice_ref_with_len(len), Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().bytes_with_len(len))), } } @@ -98,7 +128,7 @@ where { pub(crate) fn bytes(&mut self, ordering: Ordering) -> Option> { match self { - Self::Buffer(buf) => buf.deref_mut().bytes_mut().map(SliceRefMut::Slice), + Self::Buffer(buf) => buf.deref_mut().slice_ref_mut(), Self::SharedBuffer(buf) => { Some(SliceRefMut::AtomicSlice(buf.deref_mut().bytes(ordering))) } @@ -111,10 +141,7 @@ where /// the allocated buffer. pub(crate) fn bytes_with_len(&mut self, len: usize) -> Option> { match self { - Self::Buffer(buf) => buf - .deref_mut() - .bytes_with_len_mut(len) - .map(SliceRefMut::Slice), + Self::Buffer(buf) => buf.deref_mut().slice_ref_with_len_mut(len), Self::SharedBuffer(buf) => Some(SliceRefMut::AtomicSlice( buf.deref_mut().bytes_with_len(len), )), @@ -197,12 +224,65 @@ impl BufferObject { } } +/// The backing memory of an [`ArrayBuffer`]. +#[derive(Debug)] +pub(crate) enum BufferData { + /// Memory allocated and owned by Boa. + Owned(AlignedVec), + /// A region of embedder-owned memory. See [`ArrayBuffer::from_external_data`]. + /// + /// The engine only ever accesses this region through the atomic operations of + /// [`SliceRef::AtomicSlice`]/[`SliceRefMut::AtomicSlice`], and never materializes + /// `&[u8]` or `&mut [u8]` references into memory it does not own. This makes it + /// sound for the embedder to concurrently access the region from the same thread, + /// even while the engine holds a reference into the buffer. + External(&'static [AtomicU8]), +} + +impl Clone for BufferData { + fn clone(&self) -> Self { + match self { + Self::Owned(vec) => Self::Owned(vec.clone()), + // Deep-copy external regions into a Boa-owned allocation so that a cloned + // buffer is always independent of the original one; a bitwise clone would + // make two `ArrayBuffer`s alias the same external region. + Self::External(data) => Self::Owned(AlignedVec::from_iter( + 0, + SliceRef::AtomicSlice(data).to_vec(), + )), + } + } +} + +impl BufferData { + fn slice_ref(&self) -> SliceRef<'_> { + match self { + Self::Owned(vec) => SliceRef::Slice(vec), + Self::External(data) => SliceRef::AtomicSlice(data), + } + } + + fn slice_ref_mut(&mut self) -> SliceRefMut<'_> { + match self { + Self::Owned(vec) => SliceRefMut::Slice(vec), + Self::External(data) => SliceRefMut::AtomicSlice(data), + } + } + + fn len(&self) -> usize { + match self { + Self::Owned(vec) => vec.len(), + Self::External(data) => data.len(), + } + } +} + /// The internal representation of an `ArrayBuffer` object. #[derive(Debug, Clone, Trace, Finalize, JsData)] pub struct ArrayBuffer { /// The `[[ArrayBufferData]]` internal slot. #[unsafe_ignore_trace] - data: Option>, + data: Option, /// The `[[ArrayBufferMaxByteLength]]` internal slot. max_byte_len: Option, @@ -214,26 +294,135 @@ pub struct ArrayBuffer { impl ArrayBuffer { pub(crate) fn from_data(data: AlignedVec, detach_key: JsValue) -> Self { Self { - data: Some(data), + data: Some(BufferData::Owned(data)), max_byte_len: None, detach_key, } } + /// Creates a new `ArrayBuffer` over an embedder-owned memory region. + /// + /// The bytes of the buffer alias the provided region directly; writes done through + /// JavaScript are immediately visible to the embedder and vice versa. Boa never + /// allocates, grows nor frees the region, and only ever accesses it with atomic + /// operations, so the embedder can soundly access the region from the same thread + /// at any time — even while the engine holds a reference into the buffer. Accesses + /// from other threads must be synchronized with the JavaScript code that may access + /// the buffer (e.g. with a mutex or another happens-before relationship). + /// + /// Externally-backed buffers are always fixed-length and cannot be resized nor + /// transferred, but they can be detached; detaching is the way for an embedder to + /// guarantee that the engine can no longer access the region (e.g. before unmapping + /// or freeing it). See [`ArrayBuffer::detach`]. + /// + /// # Panics + /// + /// Panics if the region is non-empty and its base address is not aligned to 8 + /// bytes. Typed array views perform aligned accesses of up to 8 bytes on the + /// backing memory, so the base address must satisfy the largest alignment those + /// accesses need. + #[must_use] + pub fn from_external_data(data: &'static [AtomicU8]) -> Self { + assert_external_memory_alignment(data); + Self { + data: Some(BufferData::External(data)), + max_byte_len: None, + detach_key: JsValue::undefined(), + } + } + + /// Creates a new `ArrayBuffer` over the embedder-owned memory region starting at + /// `ptr` with `len` bytes. + /// + /// This is a convenience wrapper that builds the `&'static [AtomicU8]` slice from + /// its raw parts and delegates to [`ArrayBuffer::from_external_data`]; see that + /// method for the aliasing and threading guarantees of the returned buffer. + /// + /// # Safety + /// + /// The caller must guarantee that: + /// + /// - `ptr` is valid for reads and writes of `len` bytes, and the region stays + /// valid **and unmoved** at the same address for the whole lifetime of the + /// returned buffer and of every object that shares its data (e.g. typed arrays + /// or `DataView`s constructed over it). Note that regions that can relocate, + /// like a growable `WebAssembly` linear memory that moves its base address on + /// `memory.grow`, silently invalidate the buffer unless the embedder guarantees + /// that no relocation happens while the buffer is alive. + /// - The region is not written to non-atomically from another thread while + /// JavaScript code that may access the buffer is executing. + /// + /// # Panics + /// + /// Panics if `ptr` is null, if `len` is bigger than `isize::MAX`, or if the region + /// is non-empty and `ptr` is not aligned to 8 bytes. + #[must_use] + pub unsafe fn from_external_ptr(ptr: *mut u8, len: usize) -> Self { + assert!(!ptr.is_null(), "`ptr` must be non-null"); + assert!( + isize::try_from(len).is_ok(), + "`len` must not exceed `isize::MAX`" + ); + // SAFETY: `AtomicU8` is guaranteed to have the same layout as `u8`, and the + // caller guarantees that the region is valid for reads and writes of `len` + // bytes for the whole lifetime of the buffer. + let data = unsafe { slice::from_raw_parts(ptr.cast_const().cast::(), len) }; + Self::from_external_data(data) + } + + /// Returns `true` if this buffer is backed by embedder-owned memory. + #[must_use] + pub fn is_external(&self) -> bool { + matches!(self.data, Some(BufferData::External(_))) + } + pub(crate) fn len(&self) -> usize { - self.data.as_ref().map_or(0, AlignedVec::len) + self.data.as_ref().map_or(0, BufferData::len) } + /// Gets the bytes of the buffer if the buffer is backed by Boa-owned memory. + /// + /// Returns `None` if the buffer is detached or backed by embedder-owned memory; + /// use [`ArrayBuffer::slice_ref`] to access both kinds of backing memory. pub(crate) fn bytes(&self) -> Option<&[u8]> { - self.data.as_deref() + match self.data.as_ref() { + Some(BufferData::Owned(vec)) => Some(vec), + _ => None, + } } + /// Gets the mutable bytes of the buffer if the buffer is backed by Boa-owned memory. + /// + /// Returns `None` if the buffer is detached or backed by embedder-owned memory; + /// use [`ArrayBuffer::slice_ref_mut`] to access both kinds of backing memory. pub(crate) fn bytes_mut(&mut self) -> Option<&mut [u8]> { - self.data.as_deref_mut() + match self.data.as_mut() { + Some(BufferData::Owned(vec)) => Some(vec), + _ => None, + } + } + + /// Gets the inner data of the buffer, abstracting over owned and external backing + /// memory. + /// + /// Returns `None` if the buffer is detached. + pub(crate) fn slice_ref(&self) -> Option> { + self.data.as_ref().map(BufferData::slice_ref) + } + + /// Gets the mutable inner data of the buffer, abstracting over owned and external + /// backing memory. + /// + /// Returns `None` if the buffer is detached. + pub(crate) fn slice_ref_mut(&mut self) -> Option> { + self.data.as_mut().map(BufferData::slice_ref_mut) } pub(crate) fn vec_mut(&mut self) -> Option<&mut AlignedVec> { - self.data.as_mut() + match self.data.as_mut() { + Some(BufferData::Owned(vec)) => Some(vec), + _ => None, + } } /// Sets the maximum byte length of the buffer, returning the previous value if present. @@ -241,34 +430,45 @@ impl ArrayBuffer { self.max_byte_len.replace(max_byte_len) } - /// Gets the inner bytes of the buffer without accessing the current atomic length. + /// Gets the inner data of the buffer without accessing the current atomic length. #[track_caller] - pub(crate) fn bytes_with_len(&self, len: usize) -> Option<&[u8]> { - if let Some(s) = self.data.as_deref() { - Some(&s[..len]) - } else { - None - } + pub(crate) fn slice_ref_with_len(&self, len: usize) -> Option> { + self.slice_ref().map(|s| match s { + SliceRef::Slice(s) => SliceRef::Slice(&s[..len]), + SliceRef::AtomicSlice(s) => SliceRef::AtomicSlice(&s[..len]), + }) } - /// Gets the mutable inner bytes of the buffer without accessing the current atomic length. + /// Gets the mutable inner data of the buffer without accessing the current atomic length. #[track_caller] - pub(crate) fn bytes_with_len_mut(&mut self, len: usize) -> Option<&mut [u8]> { - if let Some(s) = self.data.as_deref_mut() { - Some(&mut s[..len]) - } else { - None - } + pub(crate) fn slice_ref_with_len_mut(&mut self, len: usize) -> Option> { + self.slice_ref_mut().map(|s| match s { + SliceRefMut::Slice(s) => SliceRefMut::Slice(&mut s[..len]), + SliceRefMut::AtomicSlice(s) => SliceRefMut::AtomicSlice(&s[..len]), + }) } - /// Gets the underlying vector for this buffer. + /// Gets the underlying bytes of this buffer if the buffer is backed by Boa-owned + /// memory. + /// + /// Returns `None` if the buffer is detached or backed by embedder-owned memory + /// (see [`ArrayBuffer::from_external_data`]); the embedder already owns an + /// externally-backed region and can read it directly. #[must_use] pub fn data(&self) -> Option<&[u8]> { - self.data.as_deref() + self.bytes() } /// Resizes the buffer to the new size, clamped to the maximum byte length if present. pub fn resize(&mut self, new_byte_length: u64) -> JsResult<()> { + if self.is_external() { + return Err(JsNativeError::typ() + .with_message( + "ArrayBuffer.resize: cannot resize a buffer backed by embedder-owned memory", + ) + .into()); + } + let Some(max_byte_len) = self.max_byte_len else { return Err(JsNativeError::typ() .with_message("ArrayBuffer.resize: cannot resize a fixed-length buffer") @@ -296,6 +496,12 @@ impl ArrayBuffer { /// Detaches the inner data of this `ArrayBuffer`, returning the original buffer if still /// present. /// + /// For a buffer backed by embedder-owned memory (see + /// [`ArrayBuffer::from_external_data`]), this returns a copy of the region's contents + /// and drops the engine's reference into the region; the embedder remains the owner + /// of the region itself. This is the way for an embedder to guarantee that the + /// engine can no longer access the region, e.g. before unmapping or freeing it. + /// /// # Errors /// /// Throws an error if the provided detach key is invalid. @@ -306,7 +512,12 @@ impl ArrayBuffer { .into()); } - Ok(self.data.take()) + Ok(self.data.take().map(|data| match data { + BufferData::Owned(vec) => vec, + BufferData::External(data) => { + AlignedVec::from_iter(0, SliceRef::AtomicSlice(data).to_vec()) + } + })) } /// `IsDetachedBuffer ( arrayBuffer )` @@ -533,16 +744,16 @@ impl ArrayBuffer { })?; // 4. If IsDetachedBuffer(O) is true, return +0𝔽. - let Some(data) = buf.bytes() else { + if buf.is_detached() { return Ok(JsValue::from(0)); - }; + } // 5. If IsFixedLengthArrayBuffer(O) is true, then // a. Let length be O.[[ArrayBufferByteLength]]. // 6. Else, // a. Let length be O.[[ArrayBufferMaxByteLength]]. // 7. Return 𝔽(length). - Ok(buf.max_byte_len.unwrap_or(data.len() as u64).into()) + Ok(buf.max_byte_len.unwrap_or(buf.len() as u64).into()) } /// [`get ArrayBuffer.prototype.resizable`][spec]. @@ -712,7 +923,7 @@ impl ArrayBuffer { // 19. If IsDetachedBuffer(new) is true, throw a TypeError exception. // 25. Let toBuf be new.[[ArrayBufferData]]. let mut new = new.borrow_mut(); - let Some(to_buf) = new.data_mut().bytes_mut() else { + let Some(mut to_buf) = new.data_mut().slice_ref_mut() else { return Err(JsNativeError::typ() .with_message("ArrayBuffer constructor returned detached ArrayBuffer") .into()); @@ -729,7 +940,7 @@ impl ArrayBuffer { // 23. If IsDetachedBuffer(O) is true, throw a TypeError exception. // 24. Let fromBuf be O.[[ArrayBufferData]]. let buf = buf.borrow(); - let Some(from_buf) = buf.data().bytes() else { + let Some(from_buf) = buf.data().slice_ref() else { return Err(JsNativeError::typ() .with_message("ArrayBuffer detached while ArrayBuffer.slice was running") .into()); @@ -738,7 +949,17 @@ impl ArrayBuffer { // 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen). let first = first as usize; let new_len = new_len as usize; - to_buf[..new_len].copy_from_slice(&from_buf[first..first + new_len]); + + // SAFETY: The bounds checks above guarantee that both buffers have at least + // `new_len` bytes at the given offsets, and `new` is a different buffer + // object than `buf`, so their owned allocations cannot overlap. + unsafe { + memcpy( + from_buf.subslice(first..first + new_len).as_ptr(), + to_buf.subslice_mut(..new_len).as_ptr(), + new_len, + ); + } } // 27. Return new. @@ -788,12 +1009,22 @@ impl ArrayBuffer { }; // 5. If IsDetachedBuffer(arrayBuffer) is true, throw a TypeError exception. - let Some(mut bytes) = buf.borrow_mut().data_mut().data.take() else { + let Some(data) = buf.borrow_mut().data_mut().data.take() else { return Err(JsNativeError::typ() .with_message("cannot transfer a detached buffer") .into()); }; + let mut bytes = match data { + BufferData::Owned(bytes) => bytes, + data @ BufferData::External(_) => { + buf.borrow_mut().data_mut().data = Some(data); + return Err(JsNativeError::typ() + .with_message("cannot transfer an ArrayBuffer backed by embedder-owned memory") + .into()); + } + }; + // 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer) // is true, then // a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]]. @@ -807,7 +1038,7 @@ impl ArrayBuffer { // 8. If arrayBuffer.[[ArrayBufferDetachKey]] is not undefined, throw a TypeError exception. if !buf.borrow().data().detach_key.is_undefined() { - buf.borrow_mut().data_mut().data = Some(bytes); + buf.borrow_mut().data_mut().data = Some(BufferData::Owned(bytes)); return Err(JsNativeError::typ() .with_message("cannot transfer a buffer with a detach key") .into()); @@ -827,7 +1058,7 @@ impl ArrayBuffer { // 16. Return newBuffer. if let Some(new_max_len) = new_max_len { if new_len > new_max_len { - buf.borrow_mut().data_mut().data = Some(bytes); + buf.borrow_mut().data_mut().data = Some(BufferData::Owned(bytes)); return Err(JsNativeError::range() .with_message("`length` cannot be bigger than `maxByteLength`") .into()); @@ -851,7 +1082,7 @@ impl ArrayBuffer { context.root_shape(), prototype, ArrayBuffer { - data: Some(bytes), + data: Some(BufferData::Owned(bytes)), max_byte_len: new_max_len, detach_key: JsValue::undefined(), }, @@ -904,7 +1135,7 @@ impl ArrayBuffer { Self { // 6. Set obj.[[ArrayBufferData]] to block. // 7. Set obj.[[ArrayBufferByteLength]] to byteLength. - data: Some(block), + data: Some(BufferData::Owned(block)), // 8. If allocatingResizableBuffer is true, then // c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength. max_byte_len, diff --git a/core/engine/src/builtins/array_buffer/shared.rs b/core/engine/src/builtins/array_buffer/shared.rs index 5c4b30b8482..b091e444e60 100644 --- a/core/engine/src/builtins/array_buffer/shared.rs +++ b/core/engine/src/builtins/array_buffer/shared.rs @@ -1,5 +1,5 @@ use std::{ - ptr, + ptr, slice, sync::{Arc, atomic::Ordering}, }; @@ -21,7 +21,7 @@ use crate::{ string::StaticJsStrings, }; -use super::{get_max_byte_len, utils::copy_shared_to_shared}; +use super::{assert_external_memory_alignment, get_max_byte_len, utils::copy_shared_to_shared}; /// The internal representation of a `SharedArrayBuffer` object. /// @@ -34,6 +34,29 @@ pub struct SharedArrayBuffer { data: Arc, } +/// The backing memory of a [`SharedArrayBuffer`]. +/// +/// Both variants hold slices of atomics, so `SharedData` is automatically `Send` and +/// `Sync`; the creator of an externally-backed `SharedArrayBuffer` guarantees that the +/// external region stays valid and unmoved for the whole lifetime of the buffer. +#[derive(Debug)] +enum SharedData { + /// Memory allocated and owned by Boa. + Owned(AlignedBox<[AtomicU8]>), + /// Memory owned by the embedder. See [`SharedArrayBuffer::from_external_data`]. + External(&'static [AtomicU8]), +} + +impl SharedData { + /// Gets the whole allocated region, disregarding the current atomic length. + fn full_buffer(&self) -> &[AtomicU8] { + match self { + Self::Owned(buffer) => buffer, + Self::External(buffer) => buffer, + } + } +} + #[derive(Debug)] struct Inner { // Technically we should have an `[[ArrayBufferData]]` internal slot, @@ -44,14 +67,14 @@ struct Inner { // The maximum buffer length is represented by `buffer.len()`, and `current_len` has the current // buffer length, or `None` if this is a fixed buffer; in this case, `buffer.len()` will be // the true length of the buffer. - buffer: AlignedBox<[AtomicU8]>, + buffer: SharedData, current_len: Option, } impl Default for Inner { fn default() -> Self { Self { - buffer: AlignedVec::new(0).into_boxed_slice(), + buffer: SharedData::Owned(AlignedVec::new(0).into_boxed_slice()), current_len: None, } } @@ -66,28 +89,103 @@ impl SharedArrayBuffer { } } + /// Creates a `SharedArrayBuffer` over an embedder-owned memory region. + /// + /// The bytes of the buffer alias the provided region directly; writes done through + /// JavaScript are immediately visible to the embedder and vice versa. Boa never + /// allocates, grows nor frees the region, and only ever accesses it with atomic + /// operations. Accesses to the region from other threads must be synchronized with + /// the JavaScript code that may access the buffer concurrently, exactly like for + /// any other `SharedArrayBuffer` memory. + /// + /// Externally-backed shared buffers are always fixed-length and cannot be grown. + /// + /// # Panics + /// + /// Panics if the region is non-empty and its base address is not aligned to 8 + /// bytes. `Atomics` and typed array views (`Int32Array`, `Float64Array`, + /// `BigInt64Array`, ...) perform aligned atomic accesses of up to 8 bytes on the + /// backing memory, so the base address must satisfy the largest alignment those + /// accesses need. + #[must_use] + pub fn from_external_data(data: &'static [AtomicU8]) -> Self { + assert_external_memory_alignment(data); + Self { + data: Arc::new(Inner { + buffer: SharedData::External(data), + current_len: None, + }), + } + } + + /// Creates a `SharedArrayBuffer` over the embedder-owned memory region starting at + /// `ptr` with `len` bytes. + /// + /// This is a convenience wrapper that builds the `&'static [AtomicU8]` slice from + /// its raw parts and delegates to [`SharedArrayBuffer::from_external_data`]; see + /// that method for the aliasing and threading guarantees of the returned buffer. + /// + /// # Safety + /// + /// The caller must guarantee that: + /// + /// - `ptr` is valid for reads and writes of `len` bytes, and the region stays + /// valid **and unmoved** at the same address for the whole lifetime of the + /// returned buffer and all of its clones (including clones sent to other + /// agents/threads). Note that regions that can relocate, like a growable + /// `WebAssembly` linear memory that moves its base address on `memory.grow`, + /// silently invalidate the buffer unless the embedder guarantees that no + /// relocation happens while the buffer is alive. + /// - All accesses to the region from outside the returned buffer are performed + /// with atomic operations, or are otherwise synchronized with any JavaScript + /// code that may access the buffer concurrently. + /// + /// # Panics + /// + /// Panics if `ptr` is null, if `len` is bigger than `isize::MAX`, or if the region + /// is non-empty and `ptr` is not aligned to 8 bytes. + #[must_use] + pub unsafe fn from_external_ptr(ptr: *mut u8, len: usize) -> Self { + assert!(!ptr.is_null(), "`ptr` must be non-null"); + assert!( + isize::try_from(len).is_ok(), + "`len` must not exceed `isize::MAX`" + ); + // SAFETY: `AtomicU8` is guaranteed to have the same layout as `u8`, and the + // caller guarantees that the region is valid for reads and writes of `len` + // bytes for the whole lifetime of the buffer and all of its clones. + let data = unsafe { slice::from_raw_parts(ptr.cast_const().cast::(), len) }; + Self::from_external_data(data) + } + + /// Returns `true` if this buffer is backed by embedder-owned memory. + #[must_use] + pub fn is_external(&self) -> bool { + matches!(self.data.buffer, SharedData::External(_)) + } + /// Gets the length of this `SharedArrayBuffer`. pub(crate) fn len(&self, ordering: Ordering) -> usize { - self.data - .current_len - .as_ref() - .map_or_else(|| self.data.buffer.len(), |len| len.load(ordering)) + self.data.current_len.as_ref().map_or_else( + || self.data.buffer.full_buffer().len(), + |len| len.load(ordering), + ) } /// Gets the inner bytes of this `SharedArrayBuffer`. pub(crate) fn bytes(&self, ordering: Ordering) -> &[AtomicU8] { - &self.data.buffer[..self.len(ordering)] + &self.data.buffer.full_buffer()[..self.len(ordering)] } /// Gets the inner data of the buffer without accessing the current atomic length. #[track_caller] pub(crate) fn bytes_with_len(&self, len: usize) -> &[AtomicU8] { - &self.data.buffer[..len] + &self.data.buffer.full_buffer()[..len] } /// Gets a pointer to the internal shared buffer. pub(crate) fn as_ptr(&self) -> *const AtomicU8 { - (*self.data.buffer).as_ptr() + self.data.buffer.full_buffer().as_ptr() } pub(crate) fn is_fixed_len(&self) -> bool { @@ -290,7 +388,7 @@ impl SharedArrayBuffer { // 5. Else, // a. Let length be O.[[ArrayBufferMaxByteLength]]. // 6. Return 𝔽(length). - Ok(buf.data.buffer.len().into()) + Ok(buf.data.buffer.full_buffer().len().into()) } /// [`SharedArrayBuffer.prototype.grow ( newLength )`][spec]. @@ -338,7 +436,7 @@ impl SharedArrayBuffer { // d. If newByteLength < currentByteLength or newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception. // Extracting this condition outside the CAS since throwing early doesn't affect the correct // behaviour of the loop. - if new_byte_len > buf.data.buffer.len() as u64 { + if new_byte_len > buf.data.buffer.full_buffer().len() as u64 { return Err(JsNativeError::range() .with_message( "SharedArrayBuffer.grow: new length cannot be bigger than `maxByteLength`", @@ -541,7 +639,7 @@ impl SharedArrayBuffer { prototype, Self { data: Arc::new(Inner { - buffer: block, + buffer: SharedData::Owned(block), current_len, }), }, diff --git a/core/engine/src/builtins/array_buffer/tests.rs b/core/engine/src/builtins/array_buffer/tests.rs index fa11ae1fc83..4ba1a632b46 100644 --- a/core/engine/src/builtins/array_buffer/tests.rs +++ b/core/engine/src/builtins/array_buffer/tests.rs @@ -1,3 +1,4 @@ +use super::AlignedVec; use crate::object::JsArrayBuffer; use crate::{TestAction, run_test_actions}; @@ -309,3 +310,233 @@ fn shared_array_buffer_slice_empty() { TestAction::assert("result.length === 0"), ]); } + +/// Tests that an externally-backed `ArrayBuffer` aliases the embedder's memory +/// with zero copies: JS writes are visible to the embedder and vice versa. +#[test] +fn external_array_buffer_zero_copy() { + use crate::{Context, JsValue, Source, js_string, property::Attribute}; + + // `AlignedVec` allocations satisfy the 8-byte alignment required for external regions. + let mut backing: AlignedVec = AlignedVec::from_iter(0, [0u8; 8]); + let context = &mut Context::default(); + + // SAFETY: `backing` stays alive and unmoved while `context` can reach the buffer. + let buffer = + unsafe { JsArrayBuffer::from_external_ptr(backing.as_mut_ptr(), backing.len(), context) }; + + assert!(buffer.is_external()); + assert_eq!(buffer.byte_length(), 8); + + context + .register_global_property(js_string!("buf"), buffer.clone(), Attribute::all()) + .unwrap(); + + // A write from the JS side must be visible through the embedder's memory. + context + .eval(Source::from_bytes("new Uint8Array(buf)[1] = 42;")) + .unwrap(); + assert_eq!(backing[1], 42); + + // A write from the embedder's side must be visible to JS. + backing[2] = 7; + let value = context + .eval(Source::from_bytes("new Uint8Array(buf)[2]")) + .unwrap(); + assert_eq!(value, JsValue::from(7)); + + // Direct slice access is not available for external buffers, but copying out is. + assert!(buffer.data().is_none()); + assert_eq!(buffer.to_vec().as_deref().map(|v| v[1]), Some(42)); +} + +/// Tests the safe `from_external_data` constructor over a static region of atomics, +/// including an 8-byte-wide view to exercise the aligned access paths. +#[test] +fn external_array_buffer_from_data() { + use crate::{Context, JsValue, Source, js_string, property::Attribute}; + use portable_atomic::AtomicU8; + use std::sync::atomic::Ordering; + + #[repr(align(8))] + struct Backing([AtomicU8; 16]); + static BACKING: Backing = Backing([const { AtomicU8::new(0) }; 16]); + + let context = &mut Context::default(); + let buffer = JsArrayBuffer::from_external_data(&BACKING.0, context); + + assert!(buffer.is_external()); + assert_eq!(buffer.byte_length(), 16); + + context + .register_global_property(js_string!("buf"), buffer, Attribute::all()) + .unwrap(); + + context + .eval(Source::from_bytes( + "new Float64Array(buf)[1] = 1.5; new Uint8Array(buf)[0] = 3;", + )) + .unwrap(); + + assert_eq!(BACKING.0[0].load(Ordering::Relaxed), 3); + let mut float_bytes = [0u8; 8]; + for (i, b) in float_bytes.iter_mut().enumerate() { + *b = BACKING.0[8 + i].load(Ordering::Relaxed); + } + assert_eq!(f64::from_ne_bytes(float_bytes).to_bits(), 1.5f64.to_bits()); + + let value = context + .eval(Source::from_bytes("new Float64Array(buf)[1]")) + .unwrap(); + assert_eq!(value, JsValue::from(1.5)); +} + +/// Tests that detaching an externally-backed `ArrayBuffer` releases the engine's +/// reference into the region and returns a copy of its contents, while resizing +/// and transferring still fail. +#[test] +fn external_array_buffer_detach_releases_region() { + use crate::{Context, JsValue}; + + let mut backing: AlignedVec = AlignedVec::from_iter(0, [1u8, 2, 3, 4, 5, 6, 7, 8]); + let context = &mut Context::default(); + + // SAFETY: `backing` stays alive and unmoved while `context` can reach the buffer. + let buffer = + unsafe { JsArrayBuffer::from_external_ptr(backing.as_mut_ptr(), backing.len(), context) }; + + // Resizing an externally-backed buffer must fail. + assert!(buffer.borrow_mut().data_mut().resize(4).is_err()); + assert_eq!(buffer.byte_length(), 8); + + // Detaching must succeed, returning a copy of the region's contents and dropping + // the engine's reference into the region. + let contents = buffer.detach(&JsValue::undefined()).unwrap(); + assert_eq!(contents.as_slice(), &[1u8, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(buffer.byte_length(), 0); + assert!(buffer.to_vec().is_none()); + + // The embedder still owns the region, which is untouched by the detach. + assert_eq!(&backing[..], &[1u8, 2, 3, 4, 5, 6, 7, 8]); +} + +/// Tests that `ArrayBuffer.prototype.slice` copies data out of an externally-backed +/// buffer into a new, Boa-owned buffer. +#[test] +fn external_array_buffer_slice() { + use crate::{Context, JsValue, Source, js_string, property::Attribute}; + + let mut backing: AlignedVec = AlignedVec::from_iter(0, [10u8, 11, 12, 13, 14, 15, 16, 17]); + let context = &mut Context::default(); + + // SAFETY: `backing` stays alive and unmoved while `context` can reach the buffer. + let buffer = + unsafe { JsArrayBuffer::from_external_ptr(backing.as_mut_ptr(), backing.len(), context) }; + + context + .register_global_property(js_string!("buf"), buffer, Attribute::all()) + .unwrap(); + + let value = context + .eval(Source::from_bytes( + "var sliced = new Uint8Array(buf.slice(2, 6)); sliced[0] + sliced[3]", + )) + .unwrap(); + assert_eq!(value, JsValue::from(12 + 15)); + + // The slice is an independent, Boa-owned buffer: writes to it must not be + // visible through the external region. + context.eval(Source::from_bytes("sliced[0] = 99;")).unwrap(); + assert_eq!(backing[2], 12); +} + +/// Tests that constructing an external buffer over a misaligned region panics. +#[test] +#[should_panic(expected = "external buffer memory must be aligned")] +fn external_array_buffer_misaligned_panics() { + use crate::Context; + + let mut backing: AlignedVec = AlignedVec::from_iter(0, [0u8; 8]); + let context = &mut Context::default(); + + // SAFETY: the pointer is valid for `len - 1` bytes; the constructor must panic + // before the buffer is ever used because the base address is misaligned. + let _buffer = unsafe { + JsArrayBuffer::from_external_ptr(backing.as_mut_ptr().add(1), backing.len() - 1, context) + }; +} + +/// Tests that an externally-backed `SharedArrayBuffer` aliases the embedder's +/// memory with zero copies. +#[test] +fn external_shared_array_buffer_zero_copy() { + use crate::{ + Context, JsValue, Source, js_string, object::builtins::JsSharedArrayBuffer, + property::Attribute, + }; + + let mut backing: AlignedVec = AlignedVec::from_iter(0, [0u8; 8]); + let context = &mut Context::default(); + + // SAFETY: `backing` stays alive and unmoved while `context` can reach the buffer. + let buffer = unsafe { + JsSharedArrayBuffer::from_external_ptr(backing.as_mut_ptr(), backing.len(), context) + }; + + assert!(buffer.is_external()); + assert_eq!(buffer.byte_length(), 8); + + context + .register_global_property(js_string!("sab"), buffer, Attribute::all()) + .unwrap(); + + // A write from the JS side must be visible through the embedder's memory. + context + .eval(Source::from_bytes("new Uint8Array(sab)[0] = 99;")) + .unwrap(); + assert_eq!(backing[0], 99); + + // A write from the embedder's side must be visible to JS. + backing[3] = 123; + let value = context + .eval(Source::from_bytes("new Uint8Array(sab)[3]")) + .unwrap(); + assert_eq!(value, JsValue::from(123)); + + // Externally-backed shared buffers are fixed-length. + let growable = context.eval(Source::from_bytes("sab.growable")).unwrap(); + assert_eq!(growable, JsValue::from(false)); +} + +/// Tests the safe `SharedArrayBuffer::from_external_data` constructor, including +/// `Atomics` operations over the external region. +#[test] +fn external_shared_array_buffer_from_data() { + use crate::{ + Context, JsValue, Source, js_string, object::builtins::JsSharedArrayBuffer, + property::Attribute, + }; + use portable_atomic::AtomicU8; + use std::sync::atomic::Ordering; + + #[repr(align(8))] + struct Backing([AtomicU8; 8]); + static BACKING: Backing = Backing([const { AtomicU8::new(0) }; 8]); + + let context = &mut Context::default(); + let buffer = JsSharedArrayBuffer::from_external_data(&BACKING.0, context); + + assert!(buffer.is_external()); + + context + .register_global_property(js_string!("sab"), buffer, Attribute::all()) + .unwrap(); + + let value = context + .eval(Source::from_bytes( + "var ta = new Int32Array(sab); Atomics.add(ta, 0, 7); Atomics.load(ta, 0)", + )) + .unwrap(); + assert_eq!(value, JsValue::from(7)); + assert_eq!(BACKING.0[0].load(Ordering::Relaxed), 7); +} diff --git a/core/engine/src/object/builtins/jsarraybuffer.rs b/core/engine/src/object/builtins/jsarraybuffer.rs index b377f684dee..3d983b53afd 100644 --- a/core/engine/src/object/builtins/jsarraybuffer.rs +++ b/core/engine/src/object/builtins/jsarraybuffer.rs @@ -11,7 +11,7 @@ use boa_gc::{Finalize, GcRef, GcRefMut, Trace}; use std::ops::Deref; #[doc(inline)] -pub use crate::builtins::array_buffer::AlignedVec; +pub use crate::builtins::array_buffer::{AlignedVec, AtomicU8}; /// `JsArrayBuffer` provides a wrapper for Boa's implementation of the ECMAScript `ArrayBuffer` object #[derive(Debug, Clone, Trace, Finalize)] @@ -132,6 +132,159 @@ impl JsArrayBuffer { Ok(Self { inner: obj }) } + /// Creates an `ArrayBuffer` that aliases a region of embedder-owned memory. + /// + /// Unlike [`JsArrayBuffer::from_byte_block`], this does **not** copy nor take + /// ownership of the memory: the bytes of the resulting `ArrayBuffer` are the + /// provided region itself. Writes performed by JavaScript code are immediately + /// visible to the embedder and vice versa, enabling zero-copy sharing of memory + /// regions like `WebAssembly` linear memories, memory-mapped files or GPU-mapped + /// buffers. + /// + /// The engine only ever accesses the region with atomic operations and never + /// creates `&[u8]`/`&mut [u8]` references into it, so the embedder can soundly + /// access the region from the same thread at any time, even while the engine + /// holds a reference into the buffer. Accesses from other threads must be + /// synchronized with the JavaScript code that may access the buffer. + /// + /// The resulting buffer is always fixed-length and cannot be resized nor + /// transferred; those operations throw a `TypeError`. It **can** be detached, + /// which is the way for an embedder to guarantee that the engine can no longer + /// access the region; see [`JsArrayBuffer::detach`]. + /// + /// Because the engine accesses external memory only through the slice's atomics, + /// [`JsArrayBuffer::data`] and [`JsArrayBuffer::data_mut`] return `None` for + /// externally-backed buffers; use [`JsArrayBuffer::to_vec`] to copy the contents + /// out, or read the region directly since the embedder owns it. + /// + /// # Example + /// + /// ``` + /// # use boa_engine::{ + /// # object::builtins::{AtomicU8, JsArrayBuffer}, + /// # property::Attribute, + /// # Context, JsResult, Source, js_string, + /// # }; + /// # use std::sync::atomic::Ordering; + /// # fn main() -> JsResult<()> { + /// # let context = &mut Context::default(); + /// // The backing region must be 8-byte aligned. + /// #[repr(align(8))] + /// struct Backing([AtomicU8; 4]); + /// static BACKING: Backing = Backing([const { AtomicU8::new(0) }; 4]); + /// + /// let array_buffer = JsArrayBuffer::from_external_data(&BACKING.0, context); + /// assert!(array_buffer.is_external()); + /// + /// context.register_global_property(js_string!("buf"), array_buffer, Attribute::all())?; + /// context.eval(Source::from_bytes("new Uint8Array(buf)[1] = 42;"))?; + /// + /// assert_eq!(BACKING.0[1].load(Ordering::Relaxed), 42); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Panics + /// + /// Panics if the region is non-empty and its base address is not aligned to 8 + /// bytes. Typed array views perform aligned accesses of up to 8 bytes on the + /// backing memory, so the base address must satisfy the largest alignment those + /// accesses need. + #[must_use] + pub fn from_external_data(data: &'static [AtomicU8], context: &mut Context) -> Self { + let prototype = context + .intrinsics() + .constructors() + .array_buffer() + .prototype(); + + let data = ArrayBuffer::from_external_data(data); + + let obj = JsObject::new(context.root_shape(), prototype, data); + + Self { inner: obj } + } + + /// Creates an `ArrayBuffer` that aliases `len` bytes of embedder-owned memory + /// starting at `ptr`. + /// + /// This is a convenience wrapper that builds the `&'static [AtomicU8]` slice from + /// its raw parts and delegates to [`JsArrayBuffer::from_external_data`]; see that + /// method for the aliasing and threading guarantees of the returned buffer. + /// + /// # Safety + /// + /// The caller must guarantee that: + /// + /// - `ptr` is valid for reads and writes of `len` bytes, and the region stays + /// valid **and unmoved** at the same address for the whole lifetime of the + /// returned buffer (and of every object that shares its data, e.g. typed arrays + /// or `DataView`s constructed over it). Note that the garbage collector may keep + /// the buffer alive for an unbounded amount of time after it becomes + /// unreachable, and that regions that can relocate, like a growable + /// `WebAssembly` linear memory that moves its base address on `memory.grow`, + /// silently invalidate the buffer unless the embedder guarantees that no + /// relocation happens while the buffer is alive. + /// - The region is not written to non-atomically from another thread while + /// JavaScript code that may access the buffer is executing. + /// + /// # Example + /// + /// ``` + /// # use boa_engine::{ + /// # object::builtins::{AlignedVec, JsArrayBuffer}, + /// # property::Attribute, + /// # Context, JsResult, Source, js_string, + /// # }; + /// # fn main() -> JsResult<()> { + /// # let context = &mut Context::default(); + /// // `AlignedVec` allocations are 64-byte aligned, which satisfies the required + /// // 8-byte alignment of external regions. + /// let mut backing: AlignedVec = AlignedVec::from_iter(0, [0u8; 8]); + /// + /// // SAFETY: `backing` stays alive and unmoved for the whole lifetime of the + /// // context that can reach the buffer. + /// let array_buffer = unsafe { + /// JsArrayBuffer::from_external_ptr(backing.as_mut_ptr(), backing.len(), context) + /// }; + /// + /// context.register_global_property(js_string!("buf"), array_buffer, Attribute::all())?; + /// context.eval(Source::from_bytes("new Uint8Array(buf).fill(42);"))?; + /// + /// assert_eq!(&backing[..], &[42u8; 8]); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Panics + /// + /// Panics if `ptr` is null, if `len` is bigger than `isize::MAX`, or if the region + /// is non-empty and `ptr` is not aligned to 8 bytes. + #[must_use] + pub unsafe fn from_external_ptr(ptr: *mut u8, len: usize, context: &mut Context) -> Self { + let prototype = context + .intrinsics() + .constructors() + .array_buffer() + .prototype(); + + // SAFETY: The caller upholds the invariants of `ArrayBuffer::from_external_ptr`. + let data = unsafe { ArrayBuffer::from_external_ptr(ptr, len) }; + + let obj = JsObject::new(context.root_shape(), prototype, data); + + Self { inner: obj } + } + + /// Returns `true` if this buffer is backed by embedder-owned memory. + /// + /// See [`JsArrayBuffer::from_external_data`]. + #[inline] + #[must_use] + pub fn is_external(&self) -> bool { + self.inner.borrow().data().is_external() + } + /// Set a maximum length for the underlying array buffer. #[inline] #[must_use] @@ -192,6 +345,13 @@ impl JsArrayBuffer { /// This tries to detach the pre-existing `JsArrayBuffer`, meaning the original detach /// key is required. By default, the key is set to `undefined`. /// + /// For a buffer backed by embedder-owned memory (see + /// [`JsArrayBuffer::from_external_data`]), this returns a copy of the region's + /// contents and drops the engine's reference into the region; the embedder remains + /// the owner of the region itself. This is the way for an embedder to guarantee + /// that the engine can no longer access the region, e.g. before unmapping or + /// freeing it. + /// /// ``` /// # use boa_engine::{ /// # object::builtins::{JsArrayBuffer, AlignedVec}, @@ -230,7 +390,10 @@ impl JsArrayBuffer { /// Get an immutable reference to the [`JsArrayBuffer`]'s data. /// - /// Returns `None` if detached. + /// Returns `None` if the buffer is detached or backed by embedder-owned memory + /// (see [`JsArrayBuffer::from_external_data`]); the embedder already owns an + /// externally-backed region and can read it directly, or copy it out with + /// [`JsArrayBuffer::to_vec`]. /// /// ``` /// # use boa_engine::{ @@ -259,7 +422,8 @@ impl JsArrayBuffer { /// Copies the contents of this [`JsArrayBuffer`] into a new [`Vec`]. /// - /// Returns `None` if the buffer has been detached. + /// Returns `None` if the buffer has been detached. This works for both Boa-owned + /// and externally-backed buffers. /// /// See also [`crate::object::builtins::JsUint8Array::to_vec`] and /// [`crate::object::builtins::JsSharedArrayBuffer::to_vec`]. @@ -283,12 +447,17 @@ impl JsArrayBuffer { #[inline] #[must_use] pub fn to_vec(&self) -> Option> { - self.data().map(|data| data.to_vec()) + self.inner + .borrow() + .data() + .slice_ref() + .map(crate::builtins::array_buffer::utils::SliceRef::to_vec) } /// Get a mutable reference to the [`JsArrayBuffer`]'s data. /// - /// Returns `None` if detached. + /// Returns `None` if the buffer is detached or backed by embedder-owned memory + /// (see [`JsArrayBuffer::from_external_data`]). /// /// ``` /// # use boa_engine::{ diff --git a/core/engine/src/object/builtins/jsdataview.rs b/core/engine/src/object/builtins/jsdataview.rs index 7bf07ade92d..e9e4187e735 100644 --- a/core/engine/src/object/builtins/jsdataview.rs +++ b/core/engine/src/object/builtins/jsdataview.rs @@ -64,14 +64,14 @@ impl JsDataView { let buffer = buffer.data(); // 4. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. - let Some(slice) = buffer.bytes() else { + if buffer.is_detached() { return Err(JsNativeError::typ() .with_message("ArrayBuffer is detached") .into()); - }; + } // 5. Let bufferByteLength be ArrayBufferByteLength(buffer, seq-cst). - let buf_len = slice.len() as u64; + let buf_len = buffer.len() as u64; // 6. If offset > bufferByteLength, throw a RangeError exception. if offset > buf_len { @@ -110,10 +110,14 @@ impl JsDataView { // 11. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. // 12. Set bufferByteLength to ArrayBufferByteLength(buffer, seq-cst). - let Some(buf_byte_len) = buffer.borrow().data().bytes().map(|s| s.len() as u64) else { - return Err(JsNativeError::typ() - .with_message("ArrayBuffer is detached") - .into()); + let buf_byte_len = { + let buffer = buffer.borrow(); + if buffer.data().is_detached() { + return Err(JsNativeError::typ() + .with_message("ArrayBuffer is detached") + .into()); + } + buffer.data().len() as u64 }; // 13. If offset > bufferByteLength, throw a RangeError exception. diff --git a/core/engine/src/object/builtins/jssharedarraybuffer.rs b/core/engine/src/object/builtins/jssharedarraybuffer.rs index 36b2453211b..ef707ffc00c 100644 --- a/core/engine/src/object/builtins/jssharedarraybuffer.rs +++ b/core/engine/src/object/builtins/jssharedarraybuffer.rs @@ -1,7 +1,7 @@ //! A Rust API wrapper for Boa's `SharedArrayBuffer` Builtin ECMAScript Object use crate::{ Context, JsResult, JsValue, - builtins::array_buffer::{SharedArrayBuffer, utils::SliceRef}, + builtins::array_buffer::{AtomicU8, SharedArrayBuffer, utils::SliceRef}, error::JsNativeError, object::JsObject, value::TryFromJs, @@ -63,6 +63,78 @@ impl JsSharedArrayBuffer { Self { inner } } + /// Creates a `SharedArrayBuffer` that aliases a region of embedder-owned memory. + /// + /// Unlike [`JsSharedArrayBuffer::new`], this does **not** allocate: the bytes of the + /// resulting `SharedArrayBuffer` are the provided region itself. Writes performed by + /// JavaScript code are immediately visible to the embedder and vice versa, enabling + /// zero-copy sharing of memory regions like `WebAssembly` linear memories, + /// memory-mapped files or GPU-mapped buffers. + /// + /// The engine only ever accesses the region with atomic operations. Accesses to the + /// region from other threads must be synchronized with the JavaScript code that may + /// access the buffer concurrently, exactly like for any other `SharedArrayBuffer` + /// memory. + /// + /// The resulting buffer is always fixed-length and cannot be grown. + /// + /// # Panics + /// + /// Panics if the region is non-empty and its base address is not aligned to 8 + /// bytes. `Atomics` and typed array views perform aligned atomic accesses of up to + /// 8 bytes on the backing memory, so the base address must satisfy the largest + /// alignment those accesses need. + #[inline] + #[must_use] + pub fn from_external_data(data: &'static [AtomicU8], context: &mut Context) -> Self { + Self::from_buffer(SharedArrayBuffer::from_external_data(data), context) + } + + /// Creates a `SharedArrayBuffer` that aliases `len` bytes of embedder-owned memory + /// starting at `ptr`. + /// + /// This is a convenience wrapper that builds the `&'static [AtomicU8]` slice from + /// its raw parts and delegates to [`JsSharedArrayBuffer::from_external_data`]; see + /// that method for the aliasing and threading guarantees of the returned buffer. + /// + /// # Safety + /// + /// The caller must guarantee that: + /// + /// - `ptr` is valid for reads and writes of `len` bytes, and the region stays + /// valid **and unmoved** at the same address for the whole lifetime of the + /// returned buffer and all of its clones (including clones sent to other + /// agents/threads). Note that the garbage collector may keep the buffer alive + /// for an unbounded amount of time after it becomes unreachable, and that + /// regions that can relocate, like a growable `WebAssembly` linear memory that + /// moves its base address on `memory.grow`, silently invalidate the buffer + /// unless the embedder guarantees that no relocation happens while the buffer + /// is alive. + /// - All accesses to the region from outside the buffer are performed with atomic + /// operations, or are otherwise synchronized with any JavaScript code that may + /// access the buffer concurrently. + /// + /// # Panics + /// + /// Panics if `ptr` is null, if `len` is bigger than `isize::MAX`, or if the region + /// is non-empty and `ptr` is not aligned to 8 bytes. + #[inline] + #[must_use] + pub unsafe fn from_external_ptr(ptr: *mut u8, len: usize, context: &mut Context) -> Self { + // SAFETY: The caller upholds the invariants of `SharedArrayBuffer::from_external_ptr`. + let buffer = unsafe { SharedArrayBuffer::from_external_ptr(ptr, len) }; + Self::from_buffer(buffer, context) + } + + /// Returns `true` if this buffer is backed by embedder-owned memory. + /// + /// See [`JsSharedArrayBuffer::from_external_data`]. + #[inline] + #[must_use] + pub fn is_external(&self) -> bool { + self.borrow().data().is_external() + } + /// Creates a [`JsSharedArrayBuffer`] from a [`JsObject`], throwing a `TypeError` if the object /// is not a shared array buffer. /// diff --git a/core/wintertc/src/store/from.rs b/core/wintertc/src/store/from.rs index 203746fd042..8be70b6f4b2 100644 --- a/core/wintertc/src/store/from.rs +++ b/core/wintertc/src/store/from.rs @@ -118,7 +118,8 @@ fn try_from_array_buffer_clone( buffer: &JsArrayBuffer, seen: &mut SeenMap, ) -> JsResult { - let data = buffer.data().ok_or_else(unsupported_type)?; + // `to_vec` works for both Boa-owned and externally-backed buffers. + let data = buffer.to_vec().ok_or_else(unsupported_type)?; let data = AlignedVec::from_slice(0, &data); let new_value = JsValueStore::new(ValueStoreInner::ArrayBuffer(data)); seen.insert(original, new_value.clone());