Repository: bytecodealliance/wasmtime
Pull request #9145 (2024-08-19 22:06:24)

Introduce `wasmtime::ArrayRef` and allocating Wasm GC arrays
This commit introduces the `wasmtime::ArrayRef` type and support for allocating Wasm GC arrays from the host. This commit does *not* add support for the `array.new` family of Wasm instructions; guests still cannot allocate Wasm GC objects yet, but initial support should be pretty straightforward after this commit lands.

The `ArrayRef` type has everything you expect from other value types in the `wasmtime` crate:

* A method to get its type or check whether it matches a given type

* An implementation of `WasmTy` so that it can be used with `Func::wrap`-style APIs

* The ability to upcast it into an `AnyRef` and to do checked downcasts in the opposite direction

There are, additionally, methods for getting, setting, and enumerating a `ArrayRef`'s elements.

Similar to how allocating a Wasm GC struct requires a `StructRefPre`, allocating a Wasm GC array requires an `ArrayRefPre`, and this is motivated by the same reasons.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->


github-actions[bot]: #### Subscribe to Label Action

cc @fitzgen

<details>
This issue or pull request has been labeled: "wasmtime:api", "wasmtime:ref-types"

Thus the following users have been cc'd because of the following labels:

* fitzgen: wasmtime:ref-types

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/crates/wasmtime/src/runtime/externals/global.rs b/crates/wasmtime/src/runtime/externals/global.rs
index 1b396001d..bc6916bc0 100644
--- a/crates/wasmtime/src/runtime/externals/global.rs
+++ b/crates/wasmtime/src/runtime/externals/global.rs
@@ -196,6 +196,7 @@ impl Global {
                 Val::ExternRef(e) => {
                     let new = match e {
                         None => None,
+                        #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
                         Some(e) => Some(e.try_gc_ref(&mut store)?.unchecked_copy()),
                     };
                     let new = new.as_ref();
@@ -204,6 +205,7 @@ impl Global {
                 Val::AnyRef(a) => {
                     let new = match a {
                         None => None,
+                        #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
                         Some(a) => Some(a.try_gc_ref(&mut store)?.unchecked_copy()),
                     };
                     let new = new.as_ref();
diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs
index c9fac0324..927348376 100644
--- a/crates/wasmtime/src/runtime/func.rs
+++ b/crates/wasmtime/src/runtime/func.rs
@@ -608,6 +608,10 @@ impl Func {
     /// | `AnyRef`                          | `(ref any)`                               |
     /// | `Option<I31>`                     | `i31ref` aka `(ref null i31)`             |
     /// | `I31`                             | `(ref i31)`                               |
+    /// | `Option<StructRef>`               | `(ref null struct)`                       |
+    /// | `StructRef`                       | `(ref struct)`                            |
+    /// | `Option<ArrayRef>`                | `(ref null array)`                        |
+    /// | `ArrayRef`                        | `(ref array)`                             |
     ///
     /// Any of the Rust types can be returned from the closure as well, in
     /// addition to some extra types
@@ -1428,6 +1432,10 @@ impl Func {
     /// | `(ref any)`                               | `AnyRef`                              |
     /// | `i31ref` aka `(ref null i31)`             | `Option<I31>`                         |
     /// | `(ref i31)`                               | `I31`                                 |
+    /// | `structref` aka `(ref null struct)`       | `Option<Struct>`                      |
+    /// | `(ref struct)`                            | `Struct`                              |
+    /// | `arrayref` aka `(ref null array)`         | `Option<Array>`                       |
+    /// | `(ref array)`                             | `Array`                               |
     /// | `funcref` aka `(ref null func)`           | `Option<Func>`                        |
     /// | `(ref func)`                              | `Func`                                |
     /// | `(ref null <func type index>)`            | `Option<Func>`                        |
diff --git a/crates/wasmtime/src/runtime/gc/disabled.rs b/crates/wasmtime/src/runtime/gc/disabled.rs
index e78ff8fd8..9b785043e 100644
--- a/crates/wasmtime/src/runtime/gc/disabled.rs
+++ b/crates/wasmtime/src/runtime/gc/disabled.rs
@@ -9,12 +9,14 @@
 #![allow(missing_docs, unreachable_code)]
 
 mod anyref;
+mod arrayref;
 mod externref;
 mod i31;
 mod rooting;
 mod structref;
 
 pub use anyref::*;
+pub use arrayref::*;
 pub use externref::*;
 pub use i31::*;
 pub use rooting::*;
diff --git a/crates/wasmtime/src/runtime/gc/disabled/anyref.rs b/crates/wasmtime/src/runtime/gc/disabled/anyref.rs
index b73b47d91..ee62ec4ae 100644
--- a/crates/wasmtime/src/runtime/gc/disabled/anyref.rs
+++ b/crates/wasmtime/src/runtime/gc/disabled/anyref.rs
@@ -1,13 +1,42 @@
 use crate::runtime::vm::VMGcRef;
 use crate::{
     store::{AutoAssertNoGc, StoreOpaque},
-    AsContext, AsContextMut, GcRefImpl, HeapType, Result, Rooted, StructRef, I31,
+    ArrayRef, AsContext, AsContextMut, GcRefImpl, HeapType, ManuallyRooted, Result, Rooted,
+    StructRef, I31,
 };
 
 /// Support for `anyref` disabled at compile time because the `gc` cargo feature
 /// was not enabled.
 pub enum AnyRef {}
 
+impl From<Rooted<StructRef>> for Rooted<AnyRef> {
+    #[inline]
+    fn from(s: Rooted<StructRef>) -> Self {
+        match s.inner {}
+    }
+}
+
+impl From<ManuallyRooted<StructRef>> for ManuallyRooted<AnyRef> {
+    #[inline]
+    fn from(s: ManuallyRooted<StructRef>) -> Self {
+        match s.inner {}
+    }
+}
+
+impl From<Rooted<ArrayRef>> for Rooted<AnyRef> {
+    #[inline]
+    fn from(s: Rooted<ArrayRef>) -> Self {
+        match s.inner {}
+    }
+}
+
+impl From<ManuallyRooted<ArrayRef>> for ManuallyRooted<AnyRef> {
+    #[inline]
+    fn from(s: ManuallyRooted<ArrayRef>) -> Self {
+        match s.inner {}
+    }
+}
+
 impl GcRefImpl for AnyRef {}
 
 impl AnyRef {
@@ -74,4 +103,24 @@ impl AnyRef {
     pub fn unwrap_struct(&self, _store: impl AsContext) -> Result<StructRef> {
         match *self {}
     }
+
+    pub fn is_array(&self, _store: impl AsContext) -> Result<bool> {
+        match *self {}
+    }
+
+    pub(crate) fn _is_array(&self, _store: &StoreOpaque) -> Result<bool> {
+        match *self {}
+    }
+
+    pub fn as_array(&self, _store: impl AsContext) -> Result<Option<ArrayRef>> {
+        match *self {}
+    }
+
+    pub(crate) fn _as_array(&self, _store: &StoreOpaque) -> Result<Option<ArrayRef>> {
+        match *self {}
+    }
+
+    pub fn unwrap_array(&self, _store: impl AsContext) -> Result<ArrayRef> {
+        match *self {}
+    }
 }
diff --git a/crates/wasmtime/src/runtime/gc/disabled/arrayref.rs b/crates/wasmtime/src/runtime/gc/disabled/arrayref.rs
new file mode 100644
index 000000000..38ec2da9c
--- /dev/null
+++ b/crates/wasmtime/src/runtime/gc/disabled/arrayref.rs
@@ -0,0 +1,56 @@
+use crate::runtime::vm::VMGcRef;
+use crate::{
+    store::{AutoAssertNoGc, StoreContextMut, StoreOpaque},
+    ArrayType, AsContext, AsContextMut, GcRefImpl, Result, Rooted, Val, I31,
+};
+
+/// Support for `ArrayRefPre` disabled at compile time because the `gc` cargo
+/// feature was not enabled.
+pub enum ArrayRefPre {}
+
+/// Support for `arrayref` disabled at compile time because the `gc` cargo feature
+/// was not enabled.
+pub enum ArrayRef {}
+
+impl GcRefImpl for ArrayRef {}
+
+impl ArrayRef {
+    pub(crate) fn from_cloned_gc_ref(
+        _store: &mut AutoAssertNoGc<'_>,
+        _gc_ref: VMGcRef,
+    ) -> Rooted<Self> {
+        unreachable!()
+    }
+
+    pub fn ty(&self, _store: impl AsContext) -> Result<ArrayType> {
+        match *self {}
+    }
+
+    pub fn matches_ty(&self, _store: impl AsContext, _ty: &ArrayType) -> Result<bool> {
+        match *self {}
+    }
+
+    pub(crate) fn _matches_ty(&self, _store: &StoreOpaque, _ty: &ArrayType) -> Result<bool> {
+        match *self {}
+    }
+
+    pub fn len(&self, _store: impl AsContext) -> Result<u32> {
+        match *self {}
+    }
+
+    pub fn elems<'a, T: 'a>(
+        &self,
+        _store: impl Into<StoreContextMut<'a, T>>,
+    ) -> Result<impl ExactSizeIterator<Item = Val> + 'a> {
+        match *self {}
+        Ok([].into_iter())
+    }
+
+    pub fn get(&self, _store: impl AsContextMut, _index: usize) -> Result<Val> {
+        match *self {}
+    }
+
+    pub fn set(&self, _store: impl AsContextMut, _index: usize, _value: Val) -> Result<()> {
+        match *self {}
+    }
+}
diff --git a/crates/wasmtime/src/runtime/gc/disabled/rooting.rs b/crates/wasmtime/src/runtime/gc/disabled/rooting.rs
index d590cc1a0..c00d93bbc 100644
--- a/crates/wasmtime/src/runtime/gc/disabled/rooting.rs
+++ b/crates/wasmtime/src/runtime/gc/disabled/rooting.rs
@@ -58,7 +58,7 @@ impl RootSet {
 /// This type is disabled because the `gc` cargo feature was not enabled at
 /// compile time.
 pub struct Rooted<T: GcRef> {
-    inner: Uninhabited,
+    pub(crate) inner: Uninhabited,
     _phantom: marker::PhantomData<T>,
 }
 
@@ -175,7 +175,7 @@ pub struct ManuallyRooted<T>
 where
     T: GcRef,
 {
-    inner: Uninhabited,
+    pub(crate) inner: Uninhabited,
     _phantom: marker::PhantomData<T>,
 }
 
diff --git a/crates/wasmtime/src/runtime/gc/enabled.rs b/crates/wasmtime/src/runtime/gc/enabled.rs
index fb7abb4e3..7e1f70ddb 100644
--- a/crates/wasmtime/src/runtime/gc/enabled.rs
+++ b/crates/wasmtime/src/runtime/gc/enabled.rs
@@ -2,12 +2,14 @@
 //! feature is enabled.
 
 mod anyref;
+mod arrayref;
 mod externref;
 mod i31;
 mod rooting;
 mod structref;
 
 pub use anyref::*;
+pub use arrayref::*;
 pub use externref::*;
 pub use i31::*;
 pub use rooting::*;
diff --git a/crates/wasmtime/src/runtime/gc/enabled/anyref.rs b/crates/wasmtime/src/runtime/gc/enabled/anyref.rs
index 605c7a503..ffd54b87b 100644
--- a/crates/wasmtime/src/runtime/gc/enabled/anyref.rs
+++ b/crates/wasmtime/src/runtime/gc/enabled/anyref.rs
@@ -4,8 +4,8 @@ use crate::prelude::*;
 use crate::runtime::vm::VMGcRef;
 use crate::{
     store::{AutoAssertNoGc, StoreOpaque},
-    ArrayType, AsContext, AsContextMut, GcRefImpl, GcRootIndex, HeapType, ManuallyRooted, RefType,
-    Result, Rooted, StructRef, StructType, ValRaw, ValType, WasmTy, I31,
+    ArrayRef, ArrayType, AsContext, AsContextMut, GcRefImpl, GcRootIndex, HeapType, ManuallyRooted,
+    RefType, Result, Rooted, StructRef, StructType, ValRaw, ValType, WasmTy, I31,
 };
 use core::mem;
 use core::mem::MaybeUninit;
@@ -109,6 +109,20 @@ impl From<ManuallyRooted<StructRef>> for ManuallyRooted<AnyRef> {
     }
 }
 
+impl From<Rooted<ArrayRef>> for Rooted<AnyRef> {
+    #[inline]
+    fn from(s: Rooted<ArrayRef>) -> Self {
+        s.to_anyref()
+    }
+}
+
+impl From<ManuallyRooted<ArrayRef>> for ManuallyRooted<AnyRef> {
+    #[inline]
+    fn from(s: ManuallyRooted<ArrayRef>) -> Self {
+        s.to_anyref()
+    }
+}
+
 unsafe impl GcRefImpl for AnyRef {
     #[allow(private_interfaces)]
     fn transmute_ref(index: &GcRootIndex) -> &Self {
@@ -428,6 +442,70 @@ impl AnyRef {
             ._as_struct(store)?
             .expect("AnyRef::unwrap_struct on non-structref"))
     }
+
+    /// Is this `anyref` an `arrayref`?
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store.
+    pub fn is_array(&self, store: impl AsContext) -> Result<bool> {
+        self._is_array(store.as_context().0)
+    }
+
+    pub(crate) fn _is_array(&self, store: &StoreOpaque) -> Result<bool> {
+        let gc_ref = self.inner.try_gc_ref(store)?;
+        Ok(!gc_ref.is_i31() && store.gc_store()?.kind(gc_ref).matches(VMGcKind::ArrayRef))
+    }
+
+    /// Downcast this `anyref` to an `arrayref`.
+    ///
+    /// If this `anyref` is an `arrayref`, then `Some(_)` is returned.
+    ///
+    /// If this `anyref` is not an `arrayref`, then `None` is returned.
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store.
+    pub fn as_array(&self, store: impl AsContext) -> Result<Option<Rooted<ArrayRef>>> {
+        self._as_array(store.as_context().0)
+    }
+
+    pub(crate) fn _as_array(&self, store: &StoreOpaque) -> Result<Option<Rooted<ArrayRef>>> {
+        if self._is_array(store)? {
+            Ok(Some(Rooted::from_gc_root_index(self.inner)))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Downcast this `anyref` to an `arrayref`, panicking if this `anyref` is
+    /// not an `arrayref`.
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store, or if
+    /// this `anyref` is not an `array`.
+    pub fn unwrap_array(&self, store: impl AsContext) -> Result<Rooted<ArrayRef>> {
+        self._unwrap_array(store.as_context().0)
+    }
+
+    pub(crate) fn _unwrap_array(&self, store: &StoreOpaque) -> Result<Rooted<ArrayRef>> {
+        Ok(self
+            ._as_array(store)?
+            .expect("AnyRef::unwrap_array on non-arrayref"))
+    }
 }
 
 unsafe impl WasmTy for Rooted<AnyRef> {
diff --git a/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs b/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs
new file mode 100644
index 000000000..dae784bac
--- /dev/null
+++ b/crates/wasmtime/src/runtime/gc/enabled/arrayref.rs
@@ -0,0 +1,879 @@
+//! Working with GC `array` objects.
+
+use crate::runtime::vm::VMGcRef;
+use crate::store::StoreId;
+use crate::vm::{GcArrayLayout, GcLayout, VMArrayRef, VMGcHeader};
+use crate::{
+    prelude::*,
+    store::{AutoAssertNoGc, StoreContextMut, StoreOpaque},
+    ArrayType, AsContext, AsContextMut, GcHeapOutOfMemory, GcRefImpl, GcRootIndex, HeapType,
+    ManuallyRooted, RefType, Rooted, Val, ValRaw, ValType, WasmTy,
+};
+use crate::{AnyRef, FieldType};
+use core::mem::{self, MaybeUninit};
+use wasmtime_environ::{VMGcKind, VMSharedTypeIndex};
+
+/// An allocator for a particular Wasm GC array type.
+///
+/// Every `ArrayRefPre` is associated with a particular [`Store`][crate::Store]
+/// and a particular [`ArrayType`][crate::ArrayType].
+///
+/// Reusing an allocator across many allocations amortizes some per-type runtime
+/// overheads inside Wasmtime. An `ArrayRefPre` is to `ArrayRef`s as an
+/// `InstancePre` is to `Instance`s.
+///
+/// # Example
+///
+/// ```
+/// use wasmtime::*;
+///
+/// # fn foo() -> Result<()> {
+/// let mut config = Config::new();
+/// config.wasm_function_references(true);
+/// config.wasm_gc(true);
+///
+/// let engine = Engine::new(&config)?;
+/// let mut store = Store::new(&engine, ());
+///
+/// // Define an array type.
+/// let array_ty = ArrayType::new(
+///    store.engine(),
+///    FieldType::new(Mutability::Var, ValType::I32.into()),
+/// );
+///
+/// // Create an allocator for the array type.
+/// let allocator = ArrayRefPre::new(&mut store, array_ty);
+///
+/// {
+///     let mut scope = RootScope::new(&mut store);
+///
+///     // Allocate a bunch of instances of our array type using the same
+///     // allocator! This is faster than creating a new allocator for each
+///     // instance we want to allocate.
+///     for i in 0..10 {
+///         let len = 42;
+///         let elem = Val::I32(36);
+///         ArrayRef::new(&mut scope, &allocator, &elem, len)?;
+///     }
+/// }
+/// # Ok(())
+/// # }
+/// # foo().unwrap();
+/// ```
+pub struct ArrayRefPre {
+    store_id: StoreId,
+    ty: ArrayType,
+}
+
+impl ArrayRefPre {
+    /// Create a new `ArrayRefPre` that is associated with the given store
+    /// and type.
+    pub fn new(mut store: impl AsContextMut, ty: ArrayType) -> Self {
+        Self::_new(store.as_context_mut().0, ty)
+    }
+
+    pub(crate) fn _new(store: &mut StoreOpaque, ty: ArrayType) -> Self {
+        store.insert_gc_host_alloc_type(ty.registered_type().clone());
+        let store_id = store.id();
+        ArrayRefPre { store_id, ty }
+    }
+
+    pub(crate) fn layout(&self) -> &GcArrayLayout {
+        self.ty
+            .registered_type()
+            .layout()
+            .expect("array types have a layout")
+            .unwrap_array()
+    }
+
+    pub(crate) fn type_index(&self) -> VMSharedTypeIndex {
+        self.ty.registered_type().index()
+    }
+}
+
+/// A reference to a GC-managed `array` instance.
+///
+/// WebAssembly `array`s are a sequence of elements of some homogeneous
+/// type. The elements length is determined at allocation time — two instances
+/// of the same array type may have different lengths — but, once allocated, an
+/// array's length can never be resized. An array's elements are mutable or
+/// constant, depending on the array's type. This determines whether any array
+/// element can be assigned a new value or not. Each element is either an
+/// unpacked [`Val`][crate::Val] or a packed 8-/16-bit integer. Array elements
+/// are dynamically accessed via indexing; out-of-bounds accesses result in
+/// traps.
+///
+/// Like all WebAssembly references, these are opaque and unforgeable to Wasm:
+/// they cannot be faked and Wasm cannot, for example, cast the integer
+/// `0x12345678` into a reference, pretend it is a valid `arrayref`, and trick
+/// the host into dereferencing it and segfaulting or worse.
+///
+/// Note that you can also use `Rooted<ArrayRef>` and `ManuallyRooted<ArrayRef>`
+/// as a type parameter with [`Func::typed`][crate::Func::typed]- and
+/// [`Func::wrap`][crate::Func::wrap]-style APIs.
+///
+/// # Example
+///
+/// ```
+/// use wasmtime::*;
+///
+/// # fn foo() -> Result<()> {
+/// let mut config = Config::new();
+/// config.wasm_function_references(true);
+/// config.wasm_gc(true);
+///
+/// let engine = Engine::new(&config)?;
+/// let mut store = Store::new(&engine, ());
+///
+/// // Define the type for an array of `i32`s.
+/// let array_ty = ArrayType::new(
+///    store.engine(),
+///    FieldType::new(Mutability::Var, ValType::I32.into()),
+/// );
+///
+/// // Create an allocator for the array type.
+/// let allocator = ArrayRefPre::new(&mut store, array_ty);
+///
+/// {
+///     let mut scope = RootScope::new(&mut store);
+///
+///     // Allocate an instance of the array type.
+///     let len = 36;
+///     let elem = Val::I32(42);
+///     let my_array = match ArrayRef::new(&mut scope, &allocator, &elem, len) {
+///         Ok(s) => s,
+///
+///         // If the heap is out of memory, then do a GC to free up some space
+///         // and try again.
+///         Err(e) if e.is::<GcHeapOutOfMemory<()>>() => {
+///             // Do a GC! Note: in an async context, you'd want to do
+///             // `scope.as_context_mut().gc_async().await`.
+///             scope.as_context_mut().gc();
+///
+///             // Try again. If the GC heap is still out of memory, then we
+///             // weren't able to free up resources for this allocation, so
+///             // propagate the error.
+///             ArrayRef::new(&mut scope, &allocator, &elem, len)?
+///         }
+///
+///         // Propagate any other kind of error.
+///         Err(e) => return Err(e),
+///     };
+///
+///     // That instance's elements should have the initial value.
+///     for i in 0..len {
+///         let val = my_array.get(&mut scope, i)?.unwrap_i32();
+///         assert_eq!(val, 42);
+///     }
+///
+///     // We can set an element to a new value because the type was defined with
+///     // mutable elements (as opposed to const).
+///     my_array.set(&mut scope, 3, Val::I32(1234))?;
+///     let new_val = my_array.get(&mut scope, 3)?.unwrap_i32();
+///     assert_eq!(new_val, 1234);
+/// }
+/// # Ok(())
+/// # }
+/// # foo().unwrap();
+/// ```
+#[derive(Debug)]
+#[repr(transparent)]
+pub struct ArrayRef {
+    pub(super) inner: GcRootIndex,
+}
+
+unsafe impl GcRefImpl for ArrayRef {
+    #[allow(private_interfaces)]
+    fn transmute_ref(index: &GcRootIndex) -> &Self {
+        // Safety: `ArrayRef` is a newtype of a `GcRootIndex`.
+        let me: &Self = unsafe { mem::transmute(index) };
+
+        // Assert we really are just a newtype of a `GcRootIndex`.
+        assert!(matches!(
+            me,
+            Self {
+                inner: GcRootIndex { .. },
+            }
+        ));
+
+        me
+    }
+}
+
+impl Rooted<ArrayRef> {
+    /// Upcast this `arrayref` into an `anyref`.
+    #[inline]
+    pub fn to_anyref(self) -> Rooted<AnyRef> {
+        self.unchecked_cast()
+    }
+}
+
+impl ManuallyRooted<ArrayRef> {
+    /// Upcast this `arrayref` into an `anyref`.
+    #[inline]
+    pub fn to_anyref(self) -> ManuallyRooted<AnyRef> {
+        self.unchecked_cast()
+    }
+}
+
+impl ArrayRef {
+    /// Allocate a new `array` of the given length, with every element
+    /// initialized to `elem`.
+    ///
+    /// For example, `ArrayRef::new(ctx, pre, &Val::I64(9), 3)` allocates the
+    /// array `[9, 9, 9]`.
+    ///
+    /// This is similar to the `array.new` instruction.
+    ///
+    /// # Errors
+    ///
+    /// If the given `elem` value's type does not match the `allocator`'s array
+    /// type's element type, an error is returned.
+    ///
+    /// If the allocation cannot be satisfied because the GC heap is currently
+    /// out of memory, but performing a garbage collection might free up space
+    /// such that retrying the allocation afterwards might succeed, then a
+    /// [`GcHeapOutOfMemory<()>`][crate::GcHeapOutOfMemory] error is returned.
+    ///
+    /// # Panics
+    ///
+    /// Panics if either the allocator or the `elem` value is not associated
+    /// with the given store.
+    pub fn new(
+        mut store: impl AsContextMut,
+        allocator: &ArrayRefPre,
+        elem: &Val,
+        len: u32,
+    ) -> Result<Rooted<ArrayRef>> {
+        Self::_new(store.as_context_mut().0, allocator, elem, len)
+    }
+
+    pub(crate) fn _new(
+        store: &mut StoreOpaque,
+        allocator: &ArrayRefPre,
+        elem: &Val,
+        len: u32,
+    ) -> Result<Rooted<ArrayRef>> {
+        assert_eq!(
+            store.id(),
+            allocator.store_id,
+            "attempted to use a `ArrayRefPre` with the wrong store"
+        );
+
+        // Type check the initial element value against the element type.
+        elem.ensure_matches_ty(store, allocator.ty.element_type().unpack())
+            .context("element type mismatch")?;
+
+        return Self::_new_unchecked(store, allocator, RepeatN(elem, len));
+
+        // NB: Can't use `iter::repeat(elem).take(len)` above because that
+        // doesn't implement `ExactSizeIterator`.
+        struct RepeatN<'a>(&'a Val, u32);
+
+        impl<'a> Iterator for RepeatN<'a> {
+            type Item = &'a Val;
+
+            fn next(&mut self) -> Option<Self::Item> {
+                if self.1 == 0 {
+                    None
+                } else {
+                    self.1 -= 1;
+                    Some(self.0)
+                }
+            }
+
+            fn size_hint(&self) -> (usize, Option<usize>) {
+                let len = self.len();
+                (len, Some(len))
+            }
+        }
+
+        impl ExactSizeIterator for RepeatN<'_> {
+            fn len(&self) -> usize {
+                usize::try_from(self.1).unwrap()
+            }
+        }
+    }
+
+    /// Allocate a new array of the given elements, without checking that the
+    /// elements' types match the array's element type.
+    fn _new_unchecked<'a>(
+        store: &mut StoreOpaque,
+        allocator: &ArrayRefPre,
+        elems: impl ExactSizeIterator<Item = &'a Val>,
+    ) -> Result<Rooted<ArrayRef>> {
+        let len = u32::try_from(elems.len()).unwrap();
+
+        // Allocate the array and write each field value into the appropriate
+        // offset.
+        let arrayref = store
+            .gc_store_mut()?
+            .alloc_uninit_array(allocator.type_index(), len, allocator.layout())
+            .err2anyhow()
+            .context("unrecoverable error when allocating new `arrayref`")?
+            .ok_or_else(|| GcHeapOutOfMemory::new(()))
+            .err2anyhow()?;
+
+        // From this point on, if we get any errors, then the array is not
+        // fully initialized, so we need to eagerly deallocate it before the
+        // next GC where the collector might try to interpret one of the
+        // uninitialized fields as a GC reference.
+        let mut store = AutoAssertNoGc::new(store);
+        match (|| {
+            let elem_ty = allocator.ty.element_type();
+            for (i, elem) in elems.enumerate() {
+                let i = u32::try_from(i).unwrap();
+                debug_assert!(i < len);
+                arrayref.initialize_elem(&mut store, allocator.layout(), &elem_ty, i, *elem)?;
+            }
+            Ok(())
+        })() {
+            Ok(()) => Ok(Rooted::new(&mut store, arrayref.into())),
+            Err(e) => {
+                store.gc_store_mut()?.dealloc_uninit_array(arrayref);
+                Err(e)
+            }
+        }
+    }
+
+    /// Allocate a new `array` containing the given elements.
+    ///
+    /// For example, `ArrayRef::new_fixed(ctx, pre, &[Val::I64(4), Val::I64(5),
+    /// Val::I64(6)])` allocates the array `[4, 5, 6]`.
+    ///
+    /// This is similar to the `array.new_fixed` instruction.
+    ///
+    /// # Errors
+    ///
+    /// If any of the `elems` values' type does not match the `allocator`'s
+    /// array type's element type, an error is returned.
+    ///
+    /// If the allocation cannot be satisfied because the GC heap is currently
+    /// out of memory, but performing a garbage collection might free up space
+    /// such that retrying the allocation afterwards might succeed, then a
+    /// [`GcHeapOutOfMemory<()>`][crate::GcHeapOutOfMemory] error is returned.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the allocator or any of the `elems` values are not associated
+    /// with the given store.
+    pub fn new_fixed(
+        mut store: impl AsContextMut,
+        allocator: &ArrayRefPre,
+        elems: &[Val],
+    ) -> Result<Rooted<ArrayRef>> {
+        Self::_new_fixed(store.as_context_mut().0, allocator, elems)
+    }
+
+    pub(crate) fn _new_fixed(
+        store: &mut StoreOpaque,
+        allocator: &ArrayRefPre,
+        elems: &[Val],
+    ) -> Result<Rooted<ArrayRef>> {
+        assert_eq!(
+            store.id(),
+            allocator.store_id,
+            "attempted to use a `ArrayRefPre` with the wrong store"
+        );
+
+        // Type check the elements against the element type.
+        for elem in elems {
+            elem.ensure_matches_ty(store, allocator.ty.element_type().unpack())
+                .context("element type mismatch")?;
+        }
+
+        return Self::_new_unchecked(store, allocator, elems.iter());
+    }
+
+    #[inline]
+    pub(crate) fn comes_from_same_store(&self, store: &StoreOpaque) -> bool {
+        self.inner.comes_from_same_store(store)
+    }
+
+    /// Get this `arrayref`'s type.
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store.
+    pub fn ty(&self, store: impl AsContext) -> Result<ArrayType> {
+        self._ty(store.as_context().0)
+    }
+
+    pub(crate) fn _ty(&self, store: &StoreOpaque) -> Result<ArrayType> {
+        assert!(self.comes_from_same_store(store));
+        let index = self.type_index(store)?;
+        Ok(ArrayType::from_shared_type_index(store.engine(), index))
+    }
+
+    /// Does this `arrayref` match the given type?
+    ///
+    /// That is, is this array's type a subtype of the given type?
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store or if the
+    /// type is not associated with the store's engine.
+    pub fn matches_ty(&self, store: impl AsContext, ty: &ArrayType) -> Result<bool> {
+        self._matches_ty(store.as_context().0, ty)
+    }
+
+    pub(crate) fn _matches_ty(&self, store: &StoreOpaque, ty: &ArrayType) -> Result<bool> {
+        assert!(self.comes_from_same_store(store));
+        Ok(self._ty(store)?.matches(ty))
+    }
+
+    pub(crate) fn ensure_matches_ty(&self, store: &StoreOpaque, ty: &ArrayType) -> Result<()> {
+        if !self.comes_from_same_store(store) {
+            bail!("function used with wrong store");
+        }
+        if self._matches_ty(store, ty)? {
+            Ok(())
+        } else {
+            let actual_ty = self._ty(store)?;
+            bail!("type mismatch: expected `(ref {ty})`, found `(ref {actual_ty})`")
+        }
+    }
+
+    /// Get the length of this array.
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store.
+    pub fn len(&self, store: impl AsContext) -> Result<u32> {
+        self._len(store.as_context().0)
+    }
+
+    pub(crate) fn _len(&self, store: &StoreOpaque) -> Result<u32> {
+        assert!(self.comes_from_same_store(store));
+        let gc_ref = self.inner.try_gc_ref(store)?;
+        debug_assert!({
+            let header = store.gc_store()?.header(gc_ref);
+            header.kind().matches(VMGcKind::ArrayRef)
+        });
+        let arrayref = gc_ref.as_arrayref_unchecked();
+        Ok(arrayref.len(store))
+    }
+
+    /// Get the values of this array's elements.
+    ///
+    /// Note that `i8` and `i16` field values are zero-extended into
+    /// `Val::I32(_)`s.
+    ///
+    /// # Errors
+    ///
+    /// Return an error if this reference has been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store.
+    pub fn elems<'a, T: 'a>(
+        &'a self,
+        store: impl Into<StoreContextMut<'a, T>>,
+    ) -> Result<impl ExactSizeIterator<Item = Val> + 'a> {
+        self._elems(store.into().0)
+    }
+
+    pub(crate) fn _elems<'a>(
+        &'a self,
+        store: &'a mut StoreOpaque,
+    ) -> Result<impl ExactSizeIterator<Item = Val> + 'a> {
+        assert!(self.comes_from_same_store(store));
+        let store = AutoAssertNoGc::new(store);
+
+        let gc_ref = self.inner.try_gc_ref(&store)?;
+        let header = store.gc_store()?.header(gc_ref);
+        debug_assert!(header.kind().matches(VMGcKind::ArrayRef));
+
+        let len = self._len(&store)?;
+
+        return Ok(Elems {
+            arrayref: self,
+            store,
+            index: 0,
+            len,
+        });
+
+        struct Elems<'a, 'b> {
+            arrayref: &'a ArrayRef,
+            store: AutoAssertNoGc<'b>,
+            index: u32,
+            len: u32,
+        }
+
+        impl Iterator for Elems<'_, '_> {
+            type Item = Val;
+
+            #[inline]
+            fn next(&mut self) -> Option<Self::Item> {
+                let i = self.index;
+                debug_assert!(i <= self.len);
+                if i >= self.len {
+                    return None;
+                }
+                self.index += 1;
+                Some(self.arrayref._get(&mut self.store, i).unwrap())
+            }
+
+            #[inline]
+            fn size_hint(&self) -> (usize, Option<usize>) {
+                let len = self.len - self.index;
+                let len = usize::try_from(len).unwrap();
+                (len, Some(len))
+            }
+        }
+
+        impl ExactSizeIterator for Elems<'_, '_> {
+            #[inline]
+            fn len(&self) -> usize {
+                let len = self.len - self.index;
+                usize::try_from(len).unwrap()
+            }
+        }
+    }
+
+    fn header<'a>(&self, store: &'a AutoAssertNoGc<'_>) -> Result<&'a VMGcHeader> {
+        assert!(self.comes_from_same_store(&store));
+        let gc_ref = self.inner.try_gc_ref(store)?;
+        Ok(store.gc_store()?.header(gc_ref))
+    }
+
+    fn arrayref<'a>(&self, store: &'a AutoAssertNoGc<'_>) -> Result<&'a VMArrayRef> {
+        assert!(self.comes_from_same_store(&store));
+        let gc_ref = self.inner.try_gc_ref(store)?;
+        debug_assert!(self.header(store)?.kind().matches(VMGcKind::ArrayRef));
+        Ok(gc_ref.as_arrayref_unchecked())
+    }
+
+    fn layout(&self, store: &AutoAssertNoGc<'_>) -> Result<GcArrayLayout> {
+        assert!(self.comes_from_same_store(&store));
+        let type_index = self.type_index(store)?;
+        let layout = store
+            .engine()
+            .signatures()
+            .layout(type_index)
+            .expect("array types should have GC layouts");
+        match layout {
+            GcLayout::Array(a) => Ok(a),
+            GcLayout::Struct(_) => unreachable!(),
+        }
+    }
+
+    fn field_ty(&self, store: &StoreOpaque) -> Result<FieldType> {
+        let ty = self._ty(store)?;
+        Ok(ty.field_type())
+    }
+
+    /// Get this array's `index`th element.
+    ///
+    /// Note that `i8` and `i16` field values are zero-extended into
+    /// `Val::I32(_)`s.
+    ///
+    /// # Errors
+    ///
+    /// Returns an `Err(_)` if the index is out of bounds or this reference has
+    /// been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this reference is associated with a different store.
+    pub fn get(&self, mut store: impl AsContextMut, index: u32) -> Result<Val> {
+        let mut store = AutoAssertNoGc::new(store.as_context_mut().0);
+        self._get(&mut store, index)
+    }
+
+    pub(crate) fn _get(&self, store: &mut AutoAssertNoGc<'_>, index: u32) -> Result<Val> {
+        assert!(
+            self.comes_from_same_store(store),
+            "attempted to use an array with the wrong store",
+        );
+        let arrayref = self.arrayref(store)?.unchecked_copy();
+        let field_ty = self.field_ty(store)?;
+        let layout = self.layout(store)?;
+        let len = arrayref.len(store);
+        ensure!(
+            index < len,
+            "index out of bounds: the length is {len} but the index is {index}"
+        );
+        Ok(arrayref.read_elem(store, &layout, field_ty.element_type(), index))
+    }
+
+    /// Set this array's `index`th element.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error in the following scenarios:
+    ///
+    /// * When given a value of the wrong type, such as trying to write an `f32`
+    ///   value into an array of `i64` elements.
+    ///
+    /// * When the array elements are not mutable.
+    ///
+    /// * When `index` is not within the range `0..self.len(ctx)`.
+    ///
+    /// * When `value` is a GC reference that has since been unrooted.
+    ///
+    /// # Panics
+    ///
+    /// Panics if either this reference or the given `value` is associated with
+    /// a different store.
+    pub fn set(&self, mut store: impl AsContextMut, index: u32, value: Val) -> Result<()> {
+        self._set(store.as_context_mut().0, index, value)
+    }
+
+    pub(crate) fn _set(&self, store: &mut StoreOpaque, index: u32, value: Val) -> Result<()> {
+        assert!(
+            self.comes_from_same_store(store),
+            "attempted to use an array with the wrong store",
+        );
+        assert!(
+            value.comes_from_same_store(store),
+            "attempted to use a value with the wrong store",
+        );
+
+        let mut store = AutoAssertNoGc::new(store);
+
+        let field_ty = self.field_ty(&store)?;
+        ensure!(
+            field_ty.mutability().is_var(),
+            "cannot set element {index}: array elements are not mutable"
+        );
+
+        value
+            .ensure_matches_ty(&store, &field_ty.element_type().unpack())
+            .with_context(|| format!("cannot set element {index}: type mismatch"))?;
+
+        let layout = self.layout(&store)?;
+        let arrayref = self.arrayref(&store)?.unchecked_copy();
+
+        let len = arrayref.len(&store);
+        ensure!(
+            index < len,
+            "index out of bounds: the length is {len} but the index is {index}"
+        );
+
+        arrayref.write_elem(&mut store, &layout, field_ty.element_type(), index, value)
+    }
+
+    pub(crate) fn type_index(&self, store: &StoreOpaque) -> Result<VMSharedTypeIndex> {
+        let gc_ref = self.inner.try_gc_ref(store)?;
+        let header = store.gc_store()?.header(gc_ref);
+        debug_assert!(header.kind().matches(VMGcKind::ArrayRef));
+        Ok(header.ty().expect("arrayrefs should have concrete types"))
+    }
+
+    /// Create a new `Rooted<ArrayRef>` from the given GC reference.
+    ///
+    /// `gc_ref` should point to a valid `arrayref` and should belong to the
+    /// store's GC heap. Failure to uphold these invariants is memory safe but
+    /// will lead to general incorrectness such as panics or wrong results.
+    pub(crate) fn from_cloned_gc_ref(
+        store: &mut AutoAssertNoGc<'_>,
+        gc_ref: VMGcRef,
+    ) -> Rooted<Self> {
+        debug_assert!(!gc_ref.is_i31());
+        Rooted::new(store, gc_ref)
+    }
+}
+
+unsafe impl WasmTy for Rooted<ArrayRef> {
+    #[inline]
+    fn valtype() -> ValType {
+        ValType::Ref(RefType::new(false, HeapType::Array))
+    }
+
+    #[inline]
+    fn compatible_with_store(&self, store: &StoreOpaque) -> bool {
+        self.comes_from_same_store(store)
+    }
+
+    #[inline]
+    fn dynamic_concrete_type_check(
+        &self,
+        store: &StoreOpaque,
+        _nullable: bool,
+        ty: &HeapType,
+    ) -> Result<()> {
+        match ty {
+            HeapType::Any | HeapType::Eq | HeapType::Array => Ok(()),
+            HeapType::ConcreteArray(ty) => self.ensure_matches_ty(store, ty),
+
+            HeapType::Extern
+            | HeapType::NoExtern
+            | HeapType::Func
+            | HeapType::ConcreteFunc(_)
+            | HeapType::NoFunc
+            | HeapType::I31
+            | HeapType::Struct
+            | HeapType::ConcreteStruct(_)
+            | HeapType::None => bail!(
+                "type mismatch: expected `(ref {ty})`, got `(ref {})`",
+                self._ty(store)?,
+            ),
+        }
+    }
+
+    fn store(self, store: &mut AutoAssertNoGc<'_>, ptr: &mut MaybeUninit<ValRaw>) -> Result<()> {
+        self.wasm_ty_store(store, ptr, ValRaw::anyref)
+    }
+
+    unsafe fn load(store: &mut AutoAssertNoGc<'_>, ptr: &ValRaw) -> Self {
+        Self::wasm_ty_load(store, ptr.get_anyref(), ArrayRef::from_cloned_gc_ref)
+    }
+}
+
+unsafe impl WasmTy for Option<Rooted<ArrayRef>> {
+    #[inline]
+    fn valtype() -> ValType {
+        ValType::ARRAYREF
+    }
+
+    #[inline]
+    fn compatible_with_store(&self, store: &StoreOpaque) -> bool {
+        self.map_or(true, |x| x.comes_from_same_store(store))
+    }
+
+    #[inline]
+    fn dynamic_concrete_type_check(
+        &self,
+        store: &StoreOpaque,
+        nullable: bool,
+        ty: &HeapType,
+    ) -> Result<()> {
+        match self {
+            Some(s) => Rooted::<ArrayRef>::dynamic_concrete_type_check(s, store, nullable, ty),
+            None => {
+                ensure!(
+                    nullable,
+                    "expected a non-null reference, but found a null reference"
+                );
+                Ok(())
+            }
+        }
+    }
+
+    #[inline]
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        self.is_some()
+    }
+
+    fn store(self, store: &mut AutoAssertNoGc<'_>, ptr: &mut MaybeUninit<ValRaw>) -> Result<()> {
+        <Rooted<ArrayRef>>::wasm_ty_option_store(self, store, ptr, ValRaw::anyref)
+    }
+
+    unsafe fn load(store: &mut AutoAssertNoGc<'_>, ptr: &ValRaw) -> Self {
+        <Rooted<ArrayRef>>::wasm_ty_option_load(
+            store,
+            ptr.get_anyref(),
+            ArrayRef::from_cloned_gc_ref,
+        )
+    }
+}
+
+unsafe impl WasmTy for ManuallyRooted<ArrayRef> {
+    #[inline]
+    fn valtype() -> ValType {
+        ValType::Ref(RefType::new(false, HeapType::Array))
+    }
+
+    #[inline]
+    fn compatible_with_store(&self, store: &StoreOpaque) -> bool {
+        self.comes_from_same_store(store)
+    }
+
+    #[inline]
+    fn dynamic_concrete_type_check(
+        &self,
+        store: &StoreOpaque,
+        _: bool,
+        ty: &HeapType,
+    ) -> Result<()> {
+        match ty {
+            HeapType::Any | HeapType::Eq | HeapType::Array => Ok(()),
+            HeapType::ConcreteArray(ty) => self.ensure_matches_ty(store, ty),
+
+            HeapType::Extern
+            | HeapType::NoExtern
+            | HeapType::Func
+            | HeapType::ConcreteFunc(_)
+            | HeapType::NoFunc
+            | HeapType::I31
+            | HeapType::Struct
+            | HeapType::ConcreteStruct(_)
+            | HeapType::None => bail!(
+                "type mismatch: expected `(ref {ty})`, got `(ref {})`",
+                self._ty(store)?,
+            ),
+        }
+    }
+
+    fn store(self, store: &mut AutoAssertNoGc<'_>, ptr: &mut MaybeUninit<ValRaw>) -> Result<()> {
+        self.wasm_ty_store(store, ptr, ValRaw::anyref)
+    }
+
+    unsafe fn load(store: &mut AutoAssertNoGc<'_>, ptr: &ValRaw) -> Self {
+        Self::wasm_ty_load(store, ptr.get_anyref(), ArrayRef::from_cloned_gc_ref)
+    }
+}
+
+unsafe impl WasmTy for Option<ManuallyRooted<ArrayRef>> {
+    #[inline]
+    fn valtype() -> ValType {
+        ValType::ARRAYREF
+    }
+
+    #[inline]
+    fn compatible_with_store(&self, store: &StoreOpaque) -> bool {
+        self.as_ref()
+            .map_or(true, |x| x.comes_from_same_store(store))
+    }
+
+    #[inline]
+    fn dynamic_concrete_type_check(
+        &self,
+        store: &StoreOpaque,
+        nullable: bool,
+        ty: &HeapType,
+    ) -> Result<()> {
+        match self {
+            Some(s) => {
+                ManuallyRooted::<ArrayRef>::dynamic_concrete_type_check(s, store, nullable, ty)
+            }
+            None => {
+                ensure!(
+                    nullable,
+                    "expected a non-null reference, but found a null reference"
+                );
+                Ok(())
+            }
+        }
+    }
+
+    #[inline]
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        self.is_some()
+    }
+
+    fn store(self, store: &mut AutoAssertNoGc<'_>, ptr: &mut MaybeUninit<ValRaw>) -> Result<()> {
+        <ManuallyRooted<ArrayRef>>::wasm_ty_option_store(self, store, ptr, ValRaw::anyref)
+    }
+
+    unsafe fn load(store: &mut AutoAssertNoGc<'_>, ptr: &ValRaw) -> Self {
+        <ManuallyRooted<ArrayRef>>::wasm_ty_option_load(
+            store,
+            ptr.get_anyref(),
+            ArrayRef::from_cloned_gc_ref,
+        )
+    }
+}
diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs
index 0efefef4e..356fe5610 100644
--- a/crates/wasmtime/src/runtime/store.rs
+++ b/crates/wasmtime/src/runtime/store.rs
@@ -1760,10 +1760,12 @@ impl StoreOpaque {
         log::trace!("End trace GC roots :: user");
     }
 
-    /// Insert a type into this store. This makes it suitable for the embedder
-    /// to allocate instances of this type in this store, and we don't have to
-    /// worry about the type being reclaimed (since it is possible that none of
-    /// the Wasm modules in this store are holding it alive).
+    /// Insert a host-allocated GC type into this store.
+    ///
+    /// This makes it suitable for the embedder to allocate instances of this
+    /// type in this store, and we don't have to worry about the type being
+    /// reclaimed (since it is possible that none of the Wasm modules in this
+    /// store are holding it alive).
     pub(crate) fn insert_gc_host_alloc_type(&mut self, ty: RegisteredType) {
         self.gc_host_alloc_types.insert(ty);
     }
diff --git a/crates/wasmtime/src/runtime/trampoline/global.rs b/crates/wasmtime/src/runtime/trampoline/global.rs
index 4896ffb89..80578f8c5 100644
--- a/crates/wasmtime/src/runtime/trampoline/global.rs
+++ b/crates/wasmtime/src/runtime/trampoline/global.rs
@@ -42,6 +42,7 @@ pub fn generate_global_export(
             Val::ExternRef(x) => {
                 let new = match x {
                     None => None,
+                    #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
                     Some(x) => Some(x.try_gc_ref(&mut store)?.unchecked_copy()),
                 };
                 let new = new.as_ref();
@@ -50,6 +51,7 @@ pub fn generate_global_export(
             Val::AnyRef(a) => {
                 let new = match a {
                     None => None,
+                    #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
                     Some(a) => Some(a.try_gc_ref(&mut store)?.unchecked_copy()),
                 };
                 let new = new.as_ref();
diff --git a/crates/wasmtime/src/runtime/types.rs b/crates/wasmtime/src/runtime/types.rs
index 321cb7199..ecf1865a2 100644
--- a/crates/wasmtime/src/runtime/types.rs
+++ b/crates/wasmtime/src/runtime/types.rs
@@ -142,6 +142,9 @@ impl ValType {
     /// The `i31ref` type, aka `(ref null i31)`.
     pub const I31REF: Self = ValType::Ref(RefType::I31REF);
 
+    /// The `arrayref` type, aka `(ref null array)`.
+    pub const ARRAYREF: Self = ValType::Ref(RefType::ARRAYREF);
+
     /// The `structref` type, aka `(ref null struct)`.
     pub const STRUCTREF: Self = ValType::Ref(RefType::STRUCTREF);
 
@@ -335,6 +338,19 @@ impl ValType {
             WasmValType::Ref(r) => Self::Ref(RefType::from_wasm_type(engine, r)),
         }
     }
+
+    /// What is the size (in bytes) of this type's values when they are stored
+    /// inside the GC heap?
+    pub(crate) fn byte_size_in_gc_heap(&self) -> u32 {
+        match self {
+            ValType::I32 => 4,
+            ValType::I64 => 8,
+            ValType::F32 => 4,
+            ValType::F64 => 8,
+            ValType::V128 => 16,
+            ValType::Ref(r) => r.byte_size_in_gc_heap(),
+        }
+    }
 }
 
 /// Opaque references to data in the Wasm heap or to host data.
@@ -407,6 +423,12 @@ impl RefType {
         heap_type: HeapType::I31,
     };
 
+    /// The `arrayref` type, aka `(ref null array)`.
+    pub const ARRAYREF: Self = RefType {
+        is_nullable: true,
+        heap_type: HeapType::Array,
+    };
+
     /// The `structref` type, aka `(ref null struct)`.
     pub const STRUCTREF: Self = RefType {
         is_nullable: true,
@@ -497,6 +519,26 @@ impl RefType {
     pub(crate) fn is_vmgcref_type_and_points_to_object(&self) -> bool {
         self.heap_type().is_vmgcref_type_and_points_to_object()
     }
+
+    /// What is the size (in bytes) of this type's values when they are stored
+    /// inside the GC heap?
+    pub(crate) fn byte_size_in_gc_heap(&self) -> u32 {
+        match &self.heap_type {
+            HeapType::Extern | HeapType::NoExtern => 4,
+
+            HeapType::Func | HeapType::ConcreteFunc(_) | HeapType::NoFunc => {
+                todo!("funcrefs in the gc heap aren't supported yet")
+            }
+            HeapType::Any
+            | HeapType::Eq
+            | HeapType::I31
+            | HeapType::Array
+            | HeapType::ConcreteArray(_)
+            | HeapType::Struct
+            | HeapType::ConcreteStruct(_)
+            | HeapType::None => 4,
+        }
+    }
 }
 
 /// The heap types that can Wasm can have references to.
@@ -1339,6 +1381,16 @@ impl StorageType {
         a.matches(b) && b.matches(a)
     }
 
+    /// What is the size (in bytes) of this type's values when they are stored
+    /// inside the GC heap?
+    pub(crate) fn byte_size_in_gc_heap(&self) -> u32 {
+        match self {
+            StorageType::I8 => 1,
+            StorageType::I16 => 2,
+            StorageType::ValType(ty) => ty.byte_size_in_gc_heap(),
+        }
+    }
+
     pub(crate) fn comes_from_same_engine(&self, engine: &Engine) -> bool {
         match self {
             StorageType::I8 | StorageType::I16 => true,
@@ -1751,6 +1803,14 @@ pub struct ArrayType {
     registered_type: RegisteredType,
 }
 
+impl fmt::Display for ArrayType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let field_ty = self.field_type();
+        write!(f, "(array (field {field_ty}))")?;
+        Ok(())
+    }
+}
+
 impl ArrayType {
     /// Construct a new `ArrayType` with the given field type's mutability and
     /// storage type.
@@ -1911,6 +1971,10 @@ impl ArrayType {
         Engine::same(self.registered_type.engine(), engine)
     }
 
+    pub(crate) fn registered_type(&self) -> &RegisteredType {
+        &self.registered_type
+    }
+
     pub(crate) fn type_index(&self) -> VMSharedTypeIndex {
         self.registered_type.index()
     }
diff --git a/crates/wasmtime/src/runtime/values.rs b/crates/wasmtime/src/runtime/values.rs
index 9d1c3bd3e..b537ce1cb 100644
--- a/crates/wasmtime/src/runtime/values.rs
+++ b/crates/wasmtime/src/runtime/values.rs
@@ -1,9 +1,8 @@
 use crate::runtime::vm::TableElement;
 use crate::store::{AutoAssertNoGc, StoreOpaque};
-use crate::{prelude::*, StructRef};
 use crate::{
-    AnyRef, AsContext, AsContextMut, ExternRef, Func, HeapType, RefType, Rooted, RootedGcRefImpl,
-    ValType, V128,
+    prelude::*, AnyRef, ArrayRef, AsContext, AsContextMut, ExternRef, Func, HeapType, RefType,
+    Rooted, RootedGcRefImpl, StructRef, ValType, V128,
 };
 use core::ptr;
 
@@ -483,14 +482,28 @@ impl From<Option<Rooted<AnyRef>>> for Val {
 impl From<Rooted<StructRef>> for Val {
     #[inline]
     fn from(val: Rooted<StructRef>) -> Val {
-        Val::AnyRef(Some(val.unchecked_cast()))
+        Val::AnyRef(Some(val.into()))
     }
 }
 
 impl From<Option<Rooted<StructRef>>> for Val {
     #[inline]
     fn from(val: Option<Rooted<StructRef>>) -> Val {
-        Val::AnyRef(val.map(|s| s.unchecked_cast()))
+        Val::AnyRef(val.map(Into::into))
+    }
+}
+
+impl From<Rooted<ArrayRef>> for Val {
+    #[inline]
+    fn from(val: Rooted<ArrayRef>) -> Val {
+        Val::AnyRef(Some(val.into()))
+    }
+}
+
+impl From<Option<Rooted<ArrayRef>>> for Val {
+    #[inline]
+    fn from(val: Option<Rooted<ArrayRef>>) -> Val {
+        Val::AnyRef(val.map(Into::into))
     }
 }
 
@@ -653,14 +666,28 @@ impl From<Option<Rooted<AnyRef>>> for Ref {
 impl From<Rooted<StructRef>> for Ref {
     #[inline]
     fn from(e: Rooted<StructRef>) -> Ref {
-        Ref::Any(Some(e.unchecked_cast::<AnyRef>()))
+        Ref::Any(Some(e.into()))
     }
 }
 
 impl From<Option<Rooted<StructRef>>> for Ref {
     #[inline]
     fn from(e: Option<Rooted<StructRef>>) -> Ref {
-        Ref::Any(e.map(|e| e.unchecked_cast::<AnyRef>()))
+        Ref::Any(e.map(Into::into))
+    }
+}
+
+impl From<Rooted<ArrayRef>> for Ref {
+    #[inline]
+    fn from(e: Rooted<ArrayRef>) -> Ref {
+        Ref::Any(Some(e.into()))
+    }
+}
+
+impl From<Option<Rooted<ArrayRef>>> for Ref {
+    #[inline]
+    fn from(e: Option<Rooted<ArrayRef>>) -> Ref {
+        Ref::Any(e.map(Into::into))
     }
 }
 
@@ -860,8 +887,12 @@ impl Ref {
                 Some(s) => s._matches_ty(store, _ty)?,
             },
             (Ref::Any(Some(_)), HeapType::Eq) => todo!("eqref"),
-            (Ref::Any(Some(_)), HeapType::Array) => todo!("wasm GC arrays"),
-            (Ref::Any(Some(_)), HeapType::ConcreteArray(_)) => todo!("wasm GC arrays"),
+            (Ref::Any(Some(a)), HeapType::Array) => a._is_array(store)?,
+            (Ref::Any(Some(a)), HeapType::ConcreteArray(_ty)) => match a._as_array(store)? {
+                None => false,
+                #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
+                Some(a) => a._matches_ty(store, _ty)?,
+            },
             (
                 Ref::Any(None),
                 HeapType::None
@@ -928,6 +959,7 @@ impl Ref {
                     assert!(ty.is_nullable());
                     Ok(TableElement::GcRef(None))
                 }
+                #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
                 Some(e) => {
                     let gc_ref = e.try_clone_gc_ref(&mut store)?;
                     Ok(TableElement::GcRef(Some(gc_ref)))
@@ -939,6 +971,7 @@ impl Ref {
                     assert!(ty.is_nullable());
                     Ok(TableElement::GcRef(None))
                 }
+                #[cfg_attr(not(feature = "gc"), allow(unreachable_patterns))]
                 Some(a) => {
                     let gc_ref = a.try_clone_gc_ref(&mut store)?;
                     Ok(TableElement::GcRef(Some(gc_ref)))
diff --git a/crates/wasmtime/src/runtime/vm/gc.rs b/crates/wasmtime/src/runtime/vm/gc.rs
index d714c1337..9593ac260 100644
--- a/crates/wasmtime/src/runtime/vm/gc.rs
+++ b/crates/wasmtime/src/runtime/vm/gc.rs
@@ -198,16 +198,36 @@ impl GcStore {
         self.gc_heap.dealloc_uninit_struct(structref);
     }
 
-    /// Get the data for the given struct reference.
+    /// Get the data for the given object reference.
     ///
     /// Panics when the structref and its size is out of the GC heap bounds.
+    pub fn gc_object_data(&mut self, gc_ref: &VMGcRef) -> VMGcObjectDataMut<'_> {
+        self.gc_heap.gc_object_data(gc_ref)
+    }
+
+    /// Allocate an uninitialized array with the given type index.
     ///
-    /// This does NOT check for mismatches where the referenced struct is not
-    /// actually `size` bytes large. Failure to pass the right `size` is memory
-    /// safe, but will lead to general incorrectness such as panics and wrong
-    /// results.
-    pub fn struct_data(&mut self, structref: &VMStructRef, size: u32) -> VMStructDataMut<'_> {
-        self.gc_heap.struct_data(structref, size)
+    /// This does NOT check that the index is currently allocated in the types
+    /// registry or that the layout matches the index's type. Failure to uphold
+    /// those invariants is memory safe, but will lead to general incorrectness
+    /// such as panics and wrong results.
+    pub fn alloc_uninit_array(
+        &mut self,
+        ty: VMSharedTypeIndex,
+        len: u32,
+        layout: &GcArrayLayout,
+    ) -> Result<Option<VMArrayRef>> {
+        self.gc_heap.alloc_uninit_array(ty, len, layout)
+    }
+
+    /// Deallocate an uninitialized array.
+    pub fn dealloc_uninit_array(&mut self, arrayref: VMArrayRef) {
+        self.gc_heap.dealloc_uninit_array(arrayref);
+    }
+
+    /// Get the length of the given array.
+    pub fn array_len(&self, arrayref: &VMArrayRef) -> u32 {
+        self.gc_heap.array_len(arrayref)
     }
 }
 
@@ -272,7 +292,24 @@ pub fn disabled_gc_heap() -> Box<dyn GcHeap> {
         fn dealloc_uninit_struct(&mut self, _structref: VMStructRef) {
             unreachable!()
         }
-        fn struct_data(&mut self, _structref: &VMStructRef, _size: u32) -> VMStructDataMut<'_> {
+        fn gc_object_data(&mut self, _gc_ref: &VMGcRef) -> VMGcObjectDataMut<'_> {
+            unreachable!()
+        }
+        fn alloc_uninit_array(
+            &mut self,
+            _ty: VMSharedTypeIndex,
+            _len: u32,
+            _layout: &GcArrayLayout,
+        ) -> Result<Option<VMArrayRef>> {
+            bail!(
+                "GC support disabled either in the `Config` or at compile time \
+                 because the `gc` cargo feature was not enabled"
+            )
+        }
+        fn dealloc_uninit_array(&mut self, _structref: VMArrayRef) {
+            unreachable!()
+        }
+        fn array_len(&self, _arrayref: &VMArrayRef) -> u32 {
             unreachable!()
         }
         fn gc<'a>(
diff --git a/crates/wasmtime/src/runtime/vm/gc/disabled.rs b/crates/wasmtime/src/runtime/vm/gc/disabled.rs
index fd777cc6e..24aa269ff 100644
--- a/crates/wasmtime/src/runtime/vm/gc/disabled.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/disabled.rs
@@ -33,7 +33,9 @@ pub enum VMExternRef {}
 
 pub enum VMStructRef {}
 
-pub struct VMStructDataMut<'a> {
+pub enum VMArrayRef {}
+
+pub struct VMGcObjectDataMut<'a> {
     inner: VMStructRef,
     _phantom: core::marker::PhantomData<&'a mut ()>,
 }
diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled.rs b/crates/wasmtime/src/runtime/vm/gc/enabled.rs
index d91e8fd7f..200d081c7 100644
--- a/crates/wasmtime/src/runtime/vm/gc/enabled.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/enabled.rs
@@ -1,10 +1,14 @@
 //! Implementation of garbage collection and GC types in Wasmtime.
 
+mod arrayref;
+mod data;
 mod drc;
 mod externref;
 mod free_list;
 mod structref;
 
+pub use arrayref::*;
+pub use data::*;
 pub use drc::*;
 pub use externref::*;
 pub use structref::*;
diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs b/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs
new file mode 100644
index 000000000..d6d192226
--- /dev/null
+++ b/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs
@@ -0,0 +1,342 @@
+use crate::{
+    prelude::*,
+    runtime::vm::{GcHeap, GcStore, VMGcRef},
+    store::{AutoAssertNoGc, StoreOpaque},
+    vm::GcArrayLayout,
+    AnyRef, ExternRef, HeapType, RootedGcRefImpl, StorageType, Val, ValType,
+};
+use core::fmt;
+use wasmtime_environ::VMGcKind;
+
+/// A `VMGcRef` that we know points to a `array`.
+///
+/// Create a `VMArrayRef` via `VMGcRef::into_arrayref` and
+/// `VMGcRef::as_arrayref`, or their untyped equivalents
+/// `VMGcRef::into_arrayref_unchecked` and `VMGcRef::as_arrayref_unchecked`.
+///
+/// Note: This is not a `TypedGcRef<_>` because each collector can have a
+/// different concrete representation of `arrayref` that they allocate inside
+/// their heaps.
+#[derive(Debug, PartialEq, Eq, Hash)]
+#[repr(transparent)]
+pub struct VMArrayRef(VMGcRef);
+
+impl fmt::Pointer for VMArrayRef {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Pointer::fmt(&self.0, f)
+    }
+}
+
+impl From<VMArrayRef> for VMGcRef {
+    #[inline]
+    fn from(x: VMArrayRef) -> Self {
+        x.0
+    }
+}
+
+impl VMGcRef {
+    /// Is this `VMGcRef` pointing to a `array`?
+    pub fn is_arrayref(&self, gc_heap: &(impl GcHeap + ?Sized)) -> bool {
+        if self.is_i31() {
+            return false;
+        }
+
+        let header = gc_heap.header(&self);
+        header.kind().matches(VMGcKind::ArrayRef)
+    }
+
+    /// Create a new `VMArrayRef` from the given `gc_ref`.
+    ///
+    /// If this is not a GC reference to an `arrayref`, `Err(self)` is
+    /// returned.
+    pub fn into_arrayref(self, gc_heap: &impl GcHeap) -> Result<VMArrayRef, VMGcRef> {
+        if self.is_arrayref(gc_heap) {
+            Ok(self.into_arrayref_unchecked())
+        } else {
+            Err(self)
+        }
+    }
+
+    /// Create a new `VMArrayRef` from `self` without actually checking that
+    /// `self` is an `arrayref`.
+    ///
+    /// This method does not check that `self` is actually an `arrayref`, but
+    /// it should be. Failure to uphold this invariant is memory safe but will
+    /// result in general incorrectness down the line such as panics or wrong
+    /// results.
+    #[inline]
+    pub fn into_arrayref_unchecked(self) -> VMArrayRef {
+        debug_assert!(!self.is_i31());
+        VMArrayRef(self)
+    }
+
+    /// Get this GC reference as an `arrayref` reference, if it actually is an
+    /// `arrayref` reference.
+    pub fn as_arrayref(&self, gc_heap: &(impl GcHeap + ?Sized)) -> Option<&VMArrayRef> {
+        if self.is_arrayref(gc_heap) {
+            Some(self.as_arrayref_unchecked())
+        } else {
+            None
+        }
+    }
+
+    /// Get this GC reference as an `arrayref` reference without checking if it
+    /// actually is an `arrayref` reference.
+    ///
+    /// Calling this method on a non-`arrayref` reference is memory safe, but
+    /// will lead to general incorrectness like panics and wrong results.
+    pub fn as_arrayref_unchecked(&self) -> &VMArrayRef {
+        debug_assert!(!self.is_i31());
+        let ptr = self as *const VMGcRef;
+        let ret = unsafe { &*ptr.cast() };
+        assert!(matches!(ret, VMArrayRef(VMGcRef { .. })));
+        ret
+    }
+}
+
+impl VMArrayRef {
+    /// Get the underlying `VMGcRef`.
+    pub fn as_gc_ref(&self) -> &VMGcRef {
+        &self.0
+    }
+
+    /// Clone this `VMArrayRef`, running any GC barriers as necessary.
+    pub fn clone(&self, gc_store: &mut GcStore) -> Self {
+        Self(gc_store.clone_gc_ref(&self.0))
+    }
+
+    /// Explicitly drop this `arrayref`, running GC drop barriers as necessary.
+    pub fn drop(self, gc_store: &mut GcStore) {
+        gc_store.drop_gc_ref(self.0);
+    }
+
+    /// Copy this `VMArrayRef` without running the GC's clone barriers.
+    ///
+    /// Prefer calling `clone(&mut GcStore)` instead! This is mostly an internal
+    /// escape hatch for collector implementations.
+    ///
+    /// Failure to run GC barriers when they would otherwise be necessary can
+    /// lead to leaks, panics, and wrong results. It cannot lead to memory
+    /// unsafety, however.
+    pub fn unchecked_copy(&self) -> Self {
+        Self(self.0.unchecked_copy())
+    }
+
+    /// Get the length of this array.
+    pub fn len(&self, store: &StoreOpaque) -> u32 {
+        store.unwrap_gc_store().array_len(self)
+    }
+
+    /// Read an element of the given `StorageType` into a `Val`.
+    ///
+    /// `i8` and `i16` fields are zero-extended into `Val::I32(_)`s.
+    ///
+    /// Does not check that this array's elements are actually of type
+    /// `ty`. That is the caller's responsibility. Failure to do so is memory
+    /// safe, but will lead to general incorrectness such as panics and wrong
+    /// results.
+    ///
+    /// Panics on out-of-bounds accesses.
+    pub fn read_elem(
+        &self,
+        store: &mut AutoAssertNoGc,
+        layout: &GcArrayLayout,
+        ty: &StorageType,
+        index: u32,
+    ) -> Val {
+        let offset = layout.elems_offset + index * ty.byte_size_in_gc_heap();
+        let data = store.unwrap_gc_store_mut().gc_object_data(self.as_gc_ref());
+        match ty {
+            StorageType::I8 => Val::I32(data.read_u8(offset).into()),
+            StorageType::I16 => Val::I32(data.read_u16(offset).into()),
+            StorageType::ValType(ValType::I32) => Val::I32(data.read_i32(offset)),
+            StorageType::ValType(ValType::I64) => Val::I64(data.read_i64(offset)),
+            StorageType::ValType(ValType::F32) => Val::F32(data.read_u32(offset)),
+            StorageType::ValType(ValType::F64) => Val::F64(data.read_u64(offset)),
+            StorageType::ValType(ValType::V128) => Val::V128(data.read_v128(offset)),
+            StorageType::ValType(ValType::Ref(r)) => match r.heap_type().top() {
+                HeapType::Extern => {
+                    let raw = data.read_u32(offset);
+                    Val::ExternRef(ExternRef::_from_raw(store, raw))
+                }
+                HeapType::Any => {
+                    let raw = data.read_u32(offset);
+                    Val::AnyRef(AnyRef::_from_raw(store, raw))
+                }
+                HeapType::Func => todo!("funcrefs inside gc objects not yet implemented"),
+                otherwise => unreachable!("not a top type: {otherwise:?}"),
+            },
+        }
+    }
+
+    /// Write the given value into this array at the given offset.
+    ///
+    /// Returns an error if `val` is a GC reference that has since been
+    /// unrooted.
+    ///
+    /// Does not check that `val` matches `ty`, nor that the field is actually
+    /// of type `ty`. Checking those things is the caller's responsibility.
+    /// Failure to do so is memory safe, but will lead to general incorrectness
+    /// such as panics and wrong results.
+    ///
+    /// Panics on out-of-bounds accesses.
+    pub fn write_elem(
+        &self,
+        store: &mut AutoAssertNoGc,
+        layout: &GcArrayLayout,
+        ty: &StorageType,
+        index: u32,
+        val: Val,
+    ) -> Result<()> {
+        debug_assert!(val._matches_ty(&store, &ty.unpack())?);
+
+        let offset = layout.elem_offset(index, ty.byte_size_in_gc_heap());
+        let mut data = store.unwrap_gc_store_mut().gc_object_data(self.as_gc_ref());
+        match val {
+            Val::I32(i) if ty.is_i8() => data.write_i8(offset, i as i8),
+            Val::I32(i) if ty.is_i16() => data.write_i16(offset, i as i16),
+            Val::I32(i) => data.write_i32(offset, i),
+            Val::I64(i) => data.write_i64(offset, i),
+            Val::F32(f) => data.write_u32(offset, f),
+            Val::F64(f) => data.write_u64(offset, f),
+            Val::V128(v) => data.write_v128(offset, v),
+
+            // For GC-managed references, we need to take care to run the
+            // appropriate barriers, even when we are writing null references
+            // into the array.
+            //
+            // POD-read the old value into a local copy, run the GC write
+            // barrier on that local copy, and then POD-write the updated
+            // value back into the array. This avoids transmuting the inner
+            // data, which would probably be fine, but this approach is
+            // Obviously Correct and should get us by for now. If LLVM isn't
+            // able to elide some of these unnecessary copies, and this
+            // method is ever hot enough, we can always come back and clean
+            // it up in the future.
+            Val::ExternRef(e) => {
+                let raw = data.read_u32(offset);
+                let mut gc_ref = VMGcRef::from_raw_u32(raw);
+                let e = match e {
+                    Some(e) => Some(e.try_gc_ref(store)?.unchecked_copy()),
+                    None => None,
+                };
+                store.gc_store_mut()?.write_gc_ref(&mut gc_ref, e.as_ref());
+                let mut data = store.gc_store_mut()?.gc_object_data(self.as_gc_ref());
+                data.write_u32(offset, gc_ref.map_or(0, |r| r.as_raw_u32()));
+            }
+            Val::AnyRef(a) => {
+                let raw = data.read_u32(offset);
+                let mut gc_ref = VMGcRef::from_raw_u32(raw);
+                let a = match a {
+                    Some(a) => Some(a.try_gc_ref(store)?.unchecked_copy()),
+                    None => None,
+                };
+                store.gc_store_mut()?.write_gc_ref(&mut gc_ref, a.as_ref());
+                let mut data = store.gc_store_mut()?.gc_object_data(self.as_gc_ref());
+                data.write_u32(offset, gc_ref.map_or(0, |r| r.as_raw_u32()));
+            }
+
+            Val::FuncRef(_) => todo!("funcrefs inside gc objects not yet implemented"),
+        }
+        Ok(())
+    }
+
+    /// Initialize an element in this arrayref that is currently uninitialized.
+    ///
+    /// The difference between this method and `write_elem` is that GC barriers
+    /// are handled differently. When overwriting an initialized element (aka
+    /// `write_elem`) we need to call the full write GC write barrier, which
+    /// logically drops the old GC reference and clones the new GC
+    /// reference. When we are initializing an element for the first time, there
+    /// is no old GC reference that is being overwritten and which we need to
+    /// drop, so we only need to clone the new GC reference.
+    ///
+    /// Calling this method on a arrayref that has already had the associated
+    /// element initialized will result in GC bugs. These are memory safe but
+    /// will lead to generally incorrect behavior such as panics, leaks, and
+    /// incorrect results.
+    ///
+    /// Does not check that `val` matches `ty`, nor that the field is actually
+    /// of type `ty`. Checking those things is the caller's responsibility.
+    /// Failure to do so is memory safe, but will lead to general incorrectness
+    /// such as panics and wrong results.
+    ///
+    /// Returns an error if `val` is a GC reference that has since been
+    /// unrooted.
+    ///
+    /// Panics on out-of-bounds accesses.
+    pub fn initialize_elem(
+        &self,
+        store: &mut AutoAssertNoGc,
+        layout: &GcArrayLayout,
+        ty: &StorageType,
+        index: u32,
+        val: Val,
+    ) -> Result<()> {
+        debug_assert!(val._matches_ty(&store, &ty.unpack())?);
+        let offset = layout.elem_offset(index, ty.byte_size_in_gc_heap());
+        match val {
+            Val::I32(i) if ty.is_i8() => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_i8(offset, i as i8),
+            Val::I32(i) if ty.is_i16() => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_i16(offset, i as i16),
+            Val::I32(i) => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_i32(offset, i),
+            Val::I64(i) => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_i64(offset, i),
+            Val::F32(f) => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_u32(offset, f),
+            Val::F64(f) => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_u64(offset, f),
+            Val::V128(v) => store
+                .gc_store_mut()?
+                .gc_object_data(self.as_gc_ref())
+                .write_v128(offset, v),
+
+            // NB: We don't need to do a write barrier when initializing a
+            // field, because there is nothing being overwritten. Therefore, we
+            // just the clone barrier.
+            Val::ExternRef(x) => {
+                let x = match x {
+                    None => 0,
+                    Some(x) => x.try_clone_gc_ref(store)?.as_raw_u32(),
+                };
+                store
+                    .gc_store_mut()?
+                    .gc_object_data(self.as_gc_ref())
+                    .write_u32(offset, x);
+            }
+            Val::AnyRef(x) => {
+                let x = match x {
+                    None => 0,
+                    Some(x) => x.try_clone_gc_ref(store)?.as_raw_u32(),
+                };
+                store
+                    .gc_store_mut()?
+                    .gc_object_data(self.as_gc_ref())
+                    .write_u32(offset, x);
+            }
+
+            Val::FuncRef(_) => {
+                // TODO: we can't trust the GC heap, which means we can't read
+                // native VMFuncRef pointers out of it and trust them. That
+                // means we need to do the same side table kind of thing we do
+                // with `externref` host data here. This isn't implemented yet.
+                todo!("funcrefs in GC objects")
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled/data.rs b/crates/wasmtime/src/runtime/vm/gc/enabled/data.rs
new file mode 100644
index 000000000..c806fbb4f
--- /dev/null
+++ b/crates/wasmtime/src/runtime/vm/gc/enabled/data.rs
@@ -0,0 +1,149 @@
+//! Unchecked methods for working with the data inside GC objects.
+
+use crate::V128;
+use core::mem;
+
+/// A plain-old-data type that can be stored in a `ValType` or a `StorageType`.
+///
+/// Safety: implementations must be POD and all bit patterns must be valid.
+pub trait PodValType<const SIZE: usize>: Copy {
+    /// Read an instance of `Self` from the given little-endian bytes.
+    fn read_le(le_bytes: &[u8; SIZE]) -> Self;
+
+    /// Write `self` into the given memory location, as little-endian bytes.
+    fn write_le(&self, into: &mut [u8; SIZE]);
+}
+
+macro_rules! impl_pod_val_type {
+    ( $( $t:ty , )* ) => {
+        $(
+            impl PodValType<{mem::size_of::<$t>()}> for $t {
+                fn read_le(le_bytes: &[u8; mem::size_of::<$t>()]) -> Self {
+                    <$t>::from_le_bytes(*le_bytes)
+                }
+                fn write_le(&self, into: &mut [u8; mem::size_of::<$t>()]) {
+                    *into = self.to_le_bytes();
+                }
+            }
+        )*
+    };
+}
+
+impl_pod_val_type! {
+    u8,
+    u16,
+    u32,
+    u64,
+    i8,
+    i16,
+    i32,
+    i64,
+}
+
+impl PodValType<{ mem::size_of::<V128>() }> for V128 {
+    fn read_le(le_bytes: &[u8; mem::size_of::<V128>()]) -> Self {
+        u128::from_le_bytes(*le_bytes).into()
+    }
+    fn write_le(&self, into: &mut [u8; mem::size_of::<V128>()]) {
+        *into = self.as_u128().to_le_bytes();
+    }
+}
+
+/// The backing storage for a GC-managed object.
+///
+/// Methods on this type do not, generally, check against things like type
+/// mismatches or that the given offset to read from even falls on a field
+/// boundary. Omitting these checks is memory safe, due to our untrusted,
+/// indexed GC heaps. Providing incorrect offsets will result in general
+/// incorrectness, such as wrong answers or even panics, however.
+///
+/// Finally, these methods *will* panic on out-of-bounds accesses, either out of
+/// the GC heap's bounds or out of this object's bounds. The former is necessary
+/// for preserving the memory safety of indexed GC heaps in the face of (for
+/// example) collector bugs, but the latter is just a defensive technique to
+/// catch bugs early and prevent action at a distance as much as possible.
+pub struct VMGcObjectDataMut<'a> {
+    data: &'a mut [u8],
+}
+
+macro_rules! impl_pod_methods {
+    ( $( $t:ty, $read:ident, $write:ident; )* ) => {
+        $(
+            /// Read a `
+            #[doc = stringify!($t)]
+            /// ` field this object.
+            ///
+            /// Panics on out-of-bounds accesses.
+            #[inline]
+            pub fn $read(&self, offset: u32) -> $t {
+                self.read_pod::<{ mem::size_of::<$t>() }, $t>(offset)
+            }
+
+            /// Write a `
+            #[doc = stringify!($t)]
+            /// ` into this object.
+            ///
+            /// Panics on out-of-bounds accesses.
+            #[inline]
+            pub fn $write(&mut self, offset: u32, val: $t) {
+                self.write_pod::<{ mem::size_of::<$t>() }, $t>(offset, val);
+            }
+        )*
+    };
+}
+
+impl<'a> VMGcObjectDataMut<'a> {
+    /// Construct a `VMStructDataMut` from the given slice of bytes.
+    #[inline]
+    pub fn new(data: &'a mut [u8]) -> Self {
+        Self { data }
+    }
+
+    /// Read a POD field out of this object.
+    ///
+    /// Panics on out-of-bounds accesses.
+    ///
+    /// Don't generally use this method, use `read_u8`, `read_i64`,
+    /// etc... instead.
+    #[inline]
+    fn read_pod<const N: usize, T>(&self, offset: u32) -> T
+    where
+        T: PodValType<N>,
+    {
+        assert_eq!(N, mem::size_of::<T>());
+        let offset = usize::try_from(offset).unwrap();
+        let end = offset.checked_add(N).unwrap();
+        let bytes = self.data.get(offset..end).expect("out of bounds field");
+        T::read_le(bytes.try_into().unwrap())
+    }
+
+    /// Read a POD field out of this object.
+    ///
+    /// Panics on out-of-bounds accesses.
+    ///
+    /// Don't generally use this method, use `write_u8`, `write_i64`,
+    /// etc... instead.
+    #[inline]
+    fn write_pod<const N: usize, T>(&mut self, offset: u32, val: T)
+    where
+        T: PodValType<N>,
+    {
+        assert_eq!(N, mem::size_of::<T>());
+        let offset = usize::try_from(offset).unwrap();
+        let end = offset.checked_add(N).unwrap();
+        let into = self.data.get_mut(offset..end).expect("out of bounds field");
+        val.write_le(into.try_into().unwrap());
+    }
+
+    impl_pod_methods! {
+        u8, read_u8, write_u8;
+        u16, read_u16, write_u16;
+        u32, read_u32, write_u32;
+        u64, read_u64, write_u64;
+        i8, read_i8, write_i8;
+        i16, read_i16, write_i16;
+        i32, read_i32, write_i32;
+        i64, read_i64, write_i64;
+        V128, read_v128, write_v128;
+    }
+}
diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled/drc.rs b/crates/wasmtime/src/runtime/vm/gc/enabled/drc.rs
index 9b7086ea3..abc1a799b 100644
--- a/crates/wasmtime/src/runtime/vm/gc/enabled/drc.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/enabled/drc.rs
@@ -42,7 +42,7 @@
 //! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
 
 use super::free_list::FreeList;
-use super::{VMStructDataMut, VMStructRef};
+use super::{VMArrayRef, VMGcObjectDataMut, VMStructRef};
 use crate::prelude::*;
 use crate::runtime::vm::{
     ExternRefHostDataId, ExternRefHostDataTable, GarbageCollection, GcArrayLayout, GcHeap,
@@ -111,12 +111,18 @@ unsafe impl GcRuntime for DrcCollector {
         let mut size = VMDrcHeader::HEADER_SIZE;
         let mut align = VMDrcHeader::HEADER_ALIGN;
         let length_field_offset = field(&mut size, &mut align, 4);
-        let elems_offset = align_up(&mut size, &mut align, size_of_wasm_ty(&ty.0.element_type));
+        debug_assert_eq!(
+            length_field_offset,
+            u32::try_from(core::mem::offset_of!(VMDrcArrayHeader, length)).unwrap(),
+        );
+        let elem_size = size_of_wasm_ty(&ty.0.element_type);
+        let elems_offset = align_up(&mut size, &mut align, elem_size);
         GcArrayLayout {
             size,
             align,
             length_field_offset,
             elems_offset,
+            elem_size,
         }
     }
 
@@ -576,6 +582,21 @@ impl VMDrcHeader {
     }
 }
 
+/// The common header for all arrays in the DRC collector.
+#[repr(C)]
+struct VMDrcArrayHeader {
+    header: VMDrcHeader,
+    length: u32,
+}
+
+unsafe impl GcHeapObject for VMDrcArrayHeader {
+    #[inline]
+    fn is(header: &VMGcHeader) -> bool {
+        header.kind() == VMGcKind::ArrayRef
+    }
+}
+
+/// The representation of an `externref` in the DRC collector.
 #[repr(C)]
 struct VMDrcExternRef {
     header: VMDrcHeader,
@@ -684,13 +705,46 @@ unsafe impl GcHeap for DrcHeap {
         self.dealloc(structref.into());
     }
 
-    fn struct_data(&mut self, structref: &VMStructRef, size: u32) -> VMStructDataMut<'_> {
-        let start = structref.as_gc_ref().as_heap_index().unwrap().get();
+    fn gc_object_data(&mut self, gc_ref: &VMGcRef) -> VMGcObjectDataMut<'_> {
+        let start = gc_ref.as_heap_index().unwrap().get();
         let start = usize::try_from(start).unwrap();
-        let size = usize::try_from(size).unwrap();
+        let size = self
+            .index::<VMDrcHeader>(gc_ref.as_typed_unchecked())
+            .object_size();
         let end = start + size;
         let data = &mut self.heap_slice_mut()[start..end];
-        VMStructDataMut::new(data)
+        VMGcObjectDataMut::new(data)
+    }
+
+    fn alloc_uninit_array(
+        &mut self,
+        ty: VMSharedTypeIndex,
+        length: u32,
+        layout: &GcArrayLayout,
+    ) -> Result<Option<VMArrayRef>> {
+        let size = usize::try_from(layout.size_for_len(length)).unwrap();
+        let align = usize::try_from(layout.align).unwrap();
+        let layout = Layout::from_size_align(size, align).unwrap();
+        let gc_ref = match self.alloc(
+            VMGcHeader::from_kind_and_index(VMGcKind::ArrayRef, ty),
+            layout,
+        )? {
+            None => return Ok(None),
+            Some(gc_ref) => gc_ref,
+        };
+        self.index_mut::<VMDrcArrayHeader>(gc_ref.as_typed_unchecked())
+            .length = length;
+        Ok(Some(gc_ref.into_arrayref_unchecked()))
+    }
+
+    fn dealloc_uninit_array(&mut self, arrayref: VMArrayRef) {
+        self.dealloc(arrayref.into())
+    }
+
+    fn array_len(&self, arrayref: &VMArrayRef) -> u32 {
+        debug_assert!(arrayref.as_gc_ref().is_typed::<VMDrcArrayHeader>(self));
+        self.index::<VMDrcArrayHeader>(arrayref.as_gc_ref().as_typed_unchecked())
+            .length
     }
 
     fn gc<'a>(
diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled/structref.rs b/crates/wasmtime/src/runtime/vm/gc/enabled/structref.rs
index 813c4268c..d0bd18d43 100644
--- a/crates/wasmtime/src/runtime/vm/gc/enabled/structref.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/enabled/structref.rs
@@ -3,9 +3,9 @@ use crate::{
     runtime::vm::{GcHeap, GcStore, VMGcRef},
     store::AutoAssertNoGc,
     vm::GcStructLayout,
-    AnyRef, ExternRef, HeapType, RootedGcRefImpl, StorageType, Val, ValType, V128,
+    AnyRef, ExternRef, HeapType, RootedGcRefImpl, StorageType, Val, ValType,
 };
-use core::{fmt, mem};
+use core::fmt;
 use wasmtime_environ::VMGcKind;
 
 /// A `VMGcRef` that we know points to a `struct`.
@@ -139,7 +139,7 @@ impl VMStructRef {
         field: usize,
     ) -> Val {
         let offset = layout.fields[field];
-        let data = store.unwrap_gc_store_mut().struct_data(self, layout.size);
+        let data = store.unwrap_gc_store_mut().gc_object_data(self.as_gc_ref());
         match ty {
             StorageType::I8 => Val::I32(data.read_u8(offset).into()),
             StorageType::I16 => Val::I32(data.read_u16(offset).into()),
@@ -185,7 +185,7 @@ impl VMStructRef {
         debug_assert!(val._matches_ty(&store, &ty.unpack())?);
 
         let offset = layout.fields[field];
-        let mut data = store.gc_store_mut()?.struct_data(self, layout.size);
+        let mut data = store.gc_store_mut()?.gc_object_data(self.as_gc_ref());
         match val {
             Val::I32(i) if ty.is_i8() => data.write_i8(offset, i as i8),
             Val::I32(i) if ty.is_i16() => data.write_i16(offset, i as i16),
@@ -215,7 +215,7 @@ impl VMStructRef {
                     None => None,
                 };
                 store.gc_store_mut()?.write_gc_ref(&mut gc_ref, e.as_ref());
-                let mut data = store.gc_store_mut()?.struct_data(self, layout.size);
+                let mut data = store.gc_store_mut()?.gc_object_data(self.as_gc_ref());
                 data.write_u32(offset, gc_ref.map_or(0, |r| r.as_raw_u32()));
             }
             Val::AnyRef(a) => {
@@ -226,7 +226,7 @@ impl VMStructRef {
                     None => None,
                 };
                 store.gc_store_mut()?.write_gc_ref(&mut gc_ref, a.as_ref());
-                let mut data = store.gc_store_mut()?.struct_data(self, layout.size);
+                let mut data = store.gc_store_mut()?.gc_object_data(self.as_gc_ref());
                 data.write_u32(offset, gc_ref.map_or(0, |r| r.as_raw_u32()));
             }
 
@@ -272,31 +272,31 @@ impl VMStructRef {
         match val {
             Val::I32(i) if ty.is_i8() => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_i8(offset, i as i8),
             Val::I32(i) if ty.is_i16() => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_i16(offset, i as i16),
             Val::I32(i) => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_i32(offset, i),
             Val::I64(i) => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_i64(offset, i),
             Val::F32(f) => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_u32(offset, f),
             Val::F64(f) => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_u64(offset, f),
             Val::V128(v) => store
                 .gc_store_mut()?
-                .struct_data(self, layout.size)
+                .gc_object_data(self.as_gc_ref())
                 .write_v128(offset, v),
 
             // NB: We don't need to do a write barrier when initializing a
@@ -309,7 +309,7 @@ impl VMStructRef {
                 };
                 store
                     .gc_store_mut()?
-                    .struct_data(self, layout.size)
+                    .gc_object_data(self.as_gc_ref())
                     .write_u32(offset, x);
             }
             Val::AnyRef(x) => {
@@ -319,7 +319,7 @@ impl VMStructRef {
                 };
                 store
                     .gc_store_mut()?
-                    .struct_data(self, layout.size)
+                    .gc_object_data(self.as_gc_ref())
                     .write_u32(offset, x);
             }
 
@@ -334,148 +334,3 @@ impl VMStructRef {
         Ok(())
     }
 }
-
-/// A plain-old-data type that can be stored in a `ValType` or a `StorageType`.
-///
-/// Safety: implementations must be POD and all bit patterns must be valid.
-pub trait PodValType<const SIZE: usize>: Copy {
-    /// Read an instance of `Self` from the given little-endian bytes.
-    fn read_le(le_bytes: &[u8; SIZE]) -> Self;
-
-    /// Write `self` into the given memory location, as little-endian bytes.
-    fn write_le(&self, into: &mut [u8; SIZE]);
-}
-
-macro_rules! impl_pod_val_type {
-    ( $( $t:ty , )* ) => {
-        $(
-            impl PodValType<{core::mem::size_of::<$t>()}> for $t {
-                fn read_le(le_bytes: &[u8; core::mem::size_of::<$t>()]) -> Self {
-                    <$t>::from_le_bytes(*le_bytes)
-                }
-                fn write_le(&self, into: &mut [u8; core::mem::size_of::<$t>()]) {
-                    *into = self.to_le_bytes();
-                }
-            }
-        )*
-    };
-}
-
-impl_pod_val_type! {
-    u8,
-    u16,
-    u32,
-    u64,
-    i8,
-    i16,
-    i32,
-    i64,
-}
-
-impl PodValType<{ mem::size_of::<V128>() }> for V128 {
-    fn read_le(le_bytes: &[u8; mem::size_of::<V128>()]) -> Self {
-        u128::from_le_bytes(*le_bytes).into()
-    }
-    fn write_le(&self, into: &mut [u8; mem::size_of::<V128>()]) {
-        *into = self.as_u128().to_le_bytes();
-    }
-}
-
-/// The backing storage for a GC-managed struct.
-///
-/// Methods on this type do not, generally, check against things like type
-/// mismatches or that the given offset to read from even falls on a field
-/// boundary. Omitting these checks is memory safe, due to our untrusted,
-/// indexed GC heaps. Providing incorrect offsets will result in general
-/// incorrectness, such as wrong answers or even panics, however.
-///
-/// Finally, these methods *will* panic on out-of-bounds accesses, either out of
-/// the GC heap's bounds or out of this struct's bounds. The former is necessary
-/// for preserving the memory safety of indexed GC heaps in the face of (for
-/// example) collector bugs, but the latter is just a defensive technique to
-/// catch bugs early and prevent action at a distance as much as possible.
-pub struct VMStructDataMut<'a> {
-    data: &'a mut [u8],
-}
-
-macro_rules! impl_pod_methods {
-    ( $( $t:ty, $read:ident, $write:ident; )* ) => {
-        $(
-            /// Read from a `
-            #[doc = stringify!($t)]
-            /// ` field in this struct.
-            ///
-            /// Panics on out-of-bounds accesses.
-            #[inline]
-            pub fn $read(&self, offset: u32) -> $t {
-                self.read_pod::<{ mem::size_of::<$t>() }, $t>(offset)
-            }
-
-            /// Write to a `
-            #[doc = stringify!($t)]
-            /// ` field in this struct.
-            ///
-            /// Panics on out-of-bounds accesses.
-            #[inline]
-            pub fn $write(&mut self, offset: u32, val: $t) {
-                self.write_pod::<{ mem::size_of::<$t>() }, $t>(offset, val);
-            }
-        )*
-    };
-}
-
-impl<'a> VMStructDataMut<'a> {
-    /// Construct a `VMStructDataMut` from the given slice of bytes.
-    #[inline]
-    pub fn new(data: &'a mut [u8]) -> Self {
-        Self { data }
-    }
-
-    /// Read a POD field out of this struct.
-    ///
-    /// Panics on out-of-bounds accesses.
-    ///
-    /// Don't generally use this method, use `read_u8`, `read_i64`,
-    /// etc... instead.
-    #[inline]
-    fn read_pod<const N: usize, T>(&self, offset: u32) -> T
-    where
-        T: PodValType<N>,
-    {
-        assert_eq!(N, mem::size_of::<T>());
-        let offset = usize::try_from(offset).unwrap();
-        let end = offset.checked_add(N).unwrap();
-        let bytes = self.data.get(offset..end).expect("out of bounds field");
-        T::read_le(bytes.try_into().unwrap())
-    }
-
-    /// Read a POD field out of this struct.
-    ///
-    /// Panics on out-of-bounds accesses.
-    ///
-    /// Don't generally use this method, use `write_u8`, `write_i64`,
-    /// etc... instead.
-    #[inline]
-    fn write_pod<const N: usize, T>(&mut self, offset: u32, val: T)
-    where
-        T: PodValType<N>,
-    {
-        assert_eq!(N, mem::size_of::<T>());
-        let offset = usize::try_from(offset).unwrap();
-        let end = offset.checked_add(N).unwrap();
-        let into = self.data.get_mut(offset..end).expect("out of bounds field");
-        val.write_le(into.try_into().unwrap());
-    }
-
-    impl_pod_methods! {
-        u8, read_u8, write_u8;
-        u16, read_u16, write_u16;
-        u32, read_u32, write_u32;
-        u64, read_u64, write_u64;
-        i8, read_i8, write_i8;
-        i16, read_i16, write_i16;
-        i32, read_i32, write_i32;
-        i64, read_i64, write_i64;
-        V128, read_v128, write_v128;
-    }
-}
diff --git a/crates/wasmtime/src/runtime/vm/gc/gc_ref.rs b/crates/wasmtime/src/runtime/vm/gc/gc_ref.rs
index 4f2a1d73b..a916e3888 100644
--- a/crates/wasmtime/src/runtime/vm/gc/gc_ref.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/gc_ref.rs
@@ -300,6 +300,17 @@ impl VMGcRef {
         }
     }
 
+    /// Is this GC reference pointing to a `T`?
+    pub fn is_typed<T>(&self, gc_heap: &impl GcHeap) -> bool
+    where
+        T: GcHeapObject,
+    {
+        if self.is_i31() {
+            return false;
+        }
+        T::is(gc_heap.header(&self))
+    }
+
     /// Borrow `self` as a typed GC reference, checking that `self` actually is
     /// a `T`.
     pub fn as_typed<T>(&self, gc_heap: &impl GcHeap) -> Option<&TypedGcRef<T>>
diff --git a/crates/wasmtime/src/runtime/vm/gc/gc_runtime.rs b/crates/wasmtime/src/runtime/vm/gc/gc_runtime.rs
index ee42134aa..d36e64ecd 100644
--- a/crates/wasmtime/src/runtime/vm/gc/gc_runtime.rs
+++ b/crates/wasmtime/src/runtime/vm/gc/gc_runtime.rs
@@ -2,15 +2,15 @@
 
 use crate::prelude::*;
 use crate::runtime::vm::{
-    ExternRefHostDataId, ExternRefHostDataTable, SendSyncPtr, VMExternRef, VMGcHeader, VMGcRef,
-    VMStructRef,
+    ExternRefHostDataId, ExternRefHostDataTable, SendSyncPtr, VMArrayRef, VMExternRef, VMGcHeader,
+    VMGcRef, VMStructRef,
 };
 use core::marker;
 use core::ptr;
 use core::{any::Any, num::NonZeroUsize};
 use wasmtime_environ::{VMSharedTypeIndex, WasmArrayType, WasmStructType};
 
-use super::VMStructDataMut;
+use super::VMGcObjectDataMut;
 
 /// Trait for integrating a garbage collector with the runtime.
 ///
@@ -218,7 +218,7 @@ pub unsafe trait GcHeap: 'static + Send + Sync {
     /// responsibility to initialize them before exposing the struct to Wasm or
     /// triggering a GC.
     ///
-    /// The `kind`, `ty`, and `layout` must match.
+    /// The `ty` and `layout` must match.
     ///
     /// Failure to do either of the above is memory safe, but may result in
     /// general failures such as panics or incorrect results.
@@ -251,14 +251,55 @@ pub unsafe trait GcHeap: 'static + Send + Sync {
     /// valid GC references, or something like that.
     fn dealloc_uninit_struct(&mut self, structref: VMStructRef);
 
-    /// Get a mutable borrow of the the given struct's data.
+    /// Get a mutable borrow of the the given object's data.
+    ///
+    /// Panics on out-of-bounds accesses.
+    fn gc_object_data(&mut self, gc_ref: &VMGcRef) -> VMGcObjectDataMut<'_>;
+
+    /// Allocate a GC-managed array of the given type and length.
+    ///
+    /// The array's elements are left uninitialized. It is the caller's
+    /// responsibility to initialize them before exposing the array to Wasm or
+    /// triggering a GC. Failure to do this is memory safe, but may result in
+    /// general failures such as panics or incorrect results.
+    ///
+    /// Return values:
+    ///
+    /// * `Ok(Some(_))`: The allocation was successful.
+    ///
+    /// * `Ok(None)`: There is currently no available space for this
+    ///   allocation. The caller should call `self.gc()`, run the GC to
+    ///   completion so the collector can reclaim space, and then try allocating
+    ///   again.
+    ///
+    /// * `Err(_)`: The collector cannot satisfy this allocation request, and
+    ///   would not be able to even after the caller were to trigger a
+    ///   collection. This could be because, for example, the requested
+    ///   allocation is larger than this collector's implementation limit for
+    ///   object size.
+    fn alloc_uninit_array(
+        &mut self,
+        ty: VMSharedTypeIndex,
+        len: u32,
+        layout: &GcArrayLayout,
+    ) -> Result<Option<VMArrayRef>>;
+
+    /// Deallocate an uninitialized, GC-managed array.
+    ///
+    /// This is useful for if initialization of the array's fields fails, so
+    /// that the array's allocation can be eagerly reclaimed, and so that the
+    /// collector doesn't attempt to treat any of the uninitialized fields as
+    /// valid GC references, or something like that.
+    fn dealloc_uninit_array(&mut self, arrayref: VMArrayRef);
+
+    /// Get the length of the given array.
     ///
     /// Panics on out-of-bounds accesses.
     ///
-    /// The given `structref` should be valid and of the given size. Failure to
+    /// The given `arrayref` should be valid and of the given size. Failure to
     /// do so is memory safe, but may result in general failures such as panics
     /// or incorrect results.
-    fn struct_data(&mut self, structref: &VMStructRef, size: u32) -> VMStructDataMut<'_>;
+    fn array_len(&self, arrayref: &VMArrayRef) -> u32;
 
     ////////////////////////////////////////////////////////////////////////////
     // Garbage Collection Methods
@@ -370,12 +411,22 @@ impl From<GcStructLayout> for GcLayout {
 
 impl GcLayout {
     /// Get the underlying `GcStructLayout`, or panic.
+    #[track_caller]
     pub fn unwrap_struct(&self) -> &GcStructLayout {
         match self {
             Self::Struct(s) => s,
             _ => panic!("GcLayout::unwrap_struct on non-struct GC layout"),
         }
     }
+
+    /// Get the underlying `GcArrayLayout`, or panic.
+    #[track_caller]
+    pub fn unwrap_array(&self) -> &GcArrayLayout {
+        match self {
+            Self::Array(a) => a,
+            _ => panic!("GcLayout::unwrap_array on non-array GC layout"),
+        }
+    }
 }
 
 /// The layout of a GC-managed array.
@@ -406,6 +457,22 @@ pub struct GcArrayLayout {
 
     /// The offset from where this array's contiguous elements begin.
     pub elems_offset: u32,
+
+    /// The size and natural alignment of each element in this array.
+    pub elem_size: u32,
+}
+
+impl GcArrayLayout {
+    /// Get the total size of this array for a given length of elements.
+    pub fn size_for_len(&self, len: u32) -> u32 {
+        self.size + len * self.elem_size
+    }
+
+    /// Get the offset of the `i`th element in an array with this layout.
+    #[inline]
+    pub fn elem_offset(&self, i: u32, elem_size: u32) -> u32 {
+        self.elems_offset + i * elem_size
+    }
 }
 
 /// The layout for a GC-managed struct type.
diff --git a/winch/codegen/src/codegen/mod.rs b/winch/codegen/src/codegen/mod.rs
index 97e1f0ce4..6d052982e 100644
--- a/winch/codegen/src/codegen/mod.rs
+++ b/winch/codegen/src/codegen/mod.rs
@@ -593,6 +593,14 @@ where
                 // Move the value of the index to the
                 // index_offset_and_access_size register to perform the overflow
                 // check to avoid clobbering the initial index value.
+                //
+                // We derive size of the operation from the heap type since:
+                //
+                // * This is the first assignment to the
+                // `index_offset_and_access_size` register
+                //
+                // * The memory64 proposal specifies that the index is bound to
+                // the heap type instead of hardcoding it to 32-bits (i32).
                 self.masm.mov(
                     index_reg.into(),
                     index_offset_and_access_size,
@@ -601,11 +609,18 @@ where
                 // Perform
                 // index = index + offset + access_size, trapping if the
                 // addition overflows.
+                //
+                // We use the target's pointer size rather than depending on the heap
+                // type since we want to check for overflow; even though the
+                // offset and access size are guaranteed to be bounded by the heap
+                // type, when added, if used with the wrong operand size, their
+                // result could be clamped, resulting in an erroneus overflow
+                // check.
                 self.masm.checked_uadd(
                     index_offset_and_access_size,
                     index_offset_and_access_size,
                     RegImm::i64(offset_with_access_size as i64),
-                    heap.ty.into(),
+                    ptr_size,
                     TrapCode::HeapOutOfBounds,
                 );
 
@@ -623,7 +638,10 @@ where
                         masm.cmp(
                             index_offset_and_access_size.into(),
                             bounds_reg.into(),
-                            heap.ty.into(),
+                            // We use the pointer size to keep the bounds
+                            // comparison consistent with the result of the
+                            // overflow check above.
+                            ptr_size,
                         );
                         IntCmpKind::GtU
                     },
@@ -707,7 +725,12 @@ where
                         masm.cmp(
                             index_reg,
                             RegImm::i64(adjusted_bounds as i64),
-                            heap.ty.into(),
+                            // Similar to the dynamic heap case, even though the
+                            // offset and access size are bound through the heap
+                            // type, when added they can overflow, resulting in
+                            // an erroneus comparison, therfore we rely on the
+                            // target pointer size.
+                            ptr_size,
                         );
                         IntCmpKind::GtU
                     },


Test patch:
diff --git a/tests/all/arrays.rs b/tests/all/arrays.rs
new file mode 100644
index 000000000..3a2d271af
--- /dev/null
+++ b/tests/all/arrays.rs
@@ -0,0 +1,888 @@
+use wasmtime::*;
+
+fn gc_store() -> Result<Store<()>> {
+    let _ = env_logger::try_init();
+
+    let mut config = Config::new();
+    config.wasm_function_references(true);
+    config.wasm_gc(true);
+
+    let engine = Engine::new(&config)?;
+    Ok(Store::new(&engine, ()))
+}
+
+#[test]
+fn array_new_empty() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = ArrayRef::new(&mut store, &pre, &Val::I32(0), 0)?;
+    assert_eq!(array.len(&store)?, 0);
+    Ok(())
+}
+
+#[test]
+fn array_new_with_elems() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = ArrayRef::new(&mut store, &pre, &Val::I32(99), 3)?;
+    assert_eq!(array.len(&store)?, 3);
+    for i in 0..3 {
+        assert_eq!(array.get(&mut store, i)?.unwrap_i32(), 99);
+    }
+    Ok(())
+}
+
+#[test]
+fn array_new_unrooted_initial_elem() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, StorageType::ValType(ValType::ANYREF)),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    // Passing an unrooted `anyref` to `StructRef::new` results in an error.
+    let anyref = {
+        let mut scope = RootScope::new(&mut store);
+        AnyRef::from_i31(&mut scope, I31::new_i32(1234).unwrap())
+    };
+
+    assert!(ArrayRef::new(&mut store, &pre, &anyref.into(), 3).is_err());
+    Ok(())
+}
+
+#[test]
+#[should_panic = "wrong store"]
+fn array_new_cross_store_initial_elem() {
+    let mut store1 = gc_store().unwrap();
+    let mut store2 = gc_store().unwrap();
+
+    let array_ty = ArrayType::new(
+        store1.engine(),
+        FieldType::new(Mutability::Var, StorageType::ValType(ValType::ANYREF)),
+    );
+    let pre = ArrayRefPre::new(&mut store1, array_ty);
+
+    // Passing an `anyref` from a different store to `ArrayRef::new` results in
+    // a panic.
+    let anyref = AnyRef::from_i31(&mut store2, I31::new_i32(1234).unwrap());
+    ArrayRef::new(&mut store1, &pre, &anyref.into(), 3).unwrap();
+}
+
+#[test]
+#[should_panic = "wrong store"]
+fn array_new_cross_store_pre() {
+    let mut store1 = gc_store().unwrap();
+    let mut store2 = gc_store().unwrap();
+
+    let array_ty = ArrayType::new(
+        store1.engine(),
+        FieldType::new(Mutability::Var, StorageType::ValType(ValType::ANYREF)),
+    );
+    let pre = ArrayRefPre::new(&mut store2, array_ty);
+
+    ArrayRef::new(&mut store1, &pre, &Val::I32(0), 3).unwrap();
+}
+
+#[test]
+fn array_new_fixed_empty() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[])?;
+    assert_eq!(array.len(&store)?, 0);
+    Ok(())
+}
+
+#[test]
+fn array_new_fixed_with_elems() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = ArrayRef::new_fixed(
+        &mut store,
+        &pre,
+        &[Val::I32(11), Val::I32(22), Val::I32(33)],
+    )?;
+    assert_eq!(array.len(&store)?, 3);
+    for i in 0..3 {
+        assert_eq!(array.get(&mut store, i)?.unwrap_i32(), (i as i32 + 1) * 11);
+    }
+    Ok(())
+}
+
+#[test]
+fn array_new_fixed_unrooted_initial_elem() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, StorageType::ValType(ValType::ANYREF)),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    // Passing an unrooted `anyref` to `StructRef::new` results in an error.
+    let anyref = {
+        let mut scope = RootScope::new(&mut store);
+        AnyRef::from_i31(&mut scope, I31::new_i32(1234).unwrap())
+    };
+
+    assert!(ArrayRef::new_fixed(&mut store, &pre, &[anyref.into()]).is_err());
+    Ok(())
+}
+
+#[test]
+#[should_panic = "wrong store"]
+fn array_new_fixed_cross_store_initial_elem() {
+    let mut store1 = gc_store().unwrap();
+    let mut store2 = gc_store().unwrap();
+
+    let array_ty = ArrayType::new(
+        store1.engine(),
+        FieldType::new(Mutability::Var, StorageType::ValType(ValType::ANYREF)),
+    );
+    let pre = ArrayRefPre::new(&mut store1, array_ty);
+
+    // Passing an `anyref` from a different store to `ArrayRef::new_fixed`
+    // results in a panic.
+    let anyref = AnyRef::from_i31(&mut store2, I31::new_i32(1234).unwrap());
+    ArrayRef::new_fixed(&mut store1, &pre, &[anyref.into()]).unwrap();
+}
+
+#[test]
+#[should_panic = "wrong store"]
+fn array_new_fixed_cross_store_pre() {
+    let mut store1 = gc_store().unwrap();
+    let mut store2 = gc_store().unwrap();
+
+    let array_ty = ArrayType::new(
+        store1.engine(),
+        FieldType::new(Mutability::Var, StorageType::ValType(ValType::ANYREF)),
+    );
+    let pre = ArrayRefPre::new(&mut store2, array_ty);
+
+    ArrayRef::new_fixed(&mut store1, &pre, &[Val::I32(0)]).unwrap();
+}
+
+#[test]
+fn anyref_as_array() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::ValType(ValType::I32)),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let a0 = ArrayRef::new_fixed(&mut store, &pre, &[])?;
+
+    let anyref: Rooted<AnyRef> = a0.into();
+    assert!(anyref.is_array(&store)?);
+    let a1 = anyref.as_array(&store)?.unwrap();
+    assert!(Rooted::ref_eq(&store, &a0, &a1)?);
+
+    let anyref = AnyRef::from_i31(&mut store, I31::new_i32(42).unwrap());
+    assert!(!anyref.is_array(&store)?);
+    assert!(anyref.as_array(&store)?.is_none());
+
+    Ok(())
+}
+
+#[test]
+fn array_len_empty() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[])?;
+    assert_eq!(array.len(&store)?, 0);
+
+    Ok(())
+}
+
+#[test]
+fn array_len_non_empty() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(
+        &mut store,
+        &pre,
+        &[Val::I32(11), Val::I32(22), Val::I32(33)],
+    )?;
+    assert_eq!(array.len(&store)?, 3);
+
+    Ok(())
+}
+
+#[test]
+fn array_get_in_bounds() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(
+        &mut store,
+        &pre,
+        &[Val::I32(11), Val::I32(22), Val::I32(33)],
+    )?;
+
+    assert_eq!(array.get(&mut store, 0)?.unwrap_i32(), 11);
+    assert_eq!(array.get(&mut store, 1)?.unwrap_i32(), 22);
+    assert_eq!(array.get(&mut store, 2)?.unwrap_i32(), 33);
+
+    Ok(())
+}
+
+#[test]
+fn array_get_out_of_bounds() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(
+        &mut store,
+        &pre,
+        &[Val::I32(11), Val::I32(22), Val::I32(33)],
+    )?;
+
+    assert!(array.get(&mut store, 3).is_err());
+    assert!(array.get(&mut store, 4).is_err());
+
+    Ok(())
+}
+
+#[test]
+fn array_get_on_unrooted() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = {
+        let mut scope = RootScope::new(&mut store);
+        ArrayRef::new_fixed(&mut scope, &pre, &[Val::I32(11)])?
+    };
+
+    assert!(array.get(&mut store, 0).is_err());
+    Ok(())
+}
+
+#[test]
+#[should_panic = "wrong store"]
+fn array_get_wrong_store() {
+    let mut store1 = gc_store().unwrap();
+    let mut store2 = gc_store().unwrap();
+
+    let array_ty = ArrayType::new(
+        store1.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store1, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store1, &pre, &[Val::I32(11)]).unwrap();
+
+    // Should panic.
+    let _ = array.get(&mut store2, 0);
+}
+
+#[test]
+fn array_set_in_bounds() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(11)])?;
+
+    assert_eq!(array.get(&mut store, 0)?.unwrap_i32(), 11);
+    array.set(&mut store, 0, Val::I32(22))?;
+    assert_eq!(array.get(&mut store, 0)?.unwrap_i32(), 22);
+
+    Ok(())
+}
+
+#[test]
+fn array_set_out_of_bounds() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(11)])?;
+
+    assert!(array.set(&mut store, 1, Val::I32(22)).is_err());
+    assert!(array.set(&mut store, 2, Val::I32(33)).is_err());
+
+    Ok(())
+}
+
+#[test]
+fn array_set_on_unrooted() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = {
+        let mut scope = RootScope::new(&mut store);
+        ArrayRef::new_fixed(&mut scope, &pre, &[Val::I32(11)])?
+    };
+
+    assert!(array.set(&mut store, 0, Val::I32(22)).is_err());
+    Ok(())
+}
+
+#[test]
+fn array_set_given_unrooted() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, ValType::ANYREF.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[Val::AnyRef(None)])?;
+
+    let anyref = {
+        let mut scope = RootScope::new(&mut store);
+        AnyRef::from_i31(&mut scope, I31::new_i32(1234).unwrap())
+    };
+
+    assert!(array.set(&mut store, 0, anyref.into()).is_err());
+    Ok(())
+}
+
+#[test]
+#[should_panic = "wrong store"]
+fn array_set_cross_store_value() {
+    let mut store1 = gc_store().unwrap();
+    let mut store2 = gc_store().unwrap();
+
+    let array_ty = ArrayType::new(
+        store1.engine(),
+        FieldType::new(Mutability::Var, ValType::ANYREF.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store1, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store1, &pre, &[Val::AnyRef(None)]).unwrap();
+
+    let anyref = AnyRef::from_i31(&mut store2, I31::new_i32(1234).unwrap());
+
+    // Should panic.
+    let _ = array.set(&mut store1, 0, anyref.into());
+}
+
+#[test]
+fn array_set_immutable_elems() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(
+        &mut store,
+        &pre,
+        &[Val::I32(11), Val::I32(22), Val::I32(33)],
+    )?;
+
+    assert!(array.set(&mut store, 0, Val::I32(22)).is_err());
+    assert!(array.set(&mut store, 1, Val::I32(33)).is_err());
+    assert!(array.set(&mut store, 2, Val::I32(44)).is_err());
+
+    Ok(())
+}
+
+#[test]
+fn array_set_wrong_field_type() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Var, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(11)])?;
+
+    assert!(array.set(&mut store, 0, Val::I64(22)).is_err());
+    Ok(())
+}
+
+#[test]
+fn array_ty() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(11)])?;
+    assert!(ArrayType::eq(&array_ty, &a.ty(&store)?));
+    Ok(())
+}
+
+#[test]
+fn array_ty_unrooted() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let a = {
+        let mut scope = RootScope::new(&mut store);
+        ArrayRef::new_fixed(&mut scope, &pre, &[Val::I32(11)])?
+    };
+    assert!(a.ty(&store).is_err());
+    Ok(())
+}
+
+#[test]
+fn array_elems_empty() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = ArrayRef::new_fixed(&mut store, &pre, &[])?;
+    let mut elems = array.elems(&mut store)?;
+    assert_eq!(elems.len(), 0);
+    assert!(elems.next().is_none());
+    Ok(())
+}
+
+#[test]
+fn array_elems_non_empty() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = ArrayRef::new_fixed(
+        &mut store,
+        &pre,
+        &[Val::I32(11), Val::I32(22), Val::I32(33)],
+    )?;
+    let mut elems = array.elems(&mut store)?;
+    assert_eq!(elems.len(), 3);
+    assert_eq!(elems.next().unwrap().unwrap_i32(), 11);
+    assert_eq!(elems.next().unwrap().unwrap_i32(), 22);
+    assert_eq!(elems.next().unwrap().unwrap_i32(), 33);
+    assert!(elems.next().is_none());
+    Ok(())
+}
+
+#[test]
+fn array_elems_unrooted() -> Result<()> {
+    let mut store = gc_store()?;
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, ValType::I32.into()),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let array = {
+        let mut scope = RootScope::new(&mut store);
+        ArrayRef::new_fixed(&mut scope, &pre, &[Val::I32(11)])?
+    };
+    assert!(array.elems(&mut store).is_err());
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn passing_arrays_through_wasm_with_untyped_calls() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (import "" "" (func $f (param (ref 0)) (result (ref 0))))
+                (func (export "run") (param (ref 0)) (result (ref 0))
+                    (call $f (local.get 0))
+                )
+            )
+        "#,
+    )?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+
+    let ref_ty = RefType::new(false, HeapType::ConcreteArray(array_ty.clone()));
+    let func_ty = FuncType::new(store.engine(), [ref_ty.clone().into()], [ref_ty.into()]);
+
+    let func = Func::new(&mut store, func_ty, |mut caller, args, results| {
+        let a = args[0].unwrap_any_ref().unwrap();
+        let a = a.unwrap_array(&mut caller)?;
+        assert_eq!(a.len(&mut caller)?, 1);
+        assert_eq!(a.get(&mut caller, 0)?.unwrap_i32(), 42);
+        results[0] = args[0];
+        Ok(())
+    });
+
+    let instance = Instance::new(&mut store, &module, &[func.into()])?;
+    let run = instance.get_func(&mut store, "run").unwrap();
+
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+
+    let mut results = vec![Val::null_any_ref()];
+    run.call(&mut store, &[a.into()], &mut results)?;
+
+    let a2 = results[0].unwrap_any_ref().unwrap();
+    let a2 = a2.unwrap_array(&mut store)?;
+    assert_eq!(a2.len(&mut store)?, 1);
+    assert_eq!(a2.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a, &a2)?);
+
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn passing_arrays_through_wasm_with_typed_calls() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (import "" "" (func $f (param (ref array)) (result (ref array))))
+                (func (export "run") (param (ref 0)) (result (ref array))
+                    (call $f (local.get 0))
+                )
+            )
+        "#,
+    )?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+
+    let func = Func::wrap(
+        &mut store,
+        |mut caller: Caller<()>, a: Rooted<ArrayRef>| -> Result<Rooted<ArrayRef>> {
+            assert_eq!(a.len(&caller)?, 1);
+            assert_eq!(a.get(&mut caller, 0)?.unwrap_i32(), 42);
+            Ok(a)
+        },
+    );
+
+    let instance = Instance::new(&mut store, &module, &[func.into()])?;
+    let run = instance.get_typed_func::<Rooted<ArrayRef>, Rooted<ArrayRef>>(&mut store, "run")?;
+
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+
+    let a2 = run.call(&mut store, a)?;
+
+    assert_eq!(a2.len(&store)?, 1);
+    assert_eq!(a2.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a, &a2)?);
+
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn host_sets_array_global() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (global $g (export "g") (mut (ref null 0)) (ref.null 0))
+                (func (export "f") (result (ref null 0))
+                    global.get $g
+                )
+            )
+        "#,
+    )?;
+
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let g = instance.get_global(&mut store, "g").unwrap();
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a0 = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+    g.set(&mut store, a0.into())?;
+
+    // Get the global from the host.
+    let val = g.get(&mut store);
+    let anyref = val.unwrap_anyref().expect("non-null");
+    let a1 = anyref.unwrap_array(&store)?;
+    assert_eq!(a1.len(&store)?, 1);
+    assert_eq!(a1.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a1)?);
+
+    // Get the global from the guest.
+    let f = instance.get_typed_func::<(), Option<Rooted<ArrayRef>>>(&mut store, "f")?;
+    let a2 = f.call(&mut store, ())?.expect("non-null");
+    assert_eq!(a2.len(&store)?, 1);
+    assert_eq!(a2.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a2)?);
+
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn wasm_sets_array_global() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (global $g (export "g") (mut (ref null 0)) (ref.null 0))
+                (func (export "get") (result (ref null 0))
+                    global.get $g
+                )
+                (func (export "set") (param (ref null 0))
+                    local.get 0
+                    global.set $g
+                )
+            )
+        "#,
+    )?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a0 = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let set = instance.get_func(&mut store, "set").unwrap();
+    set.call(&mut store, &[a0.into()], &mut [])?;
+
+    // Get the global from the host.
+    let g = instance.get_global(&mut store, "g").unwrap();
+    let val = g.get(&mut store);
+    let anyref = val.unwrap_anyref().expect("non-null");
+    let a1 = anyref.unwrap_array(&store)?;
+    assert_eq!(a1.len(&store)?, 1);
+    assert_eq!(a1.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a1)?);
+
+    // Get the global from the guest.
+    let f = instance.get_typed_func::<(), Option<Rooted<ArrayRef>>>(&mut store, "get")?;
+    let a2 = f.call(&mut store, ())?.expect("non-null");
+    assert_eq!(a2.len(&store)?, 1);
+    assert_eq!(a2.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a2)?);
+
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn host_sets_array_in_table() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (table $t (export "t") 1 1 (ref null 0) (ref.null 0))
+                (func (export "f") (result (ref null 0))
+                    i32.const 0
+                    table.get $t
+                )
+            )
+        "#,
+    )?;
+
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let t = instance.get_table(&mut store, "t").unwrap();
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a0 = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+    t.set(&mut store, 0, a0.into())?;
+
+    // Get the global from the host.
+    let val = t.get(&mut store, 0).expect("in bounds");
+    let anyref = val.unwrap_any().expect("non-null");
+    let a1 = anyref.unwrap_array(&store)?;
+    assert_eq!(a1.len(&store)?, 1);
+    assert_eq!(a1.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a1)?);
+
+    // Get the global from the guest.
+    let f = instance.get_typed_func::<(), Option<Rooted<ArrayRef>>>(&mut store, "f")?;
+    let a2 = f.call(&mut store, ())?.expect("non-null");
+    assert_eq!(a2.len(&store)?, 1);
+    assert_eq!(a2.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a2)?);
+
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn wasm_sets_array_in_table() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (table $t (export "t") 1 1 (ref null 0) (ref.null 0))
+                (func (export "get") (result (ref null 0))
+                    i32.const 0
+                    table.get $t
+                )
+                (func (export "set") (param (ref null 0))
+                    i32.const 0
+                    local.get 0
+                    table.set $t
+                )
+            )
+        "#,
+    )?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+    let pre = ArrayRefPre::new(&mut store, array_ty.clone());
+    let a0 = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let set = instance.get_func(&mut store, "set").unwrap();
+    set.call(&mut store, &[a0.into()], &mut [])?;
+
+    // Get the global from the host.
+    let t = instance.get_table(&mut store, "t").unwrap();
+    let val = t.get(&mut store, 0).expect("in bounds");
+    let anyref = val.unwrap_any().expect("non-null");
+    let a1 = anyref.unwrap_array(&store)?;
+    assert_eq!(a1.len(&mut store)?, 1);
+    assert_eq!(a1.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a1)?);
+
+    // Get the global from the guest.
+    let f = instance.get_typed_func::<(), Option<Rooted<ArrayRef>>>(&mut store, "get")?;
+    let a2 = f.call(&mut store, ())?.expect("non-null");
+    assert_eq!(a2.len(&mut store)?, 1);
+    assert_eq!(a2.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a2)?);
+
+    Ok(())
+}
+
+#[test]
+fn instantiate_with_array_global() -> Result<()> {
+    let mut store = gc_store()?;
+
+    let module = Module::new(
+        store.engine(),
+        r#"
+            (module
+                (type (array i8))
+                (import "" "" (global (ref null 0)))
+                (export "g" (global 0))
+            )
+        "#,
+    )?;
+
+    let array_ty = ArrayType::new(
+        store.engine(),
+        FieldType::new(Mutability::Const, StorageType::I8),
+    );
+    let global_ty = GlobalType::new(
+        ValType::Ref(RefType::new(
+            true,
+            HeapType::ConcreteArray(array_ty.clone()),
+        )),
+        Mutability::Const,
+    );
+
+    // Instantiate with a null-ref global.
+    let g = Global::new(&mut store, global_ty.clone(), Val::AnyRef(None))?;
+    let instance = Instance::new(&mut store, &module, &[g.into()])?;
+    let g = instance.get_global(&mut store, "g").expect("export exists");
+    let val = g.get(&mut store);
+    assert!(val.unwrap_anyref().is_none());
+
+    // Instantiate with a non-null-ref global.
+    let pre = ArrayRefPre::new(&mut store, array_ty);
+    let a0 = ArrayRef::new_fixed(&mut store, &pre, &[Val::I32(42)])?;
+    let g = Global::new(&mut store, global_ty, a0.into())?;
+    let instance = Instance::new(&mut store, &module, &[g.into()])?;
+    let g = instance.get_global(&mut store, "g").expect("export exists");
+    let val = g.get(&mut store);
+    let anyref = val.unwrap_anyref().expect("non-null");
+    let a1 = anyref.unwrap_array(&store)?;
+    assert_eq!(a1.len(&mut store)?, 1);
+    assert_eq!(a1.get(&mut store, 0)?.unwrap_i32(), 42);
+    assert!(Rooted::ref_eq(&store, &a0, &a1)?);
+
+    Ok(())
+}
diff --git a/tests/all/main.rs b/tests/all/main.rs
index f683b0268..dd2eb6158 100644
--- a/tests/all/main.rs
+++ b/tests/all/main.rs
@@ -1,5 +1,6 @@
 #![cfg_attr(miri, allow(dead_code, unused_imports))]
 
