From 526d13032ced93665efaabc2feabab4ee9066050 Mon Sep 17 00:00:00 2001 From: Doron Somech Date: Tue, 23 Jun 2026 17:00:27 +0300 Subject: [PATCH] Add memcpy-based i8 array constructor and reader to ArrayRef Building a GC arrayref of bytes from host code today goes through ArrayRef::new_fixed, which takes a &[Val] (32 bytes each) and writes the elements one Val at a time. For a host bridging byte buffers into GC arrays (streaming I/O, codecs, etc.) that's a large transient allocation plus a per-element decode, and there's no faster path. Add ArrayRef::new_from_i8_slice / new_from_i8_slice_async, which take a &[u8] and fill the element body in a single memcpy, plus copy_to_i8_slice for the reverse. Both bypass Val entirely. This only covers i8 arrays for now: byte buffers are by far the most common case, and i8 needs no endianness handling, so the slice maps directly onto the heap layout with no unsafe. Wider element types can be added later on top of this. --- .../src/runtime/gc/enabled/arrayref.rs | 202 ++++++++++++++++++ tests/all/gc.rs | 87 ++++++++ 2 files changed, 289 insertions(+) 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<()> {