diff --git a/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs b/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs index f7565aa9c3b8..b4bac74f2507 100644 --- a/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs +++ b/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs @@ -541,6 +541,208 @@ impl ArrayRef { .await } + /// Synchronously allocate a new `i8` array initialized from the given bytes. + /// + /// Unlike [`ArrayRef::new_fixed`], which initializes the array one [`Val`] + /// at a time, the element body is filled with a single `memcpy`. The bytes + /// are passed as `u8`; their signedness is only observed at read time (e.g. + /// `array.get_s` vs `array.get_u`). + /// + /// # Automatic Garbage Collection + /// + /// If the GC heap is at capacity, and there isn't room for allocating this + /// new array, then this method will automatically trigger a synchronous + /// collection in an attempt to free up space in the GC heap. + /// + /// # Errors + /// + /// If the `allocator`'s array type does not have `i8` elements, an error is + /// returned. + /// + /// If the allocation cannot be satisfied because the GC heap is currently + /// out of memory, then a [`GcHeapOutOfMemory<()>`][crate::GcHeapOutOfMemory] + /// error is returned. The allocation might succeed on a second attempt if + /// you drop some rooted GC references and try again. + /// + /// If `store` is configured with a + /// [`ResourceLimiterAsync`](crate::ResourceLimiterAsync) then an error will + /// be returned because [`ArrayRef::new_from_i8_slice_async`] should be used + /// instead. + /// + /// # Panics + /// + /// Panics if the allocator is not associated with the given store. + pub fn new_from_i8_slice( + mut store: impl AsContextMut, + allocator: &ArrayRefPre, + elems: &[u8], + ) -> Result> { + let (mut limiter, store) = store + .as_context_mut() + .0 + .validate_sync_resource_limiter_and_store_opaque()?; + vm::assert_ready(Self::_new_from_i8_slice_async( + store, + limiter.as_mut(), + allocator, + elems, + Asyncness::No, + )) + } + + /// Asynchronously allocate a new `i8` array initialized from the given + /// bytes. + /// + /// This is the `async` equivalent of [`ArrayRef::new_from_i8_slice`]; see + /// that method for details. If your engine is not configured for async, use + /// [`ArrayRef::new_from_i8_slice`] to perform synchronous allocation. + /// + /// # Automatic Garbage Collection + /// + /// If the GC heap is at capacity, and there isn't room for allocating this + /// new array, then this method will automatically trigger an asynchronous + /// collection in an attempt to free up space in the GC heap. + /// + /// # Errors + /// + /// If the `allocator`'s array type does not have `i8` elements, an error is + /// returned. + /// + /// If the allocation cannot be satisfied because the GC heap is currently + /// out of memory, then a [`GcHeapOutOfMemory<()>`][crate::GcHeapOutOfMemory] + /// error is returned. The allocation might succeed on a second attempt if + /// you drop some rooted GC references and try again. + /// + /// # Panics + /// + /// Panics if the `store` is not configured for async; use + /// [`ArrayRef::new_from_i8_slice`] to perform synchronous allocation + /// instead. + /// + /// Panics if the allocator is not associated with the given store. + #[cfg(feature = "async")] + pub async fn new_from_i8_slice_async( + mut store: impl AsContextMut, + allocator: &ArrayRefPre, + elems: &[u8], + ) -> Result> { + let (mut limiter, store) = store.as_context_mut().0.resource_limiter_and_store_opaque(); + Self::_new_from_i8_slice_async(store, limiter.as_mut(), allocator, elems, Asyncness::Yes) + .await + } + + pub(crate) async fn _new_from_i8_slice_async( + store: &mut StoreOpaque, + limiter: Option<&mut StoreResourceLimiter<'_>>, + allocator: &ArrayRefPre, + elems: &[u8], + asyncness: Asyncness, + ) -> Result> { + store + .retry_after_gc_async(limiter, (), asyncness, |store, ()| { + Self::new_from_i8_slice_inner(store, allocator, elems) + }) + .await + } + + /// Allocate a new array initialized from a slice of `i8` bytes. + /// + /// Does not attempt a GC on OOM; leaves that to callers. + fn new_from_i8_slice_inner( + store: &mut StoreOpaque, + allocator: &ArrayRefPre, + elems: &[u8], + ) -> Result> { + assert_eq!( + store.id(), + allocator.store_id, + "attempted to use a `ArrayRefPre` with the wrong store" + ); + + let elem_ty = allocator.ty.element_type(); + ensure!( + elem_ty.is_i8(), + "element type mismatch: cannot initialize an array of `{elem_ty}` elements from a slice of `i8`s" + ); + + let len = u32::try_from(elems.len())?; + let layout = allocator.layout(); + + let arrayref = store + .require_gc_store_mut()? + .alloc_uninit_array(allocator.type_index(), len, layout) + .context("unrecoverable error when allocating new `arrayref`")? + .map_err(|n| GcHeapOutOfMemory::new((), n))?; + + let mut store = AutoAssertNoGc::new(store); + let data = store + .require_gc_store_mut()? + .gc_object_data(arrayref.as_gc_ref())?; + let copied = data.copy_from_slice(layout.base_size, elems); + + // If the copy failed then the array is not fully initialized, so we + // must eagerly deallocate it before the next GC. + match copied { + Ok(()) => Ok(Rooted::new(&mut store, arrayref.into())), + Err(e) => { + store + .require_gc_store_mut()? + .dealloc_uninit_array(arrayref)?; + Err(e) + } + } + } + + /// Copy this `i8` array's elements into the given byte slice. + /// + /// Unlike [`ArrayRef::get`], which decodes each element through a [`Val`], + /// the whole element body is copied into `dst` with a single `memcpy`. The + /// `i8` elements are read out as raw `u8` bytes. + /// + /// # Errors + /// + /// If this array does not have `i8` elements, an error is returned. + /// + /// If `dst`'s length does not equal this array's length, an error is + /// returned. + /// + /// Returns an error if this reference has been unrooted. + /// + /// # Panics + /// + /// Panics if this reference is associated with a different store. + pub fn copy_to_i8_slice(&self, mut store: impl AsContextMut, dst: &mut [u8]) -> Result<()> { + let mut store = AutoAssertNoGc::new(store.as_context_mut().0); + assert!( + self.comes_from_same_store(&store), + "attempted to use an array with the wrong store", + ); + + let field_ty = self.field_ty(&store)?; + let elem_ty = field_ty.element_type(); + ensure!( + elem_ty.is_i8(), + "element type mismatch: cannot read an array of `{elem_ty}` elements into a slice of `i8`s" + ); + + let layout = self.layout(&store)?; + let arrayref = self.arrayref(&store)?.unchecked_copy(); + let len = arrayref.len(&store)?; + + let dst_len = u32::try_from(dst.len())?; + ensure!( + dst_len == len, + "destination slice length is {dst_len} but the array length is {len}", + ); + + let data = store + .require_gc_store_mut()? + .gc_object_data(arrayref.as_gc_ref())?; + let bytes = data.slice(layout.base_size, len)?; + dst.copy_from_slice(bytes); + Ok(()) + } + #[inline] pub(crate) fn comes_from_same_store(&self, store: &StoreOpaque) -> bool { self.inner.comes_from_same_store(store) diff --git a/tests/all/gc.rs b/tests/all/gc.rs index 149ca10eddb6..58f4b95eee48 100644 --- a/tests/all/gc.rs +++ b/tests/all/gc.rs @@ -3296,6 +3296,67 @@ fn typed_option_noneref() -> Result<()> { Ok(()) } +#[test] +#[cfg_attr(miri, ignore)] +fn array_i8_slice_roundtrip() -> Result<()> { + let mut store = gc_store()?; + let engine = store.engine().clone(); + + let array_ty = ArrayType::new(&engine, FieldType::new(Mutability::Var, StorageType::I8)); + let pre = ArrayRefPre::new(&mut store, array_ty); + + // Bytes 0x80 and 0xFF are `i8` -128 and -1, or `u8` 128 and 255. + let src: [u8; 5] = [0x80, 0xFF, 0x00, 0x01, 0x7F]; + let array = ArrayRef::new_from_i8_slice(&mut store, &pre, &src)?; + assert_eq!(array.len(&store)?, 5); + + // Round-trip the bytes back out. + let mut dst = [0u8; 5]; + array.copy_to_i8_slice(&mut store, &mut dst)?; + assert_eq!(dst, src); + + // `get` zero-extends, i.e. the `array.get_u` interpretation of the bytes. + assert_eq!(array.get(&mut store, 0)?.unwrap_i32(), 128); + assert_eq!(array.get(&mut store, 1)?.unwrap_i32(), 255); + + // Same logical contents as building it element-by-element with `new_fixed`. + let fixed = ArrayRef::new_fixed( + &mut store, + &pre, + &src.iter().map(|&b| Val::I32(b.into())).collect::>(), + )?; + for i in 0..src.len() as u32 { + assert_eq!( + array.get(&mut store, i)?.unwrap_i32(), + fixed.get(&mut store, i)?.unwrap_i32(), + ); + } + + // Empty slices work. + let empty = ArrayRef::new_from_i8_slice(&mut store, &pre, &[])?; + assert_eq!(empty.len(&store)?, 0); + empty.copy_to_i8_slice(&mut store, &mut [])?; + + // A destination of the wrong length is an error. + assert!(array.copy_to_i8_slice(&mut store, &mut [0u8; 3]).is_err()); + + // The element type must actually be `i8`. + let i32_ty = ArrayType::new( + &engine, + FieldType::new(Mutability::Var, ValType::I32.into()), + ); + let i32_pre = ArrayRefPre::new(&mut store, i32_ty); + assert!(ArrayRef::new_from_i8_slice(&mut store, &i32_pre, &[1, 2, 3]).is_err()); + let i32_array = ArrayRef::new(&mut store, &i32_pre, &Val::I32(0), 3)?; + assert!( + i32_array + .copy_to_i8_slice(&mut store, &mut [0u8; 3]) + .is_err() + ); + + Ok(()) +} + #[test] #[cfg_attr(miri, ignore)] fn typed_option_noextern() -> Result<()> { @@ -3452,6 +3513,32 @@ fn miri_gc_smoke_test() -> Result<()> { Ok(()) } +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn array_i8_slice_async() -> Result<()> { + let _ = env_logger::try_init(); + + let mut config = Config::new(); + config.wasm_gc(true); + config.wasm_function_references(true); + + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let array_ty = ArrayType::new(&engine, FieldType::new(Mutability::Var, StorageType::I8)); + let pre = ArrayRefPre::new(&mut store, array_ty); + + let src = [0x80, 0xFF, 0x00, 0x01, 0x7F]; + let array = ArrayRef::new_from_i8_slice_async(&mut store, &pre, &src).await?; + assert_eq!(array.len(&store)?, 5); + + let mut dst = [0u8; 5]; + array.copy_to_i8_slice(&mut store, &mut dst)?; + assert_eq!(dst, src); + + Ok(()) +} + #[tokio::test] #[cfg_attr(miri, ignore)] async fn copying_collector_async_gc_yields() -> Result<()> {