+mod arrays;
 mod async_functions;
 mod call_hook;
 mod cli_tests;
diff --git a/tests/disas/winch/x64/store/oob.wat b/tests/disas/winch/x64/store/oob.wat
new file mode 100644
index 000000000..4cb68e0d8
--- /dev/null
+++ b/tests/disas/winch/x64/store/oob.wat
@@ -0,0 +1,48 @@
+;;! target = "x86_64"
+;;! test = "winch"
+;;! flags = " -O static-memory-maximum-size=0"
+(module
+  (memory 1)
+  (func (export "foo") (param $i i32)
+    i32.const 0
+    (local.get $i)
+    i32.store8 offset=4294967295
+  )
+)
+
+;; wasm[0]::function[0]:
+;;       pushq   %rbp
+;;       movq    %rsp, %rbp
+;;       movq    8(%rdi), %r11
+;;       movq    (%r11), %r11
+;;       addq    $0x20, %r11
+;;       cmpq    %rsp, %r11
+;;       ja      0x85
+;;   1b: movq    %rdi, %r14
+;;       subq    $0x20, %rsp
+;;       movq    %rdi, 0x18(%rsp)
+;;       movq    %rsi, 0x10(%rsp)
+;;       movl    %edx, 0xc(%rsp)
+;;       movl    0xc(%rsp), %eax
+;;       movl    $0, %ecx
+;;       movq    0x68(%r14), %rdx
+;;       movl    %ecx, %ebx
+;;       movabsq $0x100000000, %r11
+;;       addq    %r11, %rbx
+;;       jb      0x87
+;;   52: cmpq    %rdx, %rbx
+;;       ja      0x89
+;;   5b: movq    0x60(%r14), %rsi
+;;       addq    %rcx, %rsi
+;;       movabsq $0xffffffff, %r11
+;;       addq    %r11, %rsi
+;;       movq    $0, %rdi
+;;       cmpq    %rdx, %rbx
+;;       cmovaq  %rdi, %rsi
+;;       movb    %al, (%rsi)
+;;       addq    $0x20, %rsp
+;;       popq    %rbp
+;;       retq
+;;   85: ud2
+;;   87: ud2
+;;   89: ud2
diff --git a/tests/misc_testsuite/winch/oob.wast b/tests/misc_testsuite/winch/oob.wast
new file mode 100644
index 000000000..c16344d64
--- /dev/null
+++ b/tests/misc_testsuite/winch/oob.wast
@@ -0,0 +1,10 @@
+(module
+  (memory 1)
+  (func (export "foo") (param $i i32)
+    i32.const 0
+    (local.get $i)
+    i32.store8 offset=4294967295
+  )
+)
+
+(assert_trap (invoke "foo" (i32.const 0)) "out of bounds")