-
-
Notifications
You must be signed in to change notification settings - Fork 648
feat: allow backing an ArrayBuffer/SharedArrayBuffer with embedder-owned memory #5466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,11 @@ pub(crate) mod utils; | |
| #[cfg(test)] | ||
| mod tests; | ||
|
|
||
| use std::ops::{Deref, DerefMut}; | ||
| use std::{ | ||
| ops::{Deref, DerefMut}, | ||
| ptr::NonNull, | ||
| slice, | ||
| }; | ||
|
|
||
| use aligned_vec::{ABox, AVec, ConstAlign}; | ||
| pub use shared::SharedArrayBuffer; | ||
|
|
@@ -197,12 +201,73 @@ impl BufferObject { | |
| } | ||
| } | ||
|
|
||
| /// A region of embedder-owned memory backing an [`ArrayBuffer`] or a | ||
| /// [`SharedArrayBuffer`]. | ||
| /// | ||
| /// Boa never allocates, reallocates nor deallocates this region; the embedder | ||
| /// that created it is fully responsible for its lifetime and validity. | ||
| #[derive(Debug, Clone, Copy)] | ||
| pub(crate) struct ExternalMemory { | ||
| ptr: NonNull<u8>, | ||
| len: usize, | ||
| } | ||
|
|
||
| impl ExternalMemory { | ||
| fn as_slice(&self) -> &[u8] { | ||
| // SAFETY: The creator of an `ExternalMemory` guarantees that `ptr` is valid | ||
| // for reads and writes of `len` bytes for the whole lifetime of the buffer. | ||
| unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) } | ||
| } | ||
|
|
||
| fn as_slice_mut(&mut self) -> &mut [u8] { | ||
| // SAFETY: The creator of an `ExternalMemory` guarantees that `ptr` is valid | ||
| // for reads and writes of `len` bytes for the whole lifetime of the buffer. | ||
| unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) } | ||
| } | ||
|
|
||
| fn len(&self) -> usize { | ||
| self.len | ||
| } | ||
| } | ||
|
|
||
| /// The backing memory of an [`ArrayBuffer`]. | ||
| #[derive(Debug, Clone)] | ||
| pub(crate) enum BufferData { | ||
| /// Memory allocated and owned by Boa. | ||
| Owned(AlignedVec<u8>), | ||
| /// Memory owned by the embedder. See [`ArrayBuffer::from_external_data`]. | ||
| External(ExternalMemory), | ||
| } | ||
|
|
||
| impl BufferData { | ||
| fn as_slice(&self) -> &[u8] { | ||
| match self { | ||
| Self::Owned(vec) => vec, | ||
| Self::External(ext) => ext.as_slice(), | ||
| } | ||
| } | ||
|
|
||
| fn as_slice_mut(&mut self) -> &mut [u8] { | ||
| match self { | ||
| Self::Owned(vec) => vec, | ||
| Self::External(ext) => ext.as_slice_mut(), | ||
| } | ||
| } | ||
|
|
||
| fn len(&self) -> usize { | ||
| match self { | ||
| Self::Owned(vec) => vec.len(), | ||
| Self::External(ext) => ext.len(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// The internal representation of an `ArrayBuffer` object. | ||
| #[derive(Debug, Clone, Trace, Finalize, JsData)] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would a manual |
||
| pub struct ArrayBuffer { | ||
| /// The `[[ArrayBufferData]]` internal slot. | ||
| #[unsafe_ignore_trace] | ||
| data: Option<AlignedVec<u8>>, | ||
| data: Option<BufferData>, | ||
|
|
||
| /// The `[[ArrayBufferMaxByteLength]]` internal slot. | ||
| max_byte_len: Option<u64>, | ||
|
|
@@ -214,26 +279,70 @@ pub struct ArrayBuffer { | |
| impl ArrayBuffer { | ||
| pub(crate) fn from_data(data: AlignedVec<u8>, 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. | ||
| /// | ||
| /// Externally-backed buffers are always fixed-length and cannot be detached, | ||
| /// resized nor transferred. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// The caller must guarantee that: | ||
| /// | ||
| /// - `ptr` is non-null and valid for reads and writes of `len` bytes 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). | ||
| /// - The region is not accessed (read or written) from Rust while JavaScript code | ||
| /// that may access the buffer is executing, and no other Rust references to the | ||
| /// region are alive while Boa holds a reference into it. | ||
| /// - The region is not aliased by another `ArrayBuffer` or `SharedArrayBuffer` | ||
| /// while both can be accessed. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if `ptr` is null. | ||
| #[must_use] | ||
| pub unsafe fn from_external_data(ptr: *mut u8, len: usize) -> Self { | ||
| let ptr = NonNull::new(ptr).expect("`ptr` must be non-null"); | ||
| Self { | ||
| data: Some(BufferData::External(ExternalMemory { ptr, len })), | ||
| max_byte_len: None, | ||
| detach_key: JsValue::undefined(), | ||
| } | ||
| } | ||
|
|
||
| /// 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) | ||
| } | ||
|
|
||
| pub(crate) fn bytes(&self) -> Option<&[u8]> { | ||
| self.data.as_deref() | ||
| self.data.as_ref().map(BufferData::as_slice) | ||
| } | ||
|
|
||
| pub(crate) fn bytes_mut(&mut self) -> Option<&mut [u8]> { | ||
| self.data.as_deref_mut() | ||
| self.data.as_mut().map(BufferData::as_slice_mut) | ||
| } | ||
|
|
||
| pub(crate) fn vec_mut(&mut self) -> Option<&mut AlignedVec<u8>> { | ||
| 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. | ||
|
|
@@ -244,7 +353,7 @@ impl ArrayBuffer { | |
| /// Gets the inner bytes 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() { | ||
| if let Some(s) = self.bytes() { | ||
| Some(&s[..len]) | ||
| } else { | ||
| None | ||
|
|
@@ -254,7 +363,7 @@ impl ArrayBuffer { | |
| /// Gets the mutable inner bytes 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() { | ||
| if let Some(s) = self.bytes_mut() { | ||
| Some(&mut s[..len]) | ||
| } else { | ||
| None | ||
|
|
@@ -264,11 +373,19 @@ impl ArrayBuffer { | |
| /// Gets the underlying vector for this buffer. | ||
| #[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") | ||
|
|
@@ -298,15 +415,25 @@ impl ArrayBuffer { | |
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Throws an error if the provided detach key is invalid. | ||
| /// Throws an error if the provided detach key is invalid, or if the buffer is backed by | ||
| /// embedder-owned memory (see [`ArrayBuffer::from_external_data`]). | ||
| pub fn detach(&mut self, key: &JsValue) -> JsResult<Option<AlignedVec<u8>>> { | ||
| if !JsValue::same_value(&self.detach_key, key) { | ||
| return Err(JsNativeError::typ() | ||
| .with_message("Cannot detach array buffer with different key") | ||
| .into()); | ||
| } | ||
|
|
||
| Ok(self.data.take()) | ||
| if self.is_external() { | ||
| return Err(JsNativeError::typ() | ||
| .with_message("cannot detach an ArrayBuffer backed by embedder-owned memory") | ||
| .into()); | ||
| } | ||
|
|
||
| Ok(self.data.take().map(|data| match data { | ||
| BufferData::Owned(vec) => vec, | ||
| BufferData::External(_) => unreachable!("already checked for external data"), | ||
| })) | ||
| } | ||
|
|
||
| /// `IsDetachedBuffer ( arrayBuffer )` | ||
|
|
@@ -788,12 +915,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 +944,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 +964,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 +988,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 +1041,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, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more precondition for
slice::from_raw_parts: the region size must be<= isize::MAXbytes. It's a documented safety requirement offrom_raw_parts, so it'd be worth either asserting it at construction (from_external_data/from_external_ptr) or adding it to the# Safetylist the caller must uphold.