diff --git a/boa_engine/src/builtins/array_buffer/mod.rs b/boa_engine/src/builtins/array_buffer/mod.rs index 8fe4c5dac7c..c8bf6481ed8 100644 --- a/boa_engine/src/builtins/array_buffer/mod.rs +++ b/boa_engine/src/builtins/array_buffer/mod.rs @@ -139,7 +139,7 @@ impl ArrayBuffer { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength - fn get_byte_length( + pub(crate) fn get_byte_length( this: &JsValue, _args: &[JsValue], context: &mut Context, diff --git a/boa_engine/src/object/jsarraybuffer.rs b/boa_engine/src/object/jsarraybuffer.rs new file mode 100644 index 00000000000..d15bd7226ea --- /dev/null +++ b/boa_engine/src/object/jsarraybuffer.rs @@ -0,0 +1,123 @@ +use crate::{ + builtins::array_buffer::ArrayBuffer, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, JsObject, JsObjectType, ObjectData, + }, + Context, JsResult, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use std::ops::Deref; + +/// JavaScript `ArrayBuffer` rust object. +#[derive(Debug, Clone, Trace, Finalize)] +pub struct JsArrayBuffer { + inner: JsObject, +} + +impl JsArrayBuffer { + /// Create a new array buffer with byte length. + #[inline] + pub fn new(byte_length: usize, context: &mut Context) -> JsResult { + let inner = ArrayBuffer::allocate( + &context + .intrinsics() + .constructors() + .array_buffer() + .constructor() + .into(), + byte_length, + context, + )?; + + Ok(Self { inner }) + } + + /// Create a new array buffer from byte block. + /// + /// This uses the passed byte block as the internal storage, it does not clone it! + /// + /// The `byte_length` will be set to `byte_block.len()`. + #[inline] + pub fn from_byte_block(byte_block: Vec, context: &mut Context) -> JsResult { + let byte_length = byte_block.len(); + + let constructor = context + .intrinsics() + .constructors() + .array_buffer() + .constructor() + .into(); + + // 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] »). + let prototype = get_prototype_from_constructor( + &constructor, + StandardConstructors::array_buffer, + context, + )?; + let obj = context.construct_object(); + obj.set_prototype(prototype.into()); + + // 2. Let block be ? CreateByteDataBlock(byteLength). + // + // NOTE: We skip step 2. because we already have the block + // that is passed to us as a function argument. + let block = byte_block; + + // 3. Set obj.[[ArrayBufferData]] to block. + // 4. Set obj.[[ArrayBufferByteLength]] to byteLength. + obj.borrow_mut().data = ObjectData::array_buffer(ArrayBuffer { + array_buffer_data: Some(block), + array_buffer_byte_length: byte_length, + array_buffer_detach_key: JsValue::Undefined, + }); + + Ok(Self { inner: obj }) + } + + /// Create a [`JsArrayBuffer`] from a [`JsObject`], if the object is not an array buffer throw a `TypeError`. + /// + /// This does not clone the fields of the array buffer, it only does a shallow clone of the object. + #[inline] + pub fn from_object(object: JsObject, context: &mut Context) -> JsResult { + if object.borrow().is_array_buffer() { + Ok(Self { inner: object }) + } else { + context.throw_type_error("object is not an ArrayBuffer") + } + } + + /// Returns the byte length of the array buffer. + #[inline] + pub fn byte_length(&self, context: &mut Context) -> usize { + ArrayBuffer::get_byte_length(&self.inner.clone().into(), &[], context) + .expect("it should not throw") + .as_number() + .expect("expected a number") as usize + } +} + +impl From for JsObject { + #[inline] + fn from(o: JsArrayBuffer) -> Self { + o.inner.clone() + } +} + +impl From for JsValue { + #[inline] + fn from(o: JsArrayBuffer) -> Self { + o.inner.clone().into() + } +} + +impl Deref for JsArrayBuffer { + type Target = JsObject; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl JsObjectType for JsArrayBuffer {} diff --git a/boa_engine/src/object/jstypedarray.rs b/boa_engine/src/object/jstypedarray.rs index c23bd04de40..6fb5bc60bfd 100644 --- a/boa_engine/src/object/jstypedarray.rs +++ b/boa_engine/src/object/jstypedarray.rs @@ -1,6 +1,6 @@ use crate::{ builtins::typed_array::TypedArray, - object::{JsArray, JsFunction, JsObject, JsObjectType}, + object::{JsArrayBuffer, JsFunction, JsObject, JsObjectType}, value::IntoOrUndefined, Context, JsResult, JsString, JsValue, }; @@ -329,12 +329,43 @@ macro_rules! JsTypedArrayType { } impl $name { + #[inline] + pub fn from_array_buffer( + array_buffer: JsArrayBuffer, + context: &mut Context, + ) -> JsResult { + let new_target = context + .intrinsics() + .constructors() + .$constructor_object() + .constructor() + .into(); + let object = crate::builtins::typed_array::$constructor_function::constructor( + &new_target, + &[array_buffer.into()], + context, + )? + .as_object() + .expect("object") + .clone(); + + Ok(Self { + inner: JsTypedArray { + inner: object.into(), + }, + }) + } + #[inline] pub fn from_iter(elements: I, context: &mut Context) -> JsResult where I: IntoIterator, { - let array = JsArray::from_iter(elements.into_iter().map(JsValue::new), context); + let bytes: Vec<_> = elements + .into_iter() + .flat_map(<$element>::to_ne_bytes) + .collect(); + let array_buffer = JsArrayBuffer::from_byte_block(bytes, context)?; let new_target = context .intrinsics() .constructors() @@ -343,7 +374,7 @@ macro_rules! JsTypedArrayType { .into(); let object = crate::builtins::typed_array::$constructor_function::constructor( &new_target, - &[array.into()], + &[array_buffer.into()], context, )? .as_object() diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 4c3dd10b98d..5922885ab76 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -62,6 +62,7 @@ mod tests; pub(crate) mod internal_methods; mod jsarray; +mod jsarraybuffer; mod jsfunction; mod jsmap; mod jsmap_iterator; @@ -72,6 +73,7 @@ mod operations; mod property_map; pub use jsarray::*; +pub use jsarraybuffer::*; pub use jsfunction::*; pub use jsmap::*; pub use jsmap_iterator::*; diff --git a/boa_examples/src/bin/jsarraybuffer.rs b/boa_examples/src/bin/jsarraybuffer.rs new file mode 100644 index 00000000000..4f6773b27a1 --- /dev/null +++ b/boa_examples/src/bin/jsarraybuffer.rs @@ -0,0 +1,49 @@ +// This example shows how to manipulate a Javascript array using Rust code. + +use boa_engine::{ + object::{JsArrayBuffer, JsUint32Array, JsUint8Array}, + property::Attribute, + Context, JsResult, JsValue, +}; + +fn main() -> JsResult<()> { + // We create a new `Context` to create a new Javascript executor. + let context = &mut Context::default(); + + // This create an array buffer of byte length 4 + let array_buffer = JsArrayBuffer::new(4, context)?; + + // We can now create an typed array to access the data. + let uint32_typed_array = JsUint32Array::from_array_buffer(array_buffer, context)?; + + let value = 0x12345678u32; + uint32_typed_array.set(0, value, true, context)?; + + assert_eq!(uint32_typed_array.get(0, context)?, JsValue::new(value)); + + // We can also create array buffers from a user defined block of data. + // + // NOTE: The block data will not be cloned. + let blob_of_data: Vec = (0..=255).collect(); + let array_buffer = JsArrayBuffer::from_byte_block(blob_of_data, context)?; + + // This the byte length of the new array buffer will be the length of block of data. + let byte_length = array_buffer.byte_length(context); + assert_eq!(byte_length, 256); + + // We can now create an typed array to access the data. + let uint8_typed_array = JsUint8Array::from_array_buffer(array_buffer.clone(), context)?; + + for i in 0..byte_length { + assert_eq!(uint8_typed_array.get(i, context)?, JsValue::new(i)); + } + + // We can also register it as a global property + context.register_global_property( + "myArrayBuffer", + array_buffer, + Attribute::WRITABLE | Attribute::ENUMERABLE | Attribute::CONFIGURABLE, + ); + + Ok(()) +}