Repository: bytecodealliance/wasmtime
Pull request #7990 (2024-02-23 15:44:58)

Update wit-bindgen dependency
Pulls in latest fixes and gets everything aligned on the latest wasm-tools release as well.

<!--
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
-->



Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9238ad276..74ce0cb54 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -288,6 +288,10 @@ jobs:
     - run: cargo check -p wasmtime --no-default-features --features wmemcheck
     - run: cargo check -p wasmtime --no-default-features --features demangle
     - run: cargo check -p wasmtime --no-default-features --features addr2line
+    - run: cargo check -p wasmtime --no-default-features --features gc
+    - run: cargo check -p wasmtime --no-default-features --features runtime,gc
+    - run: cargo check -p wasmtime --no-default-features --features cranelift,gc
+    - run: cargo check -p wasmtime --no-default-features --features runtime
     - run: cargo check --features component-model
     - run: cargo check -p wasmtime --features incremental-cache
 
diff --git a/Cargo.lock b/Cargo.lock
index d76ef92d4..769834368 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -653,7 +653,7 @@ dependencies = [
  "target-lexicon",
  "thiserror",
  "toml",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wat",
 ]
 
@@ -832,7 +832,7 @@ dependencies = [
  "serde_derive",
  "smallvec",
  "target-lexicon",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime-types",
  "wat",
 ]
@@ -2620,7 +2620,7 @@ version = "0.0.0"
 dependencies = [
  "cargo_metadata",
  "heck",
- "wit-component 0.200.0",
+ "wit-component",
 ]
 
 [[package]]
@@ -2930,7 +2930,7 @@ name = "verify-component-adapter"
 version = "19.0.0"
 dependencies = [
  "anyhow",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wat",
 ]
 
@@ -3021,7 +3021,7 @@ dependencies = [
  "byte-array-literals",
  "object",
  "wasi",
- "wasm-encoder 0.200.0",
+ "wasm-encoder",
  "wit-bindgen",
 ]
 
@@ -3079,15 +3079,6 @@ version = "0.2.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
 
-[[package]]
-name = "wasm-encoder"
-version = "0.41.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "972f97a5d8318f908dded23594188a90bcd09365986b1163e66d70170e5287ae"
-dependencies = [
- "leb128",
-]
-
 [[package]]
 name = "wasm-encoder"
 version = "0.200.0"
@@ -3097,22 +3088,6 @@ dependencies = [
  "leb128",
 ]
 
-[[package]]
-name = "wasm-metadata"
-version = "0.10.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18ebaa7bd0f9e7a5e5dd29b9a998acf21c4abed74265524dd7e85934597bfb10"
-dependencies = [
- "anyhow",
- "indexmap 2.0.0",
- "serde",
- "serde_derive",
- "serde_json",
- "spdx",
- "wasm-encoder 0.41.2",
- "wasmparser 0.121.2",
-]
-
 [[package]]
 name = "wasm-metadata"
 version = "0.200.0"
@@ -3125,8 +3100,8 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "spdx",
- "wasm-encoder 0.200.0",
- "wasmparser 0.200.0",
+ "wasm-encoder",
+ "wasmparser",
 ]
 
 [[package]]
@@ -3139,8 +3114,8 @@ dependencies = [
  "log",
  "rand",
  "thiserror",
- "wasm-encoder 0.200.0",
- "wasmparser 0.200.0",
+ "wasm-encoder",
+ "wasmparser",
 ]
 
 [[package]]
@@ -3154,7 +3129,7 @@ dependencies = [
  "flagset",
  "indexmap 2.0.0",
  "leb128",
- "wasm-encoder 0.200.0",
+ "wasm-encoder",
 ]
 
 [[package]]
@@ -3197,17 +3172,6 @@ dependencies = [
  "paste",
 ]
 
-[[package]]
-name = "wasmparser"
-version = "0.121.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab"
-dependencies = [
- "bitflags 2.4.1",
- "indexmap 2.0.0",
- "semver",
-]
-
 [[package]]
 name = "wasmparser"
 version = "0.200.0"
@@ -3235,7 +3199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2bd153b0dce4e727565ccb3bd41b0bd2b62b2b9b6b1057d1cdb7fe8e0f3e06ab"
 dependencies = [
  "anyhow",
- "wasmparser 0.200.0",
+ "wasmparser",
 ]
 
 [[package]]
@@ -3266,8 +3230,8 @@ dependencies = [
  "target-lexicon",
  "tempfile",
  "wasi-common",
- "wasm-encoder 0.200.0",
- "wasmparser 0.200.0",
+ "wasm-encoder",
+ "wasmparser",
  "wasmtime-cache",
  "wasmtime-component-macro",
  "wasmtime-component-util",
@@ -3277,6 +3241,7 @@ dependencies = [
  "wasmtime-jit-debug",
  "wasmtime-jit-icache-coherence",
  "wasmtime-runtime",
+ "wasmtime-slab",
  "wasmtime-winch",
  "wat",
  "windows-sys 0.52.0",
@@ -3397,7 +3362,7 @@ dependencies = [
  "tracing",
  "walkdir",
  "wasi-common",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime",
  "wasmtime-cache",
  "wasmtime-cli-flags",
@@ -3414,7 +3379,7 @@ dependencies = [
  "wast 200.0.0",
  "wat",
  "windows-sys 0.52.0",
- "wit-component 0.200.0",
+ "wit-component",
 ]
 
 [[package]]
@@ -3443,7 +3408,7 @@ dependencies = [
  "wasmtime",
  "wasmtime-component-util",
  "wasmtime-wit-bindgen",
- "wit-parser 0.200.0",
+ "wit-parser",
 ]
 
 [[package]]
@@ -3467,7 +3432,7 @@ dependencies = [
  "object",
  "target-lexicon",
  "thiserror",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime-cranelift-shared",
  "wasmtime-environ",
  "wasmtime-versioned-export-macros",
@@ -3506,8 +3471,8 @@ dependencies = [
  "serde_derive",
  "target-lexicon",
  "thiserror",
- "wasm-encoder 0.200.0",
- "wasmparser 0.200.0",
+ "wasm-encoder",
+ "wasmparser",
  "wasmprinter",
  "wasmtime-component-util",
  "wasmtime-types",
@@ -3522,7 +3487,7 @@ dependencies = [
  "component-fuzz-util",
  "env_logger",
  "libfuzzer-sys",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmprinter",
  "wasmtime-environ",
  "wat",
@@ -3579,7 +3544,7 @@ dependencies = [
  "rand",
  "smallvec",
  "target-lexicon",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime",
  "wasmtime-fuzzing",
 ]
@@ -3599,12 +3564,12 @@ dependencies = [
  "target-lexicon",
  "tempfile",
  "v8",
- "wasm-encoder 0.200.0",
+ "wasm-encoder",
  "wasm-mutate",
  "wasm-smith",
  "wasm-spec-interpreter",
  "wasmi",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmprinter",
  "wasmtime",
  "wasmtime-wast",
@@ -3651,7 +3616,7 @@ dependencies = [
  "rand",
  "rustix",
  "sptr",
- "wasm-encoder 0.200.0",
+ "wasm-encoder",
  "wasmtime-asm-macros",
  "wasmtime-environ",
  "wasmtime-fiber",
@@ -3661,6 +3626,10 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "wasmtime-slab"
+version = "19.0.0"
+
 [[package]]
 name = "wasmtime-types"
 version = "19.0.0"
@@ -3669,7 +3638,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "thiserror",
- "wasmparser 0.200.0",
+ "wasmparser",
 ]
 
 [[package]]
@@ -3786,7 +3755,7 @@ dependencies = [
  "gimli",
  "object",
  "target-lexicon",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime-cranelift-shared",
  "wasmtime-environ",
  "winch-codegen",
@@ -3799,7 +3768,7 @@ dependencies = [
  "anyhow",
  "heck",
  "indexmap 2.0.0",
- "wit-parser 0.200.0",
+ "wit-parser",
 ]
 
 [[package]]
@@ -3825,7 +3794,7 @@ dependencies = [
  "leb128",
  "memchr",
  "unicode-width",
- "wasm-encoder 0.200.0",
+ "wasm-encoder",
 ]
 
 [[package]]
@@ -3959,7 +3928,7 @@ dependencies = [
  "regalloc2",
  "smallvec",
  "target-lexicon",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime-environ",
 ]
 
@@ -3975,7 +3944,7 @@ dependencies = [
  "similar",
  "target-lexicon",
  "toml",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime-environ",
  "wat",
  "winch-codegen",
@@ -4005,7 +3974,7 @@ dependencies = [
  "similar",
  "target-lexicon",
  "toml",
- "wasmparser 0.200.0",
+ "wasmparser",
  "wasmtime-environ",
  "wat",
  "winch-codegen",
@@ -4175,9 +4144,9 @@ dependencies = [
 
 [[package]]
 name = "wit-bindgen"
-version = "0.17.0"
+version = "0.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6237168d93497b26dacdab157b08ad2787d74cdce10f89735f791b2a225eba4d"
+checksum = "a4ee004d9efcb9931de5eb7ac424d6f32c176ad81297636302b66e4fedf7bd02"
 dependencies = [
  "bitflags 2.4.1",
  "wit-bindgen-rust-macro",
@@ -4185,33 +4154,32 @@ dependencies = [
 
 [[package]]
 name = "wit-bindgen-core"
-version = "0.17.0"
+version = "0.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3d22547a645a7d3e4311b977a34f12e1d47df48125dc06e11e45984c99282ed"
+checksum = "81b9b74b29ab2b8652cd8fbefcf93616bd5415088b539f74edb277d94a3d9765"
 dependencies = [
  "anyhow",
- "wit-component 0.20.1",
- "wit-parser 0.13.1",
+ "wit-parser",
 ]
 
 [[package]]
 name = "wit-bindgen-rust"
-version = "0.17.0"
+version = "0.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6a691b95d07cc4e7f7aa259b480f7f0208c4ded49406fd4f8bfd5a5a61c5db1"
+checksum = "bafb494693e7380aafc96a4911c6f4e57abc507b5f9694b7c5fbe88b07101f7a"
 dependencies = [
  "anyhow",
  "heck",
- "wasm-metadata 0.10.20",
+ "wasm-metadata",
  "wit-bindgen-core",
- "wit-component 0.20.1",
+ "wit-component",
 ]
 
 [[package]]
 name = "wit-bindgen-rust-macro"
-version = "0.17.0"
+version = "0.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "763cc6195d4ede622e5388051e381d397930016c8dc2f1199d3b48eb26781b6a"
+checksum = "b62c1deb957177fb2f072e33ce24016d68162bc3863661dec85e9b81ae91900f"
 dependencies = [
  "anyhow",
  "proc-macro2",
@@ -4219,26 +4187,6 @@ dependencies = [
  "syn 2.0.32",
  "wit-bindgen-core",
  "wit-bindgen-rust",
- "wit-component 0.20.1",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.20.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "331de496d439010797c17637d8002712b9b69110f1669164c09dfa226ad277bb"
-dependencies = [
- "anyhow",
- "bitflags 2.4.1",
- "indexmap 2.0.0",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder 0.41.2",
- "wasm-metadata 0.10.20",
- "wasmparser 0.121.2",
- "wit-parser 0.13.1",
 ]
 
 [[package]]
@@ -4254,27 +4202,10 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "wasm-encoder 0.200.0",
- "wasm-metadata 0.200.0",
- "wasmparser 0.200.0",
- "wit-parser 0.200.0",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df4913a2219096373fd6512adead1fb77ecdaa59d7fc517972a7d30b12f625be"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap 2.0.0",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
 ]
 
 [[package]]
@@ -4292,7 +4223,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "unicode-xid",
- "wasmparser 0.200.0",
+ "wasmparser",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 1f3059c09..76cc5a55c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -121,7 +121,7 @@ members = [
   "examples/component/wasm",
   "fuzz",
   "winch",
-  "winch/codegen",
+  "winch/codegen", "crates/slab",
 ]
 exclude = [
   'docs/rust_wasi_markdown_parser',
@@ -173,6 +173,7 @@ wasmtime-component-util = { path = "crates/component-util", version = "=19.0.0"
 wasmtime-component-macro = { path = "crates/component-macro", version = "=19.0.0" }
 wasmtime-asm-macros = { path = "crates/asm-macros", version = "=19.0.0" }
 wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", version = "=19.0.0" }
+wasmtime-slab = { path = "crates/slab", version = "=19.0.0" }
 component-test-util = { path = "crates/misc/component-test-util" }
 component-fuzz-util = { path = "crates/misc/component-fuzz-util" }
 wiggle = { path = "crates/wiggle", version = "=19.0.0", default-features = false }
@@ -225,7 +226,7 @@ io-lifetimes = { version = "2.0.3", default-features = false }
 io-extras = "0.18.1"
 rustix = "0.38.31"
 # wit-bindgen:
-wit-bindgen = { version = "0.17.0", default-features = false }
+wit-bindgen = { version = "0.19.1", default-features = false }
 
 # wasm-tools family:
 wasmparser = "0.200.0"
diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml
index 6b470b15c..2e6942f45 100644
--- a/crates/c-api/Cargo.toml
+++ b/crates/c-api/Cargo.toml
@@ -22,7 +22,7 @@ doctest = false
 env_logger = { workspace = true, optional = true }
 anyhow = { workspace = true }
 once_cell = { workspace = true }
-wasmtime = { workspace = true, features = ['cranelift', 'runtime'] }
+wasmtime = { workspace = true, features = ['cranelift', 'runtime', 'gc'] }
 wasmtime-c-api-macros = { workspace = true }
 log = { workspace = true }
 tracing = { workspace = true }
diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml
index 67e02dbe9..543a50876 100644
--- a/crates/cli-flags/Cargo.toml
+++ b/crates/cli-flags/Cargo.toml
@@ -17,7 +17,7 @@ clap = { workspace = true }
 file-per-thread-logger = { workspace = true, optional = true }
 tracing-subscriber = { workspace = true, optional = true }
 rayon = { version = "1.5.0", optional = true }
-wasmtime = { workspace = true }
+wasmtime = { workspace = true, features = ["gc"] }
 humantime = { workspace = true }
 
 [features]
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index 199a4f082..2b03d21b7 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -39,7 +39,7 @@ fn init_file_per_thread_logger(prefix: &'static str) {
 wasmtime_option_group! {
     #[derive(PartialEq, Clone)]
     pub struct OptimizeOptions {
-        /// Optimization level of generated code (0-2, s; default: 0)
+        /// Optimization level of generated code (0-2, s; default: 2)
         pub opt_level: Option<wasmtime::OptLevel>,
 
         /// Byte size of the guard region after dynamic memories are allocated
@@ -59,6 +59,10 @@ wasmtime_option_group! {
         /// memories.
         pub dynamic_memory_reserved_for_growth: Option<u64>,
 
+        /// Indicates whether an unmapped region of memory is placed before all
+        /// linear memories.
+        pub guard_before_linear_memory: Option<bool>,
+
         /// Enable the pooling allocator, in place of the on-demand allocator.
         pub pooling_allocator: Option<bool>,
 
@@ -494,6 +498,9 @@ impl CommonOptions {
         if let Some(size) = self.opts.dynamic_memory_reserved_for_growth {
             config.dynamic_memory_reserved_for_growth(size);
         }
+        if let Some(enable) = self.opts.guard_before_linear_memory {
+            config.guard_before_linear_memory(enable);
+        }
 
         // If fuel has been configured, set the `consume fuel` flag on the config.
         if self.wasm.fuel.is_some() {
diff --git a/crates/cranelift-shared/src/isa_builder.rs b/crates/cranelift-shared/src/isa_builder.rs
index 94c241be2..1c01ed7ce 100644
--- a/crates/cranelift-shared/src/isa_builder.rs
+++ b/crates/cranelift-shared/src/isa_builder.rs
@@ -28,9 +28,12 @@ impl<T> IsaBuilder<T> {
             .set("enable_probestack", "false")
             .expect("should be valid flag");
 
+        let triple_specified = triple.is_some();
         let triple = triple.unwrap_or_else(Triple::host);
         let mut isa_flags = lookup(triple)?;
-        cranelift_native::infer_native_flags(&mut isa_flags).unwrap();
+        if !triple_specified {
+            cranelift_native::infer_native_flags(&mut isa_flags).unwrap();
+        }
 
         Ok(Self {
             shared_flags: flags,
diff --git a/crates/cranelift/Cargo.toml b/crates/cranelift/Cargo.toml
index cde0d4e71..2c5ce4845 100644
--- a/crates/cranelift/Cargo.toml
+++ b/crates/cranelift/Cargo.toml
@@ -38,3 +38,4 @@ host-arch = ["cranelift-codegen/host-arch"]
 component-model = ["wasmtime-environ/component-model"]
 incremental-cache = ["cranelift-codegen/incremental-cache"]
 wmemcheck = []
+gc = ["wasmtime-environ/gc"]
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index 51bca04ad..57b965c3a 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -34,26 +34,41 @@ macro_rules! declare_function_signatures {
     ) => {
         /// A struct with an `Option<ir::SigRef>` member for every builtin
         /// function, to de-duplicate constructing/getting its signature.
+        #[allow(unused_doc_comments)]
         struct BuiltinFunctionSignatures {
             pointer_type: ir::Type,
+
+            #[cfg(feature = "gc")]
             reference_type: ir::Type,
+
             call_conv: isa::CallConv,
+
             $(
+                $( #[$attr] )*
                 $name: Option<ir::SigRef>,
             )*
         }
 
+        #[allow(unused_doc_comments)]
         impl BuiltinFunctionSignatures {
             fn new(
                 pointer_type: ir::Type,
                 reference_type: ir::Type,
                 call_conv: isa::CallConv,
             ) -> Self {
+                #[cfg(not(feature = "gc"))]
+                let _ = reference_type;
+
                 Self {
                     pointer_type,
+
+                    #[cfg(feature = "gc")]
                     reference_type,
+
                     call_conv,
+
                     $(
+                        $( #[$attr] )*
                         $name: None,
                     )*
                 }
@@ -63,6 +78,7 @@ macro_rules! declare_function_signatures {
                 AbiParam::special(self.pointer_type, ArgumentPurpose::VMContext)
             }
 
+            #[cfg(feature = "gc")]
             fn reference(&self) -> AbiParam {
                 AbiParam::new(self.reference_type)
             }
@@ -89,6 +105,7 @@ macro_rules! declare_function_signatures {
             }
 
             $(
+                $( #[$attr] )*
                 fn $name(&mut self, func: &mut Function) -> ir::SigRef {
                     let sig = self.$name.unwrap_or_else(|| {
                         func.import_signature(Signature {
@@ -336,6 +353,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
     /// reference count.
     ///
     /// The new reference count is returned.
+    #[cfg(feature = "gc")]
     fn mutate_externref_ref_count(
         &mut self,
         builder: &mut FunctionBuilder,
@@ -1324,11 +1342,19 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                     self.builtin_function_signatures
                         .table_grow_func_ref(&mut pos.func),
                 ),
+                #[cfg(feature = "gc")]
                 WasmHeapType::Extern => (
                     BuiltinFunctionIndex::table_grow_externref(),
                     self.builtin_function_signatures
                         .table_grow_externref(&mut pos.func),
                 ),
+                #[cfg(not(feature = "gc"))]
+                WasmHeapType::Extern => {
+                    return Err(cranelift_wasm::wasm_unsupported!(
+                        "support for `externref` disabled at compile time because \
+                     the `gc` cargo feature was not enabled",
+                    ))
+                }
             };
 
         let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx);
@@ -1350,8 +1376,6 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         table: ir::Table,
         index: ir::Value,
     ) -> WasmResult<ir::Value> {
-        let pointer_type = self.pointer_type();
-
         let plan = &self.module.table_plans[table_index];
         match plan.table.wasm_ty.heap_type {
             WasmHeapType::Func | WasmHeapType::Concrete(_) | WasmHeapType::NoFunc => match plan
@@ -1361,6 +1385,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                     Ok(self.get_or_init_func_ref_table_elem(builder, table_index, table, index))
                 }
             },
+            #[cfg(feature = "gc")]
             WasmHeapType::Extern => {
                 // Our read barrier for `externref` tables is roughly equivalent
                 // to the following pseudocode:
@@ -1382,6 +1407,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                 // onto the stack are safely held alive by the
                 // `VMExternRefActivationsTable`.
 
+                let pointer_type = self.pointer_type();
                 let reference_type = self.reference_type(WasmHeapType::Extern);
 
                 builder.ensure_inserted_block();
@@ -1477,6 +1503,13 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
 
                 Ok(elem)
             }
+            #[cfg(not(feature = "gc"))]
+            WasmHeapType::Extern => {
+                return Err(cranelift_wasm::wasm_unsupported!(
+                    "support for `externref` disabled at compile time because the \
+                 `gc` cargo feature was not enabled",
+                ))
+            }
         }
     }
 
@@ -1510,6 +1543,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                 }
             },
 
+            #[cfg(feature = "gc")]
             WasmHeapType::Extern => {
                 // Our write barrier for `externref`s being copied out of the
                 // stack and into a table is roughly equivalent to the following
@@ -1640,6 +1674,14 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
 
                 Ok(())
             }
+
+            #[cfg(not(feature = "gc"))]
+            WasmHeapType::Extern => {
+                return Err(cranelift_wasm::wasm_unsupported!(
+                    "support for `externref` disabled at compile time because the \
+                     `gc` cargo feature was not enabled",
+                ))
+            }
         }
     }
 
@@ -1658,11 +1700,19 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
                     self.builtin_function_signatures
                         .table_fill_func_ref(&mut pos.func),
                 ),
+                #[cfg(feature = "gc")]
                 WasmHeapType::Extern => (
                     BuiltinFunctionIndex::table_fill_externref(),
                     self.builtin_function_signatures
                         .table_fill_externref(&mut pos.func),
                 ),
+                #[cfg(not(feature = "gc"))]
+                WasmHeapType::Extern => {
+                    return Err(cranelift_wasm::wasm_unsupported!(
+                        "support for `externref` disabled at compile time because the \
+                         `gc` cargo feature was not enabled",
+                    ));
+                }
             };
 
         let (vmctx, builtin_addr) =
@@ -1727,6 +1777,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         Ok(pos.func.dfg.first_result(call_inst))
     }
 
+    #[cfg(feature = "gc")]
     fn translate_custom_global_get(
         &mut self,
         mut pos: cranelift_codegen::cursor::FuncCursor<'_>,
@@ -1754,6 +1805,24 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         Ok(pos.func.dfg.first_result(call_inst))
     }
 
+    #[cfg(not(feature = "gc"))]
+    fn translate_custom_global_get(
+        &mut self,
+        _pos: FuncCursor,
+        index: GlobalIndex,
+    ) -> WasmResult<ir::Value> {
+        debug_assert_eq!(
+            self.module.globals[index].wasm_ty,
+            WasmValType::Ref(WasmRefType::EXTERNREF),
+            "We only use GlobalVariable::Custom for externref"
+        );
+        Err(cranelift_wasm::wasm_unsupported!(
+            "support for `externref` disabled at compile time because the \
+             `gc` cargo feature was not enabled",
+        ))
+    }
+
+    #[cfg(feature = "gc")]
     fn translate_custom_global_set(
         &mut self,
         mut pos: cranelift_codegen::cursor::FuncCursor<'_>,
@@ -1781,6 +1850,24 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         Ok(())
     }
 
+    #[cfg(not(feature = "gc"))]
+    fn translate_custom_global_set(
+        &mut self,
+        _pos: FuncCursor,
+        index: GlobalIndex,
+        _value: ir::Value,
+    ) -> WasmResult<()> {
+        debug_assert_eq!(
+            self.module.globals[index].wasm_ty,
+            WasmValType::Ref(WasmRefType::EXTERNREF),
+            "We only use GlobalVariable::Custom for externref"
+        );
+        Err(cranelift_wasm::wasm_unsupported!(
+            "support for `externref` disabled at compile time because the \
+             `gc` cargo feature was not enabled",
+        ))
+    }
+
     fn make_heap(&mut self, func: &mut ir::Function, index: MemoryIndex) -> WasmResult<Heap> {
         let pointer_type = self.pointer_type();
         let is_shared = self.module.memory_plans[index].memory.shared;
diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml
index cb471956b..500931122 100644
--- a/crates/environ/Cargo.toml
+++ b/crates/environ/Cargo.toml
@@ -49,3 +49,4 @@ component-model = [
   "dep:wasmtime-component-util",
 ]
 demangle = ['dep:rustc-demangle', 'dep:cpp_demangle']
+gc = []
diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs
index 07a5a03ab..94abe9377 100644
--- a/crates/environ/src/builtin.rs
+++ b/crates/environ/src/builtin.rs
@@ -26,21 +26,8 @@ macro_rules! foreach_builtin_function {
             table_get_lazy_init_func_ref(vmctx: vmctx, table: i32, index: i32) -> pointer;
             /// Returns an index for Wasm's `table.grow` instruction for `funcref`s.
             table_grow_func_ref(vmctx: vmctx, table: i32, delta: i32, init: pointer) -> i32;
-            /// Returns an index for Wasm's `table.grow` instruction for `externref`s.
-            table_grow_externref(vmctx: vmctx, table: i32, delta: i32, init: reference) -> i32;
-            /// Returns an index for Wasm's `table.fill` instruction for `externref`s.
-            table_fill_externref(vmctx: vmctx, table: i32, dst: i32, val: reference, len: i32);
             /// Returns an index for Wasm's `table.fill` instruction for `funcref`s.
             table_fill_func_ref(vmctx: vmctx, table: i32, dst: i32, val: pointer, len: i32);
-            /// Returns an index to drop a `VMExternRef`.
-            drop_externref(vmctx: vmctx, val: pointer);
-            /// Returns an index to do a GC and then insert a `VMExternRef` into the
-            /// `VMExternRefActivationsTable`.
-            activations_table_insert_with_gc(vmctx: vmctx, val: reference);
-            /// Returns an index for Wasm's `global.get` instruction for `externref`s.
-            externref_global_get(vmctx: vmctx, global: i32) -> reference;
-            /// Returns an index for Wasm's `global.get` instruction for `externref`s.
-            externref_global_set(vmctx: vmctx, global: i32, val: reference);
             /// Returns an index for wasm's `memory.atomic.notify` instruction.
             memory_atomic_notify(vmctx: vmctx, memory: i32, addr: i64, count: i32) -> i32;
             /// Returns an index for wasm's `memory.atomic.wait32` instruction.
@@ -67,6 +54,31 @@ macro_rules! foreach_builtin_function {
             update_stack_pointer(vmctx: vmctx, value: i32);
             /// Invoked before memory.grow is called.
             update_mem_size(vmctx: vmctx, num_bytes: i32);
+
+            /// Returns an index to drop a `VMExternRef`.
+            #[cfg(feature = "gc")]
+            drop_externref(vmctx: vmctx, val: pointer);
+
+            /// Returns an index to do a GC and then insert a `VMExternRef` into the
+            /// `VMExternRefActivationsTable`.
+            #[cfg(feature = "gc")]
+            activations_table_insert_with_gc(vmctx: vmctx, val: reference);
+
+            /// Returns an index for Wasm's `global.get` instruction for `externref`s.
+            #[cfg(feature = "gc")]
+            externref_global_get(vmctx: vmctx, global: i32) -> reference;
+
+            /// Returns an index for Wasm's `global.get` instruction for `externref`s.
+            #[cfg(feature = "gc")]
+            externref_global_set(vmctx: vmctx, global: i32, val: reference);
+
+            /// Returns an index for Wasm's `table.grow` instruction for `externref`s.
+            #[cfg(feature = "gc")]
+            table_grow_externref(vmctx: vmctx, table: i32, delta: i32, init: reference) -> i32;
+
+            /// Returns an index for Wasm's `table.fill` instruction for `externref`s.
+            #[cfg(feature = "gc")]
+            table_fill_externref(vmctx: vmctx, table: i32, dst: i32, val: reference, len: i32);
         }
     };
 }
diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml
index 6828481bb..7a7c18f8c 100644
--- a/crates/fuzzing/Cargo.toml
+++ b/crates/fuzzing/Cargo.toml
@@ -22,7 +22,7 @@ target-lexicon = { workspace = true }
 tempfile = "3.3.0"
 wasmparser = { workspace = true }
 wasmprinter = { workspace = true }
-wasmtime = { workspace = true, features = ['default', 'winch'] }
+wasmtime = { workspace = true, features = ['default', 'winch', 'gc'] }
 wasmtime-wast = { workspace = true }
 wasm-encoder = { workspace = true }
 wasm-smith = { workspace = true }
diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml
index 3738335b4..364668577 100644
--- a/crates/runtime/Cargo.toml
+++ b/crates/runtime/Cargo.toml
@@ -70,3 +70,4 @@ pooling-allocator = []
 component-model = ["wasmtime-environ/component-model", "dep:encoding_rs"]
 wmemcheck = []
 debug-builtins = ['wasmtime-jit-debug']
+gc = ["wasmtime-environ/gc"]
diff --git a/crates/runtime/src/externref.rs b/crates/runtime/src/externref.rs
index 2ba82944c..547a87410 100644
--- a/crates/runtime/src/externref.rs
+++ b/crates/runtime/src/externref.rs
@@ -1,796 +1,14 @@
-//! # `VMExternRef`
-//!
-//! `VMExternRef` is a reference-counted box for any kind of data that is
-//! external and opaque to running Wasm. Sometimes it might hold a Wasmtime
-//! thing, other times it might hold something from a Wasmtime embedder and is
-//! opaque even to us. It is morally equivalent to `Rc<dyn Any>` in Rust, but
-//! additionally always fits in a pointer-sized word. `VMExternRef` is
-//! non-nullable, but `Option<VMExternRef>` is a null pointer.
-//!
-//! The one part of `VMExternRef` that can't ever be opaque to us is the
-//! reference count. Even when we don't know what's inside an `VMExternRef`, we
-//! need to be able to manipulate its reference count as we add and remove
-//! references to it. And we need to do this from compiled Wasm code, so it must
-//! be `repr(C)`!
-//!
-//! ## Memory Layout
-//!
-//! `VMExternRef` itself is just a pointer to an `VMExternData`, which holds the
-//! opaque, boxed value, its reference count, and its vtable pointer.
-//!
-//! The `VMExternData` struct is *preceded* by the dynamically-sized value boxed
-//! up and referenced by one or more `VMExternRef`s:
-//!
-//! ```text
-//!      ,-------------------------------------------------------.
-//!      |                                                       |
-//!      V                                                       |
-//!     +----------------------------+-----------+-----------+   |
-//!     | dynamically-sized value... | ref_count | value_ptr |---'
-//!     +----------------------------+-----------+-----------+
-//!                                  | VMExternData          |
-//!                                  +-----------------------+
-//!                                   ^
-//! +-------------+                   |
-//! | VMExternRef |-------------------+
-//! +-------------+                   |
-//!                                   |
-//! +-------------+                   |
-//! | VMExternRef |-------------------+
-//! +-------------+                   |
-//!                                   |
-//!   ...                            ===
-//!                                   |
-//! +-------------+                   |
-//! | VMExternRef |-------------------'
-//! +-------------+
-//! ```
-//!
-//! The `value_ptr` member always points backwards to the start of the
-//! dynamically-sized value (which is also the start of the heap allocation for
-//! this value-and-`VMExternData` pair). Because it is a `dyn` pointer, it is
-//! fat, and also points to the value's `Any` vtable.
-//!
-//! The boxed value and the `VMExternRef` footer are held a single heap
-//! allocation. The layout described above is used to make satisfying the
-//! value's alignment easy: we just need to ensure that the heap allocation used
-//! to hold everything satisfies its alignment. It also ensures that we don't
-//! need a ton of excess padding between the `VMExternData` and the value for
-//! values with large alignment.
-//!
-//! ## Reference Counting, Wasm Functions, and Garbage Collection
-//!
-//! For host VM code, we use plain reference counting, where cloning increments
-//! the reference count, and dropping decrements it. We can avoid many of the
-//! on-stack increment/decrement operations that typically plague the
-//! performance of reference counting via Rust's ownership and borrowing system.
-//! Moving a `VMExternRef` avoids mutating its reference count, and borrowing it
-//! either avoids the reference count increment or delays it until if/when the
-//! `VMExternRef` is cloned.
-//!
-//! When passing a `VMExternRef` into compiled Wasm code, we don't want to do
-//! reference count mutations for every compiled `local.{get,set}`, nor for
-//! every function call. Therefore, we use a variation of **deferred reference
-//! counting**, where we only mutate reference counts when storing
-//! `VMExternRef`s somewhere that outlives the activation: into a global or
-//! table. Simultaneously, we over-approximate the set of `VMExternRef`s that
-//! are inside Wasm function activations. Periodically, we walk the stack at GC
-//! safe points, and use stack map information to precisely identify the set of
-//! `VMExternRef`s inside Wasm activations. Then we take the difference between
-//! this precise set and our over-approximation, and decrement the reference
-//! count for each of the `VMExternRef`s that are in our over-approximation but
-//! not in the precise set. Finally, the over-approximation is replaced with the
-//! precise set.
-//!
-//! The `VMExternRefActivationsTable` implements the over-approximized set of
-//! `VMExternRef`s referenced by Wasm activations. Calling a Wasm function and
-//! passing it a `VMExternRef` moves the `VMExternRef` into the table, and the
-//! compiled Wasm function logically "borrows" the `VMExternRef` from the
-//! table. Similarly, `global.get` and `table.get` operations clone the gotten
-//! `VMExternRef` into the `VMExternRefActivationsTable` and then "borrow" the
-//! reference out of the table.
-//!
-//! When a `VMExternRef` is returned to host code from a Wasm function, the host
-//! increments the reference count (because the reference is logically
-//! "borrowed" from the `VMExternRefActivationsTable` and the reference count
-//! from the table will be dropped at the next GC).
-//!
-//! For more general information on deferred reference counting, see *An
-//! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
-//! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
+#[cfg(feature = "gc")]
+mod gc;
+#[cfg(feature = "gc")]
+pub use gc::*;
 
-use crate::{Backtrace, SendSyncPtr, VMRuntimeLimits};
-use std::alloc::Layout;
-use std::any::Any;
-use std::cell::UnsafeCell;
-use std::cmp;
-use std::collections::HashSet;
-use std::hash::{Hash, Hasher};
-use std::mem;
-use std::ops::Deref;
-use std::ptr::{self, NonNull};
-use std::sync::atomic::{self, AtomicUsize, Ordering};
-use wasmtime_environ::StackMap;
-
-/// An external reference to some opaque data.
-///
-/// `VMExternRef`s dereference to their underlying opaque data as `dyn Any`.
-///
-/// Unlike the `externref` in the Wasm spec, `VMExternRef`s are non-nullable,
-/// and always point to a valid value. You may use `Option<VMExternRef>` to
-/// represent nullable references, and `Option<VMExternRef>` is guaranteed to
-/// have the same size and alignment as a raw pointer, with `None` represented
-/// with the null pointer.
-///
-/// `VMExternRef`s are reference counted, so cloning is a cheap, shallow
-/// operation. It also means they are inherently shared, so you may not get a
-/// mutable, exclusive reference to their inner contents, only a shared,
-/// immutable reference. You may use interior mutability with `RefCell` or
-/// `Mutex` to work around this restriction, if necessary.
-///
-/// `VMExternRef`s have pointer-equality semantics, not structural-equality
-/// semantics. Given two `VMExternRef`s `a` and `b`, `a == b` only if `a` and
-/// `b` point to the same allocation. `a` and `b` are considered not equal, even
-/// if `a` and `b` are two different identical copies of the same data, if they
-/// are in two different allocations. The hashing and ordering implementations
-/// also only operate on the pointer.
-///
-/// # Example
-///
-/// ```
-/// # fn foo() -> Result<(), Box<dyn std::error::Error>> {
-/// use std::cell::RefCell;
-/// use wasmtime_runtime::VMExternRef;
-///
-/// // Open a file. Wasm doesn't know about files, but we can let Wasm instances
-/// // work with files via opaque `externref` handles.
-/// let file = std::fs::File::create("some/file/path")?;
-///
-/// // Wrap the file up as an `VMExternRef` that can be passed to Wasm.
-/// let extern_ref_to_file = VMExternRef::new(file);
-///
-/// // `VMExternRef`s dereference to `dyn Any`, so you can use `Any` methods to
-/// // perform runtime type checks and downcasts.
-///
-/// assert!(extern_ref_to_file.is::<std::fs::File>());
-/// assert!(!extern_ref_to_file.is::<String>());
-///
-/// if let Some(mut file) = extern_ref_to_file.downcast_ref::<std::fs::File>() {
-///     use std::io::Write;
-///     writeln!(&mut file, "Hello, `VMExternRef`!")?;
-/// }
-/// # Ok(())
-/// # }
-/// ```
-#[derive(Debug)]
-#[repr(transparent)]
-pub struct VMExternRef(SendSyncPtr<VMExternData>);
-
-impl std::fmt::Pointer for VMExternRef {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        std::fmt::Pointer::fmt(&self.0, f)
-    }
-}
-
-#[repr(C)]
-pub(crate) struct VMExternData {
-    // Implicit, dynamically-sized member that always preceded an
-    // `VMExternData`.
-    //
-    // value: [u8],
-    //
-    /// The reference count for this `VMExternData` and value. When it reaches
-    /// zero, we can safely destroy the value and free this heap
-    /// allocation. This is an `UnsafeCell`, rather than plain `Cell`, because
-    /// it can be modified by compiled Wasm code.
-    ///
-    /// Note: this field's offset must be kept in sync with
-    /// `wasmtime_environ::VMOffsets::vm_extern_data_ref_count()` which is
-    /// currently always zero.
-    ref_count: AtomicUsize,
-
-    /// Always points to the implicit, dynamically-sized `value` member that
-    /// precedes this `VMExternData`.
-    value_ptr: SendSyncPtr<dyn Any + Send + Sync>,
-}
-
-impl Clone for VMExternRef {
-    #[inline]
-    fn clone(&self) -> VMExternRef {
-        self.extern_data().increment_ref_count();
-        VMExternRef(self.0)
-    }
-}
-
-impl Drop for VMExternRef {
-    #[inline]
-    fn drop(&mut self) {
-        let data = self.extern_data();
-
-        // Note that the memory orderings here also match the standard library
-        // itself. Documentation is more available in the implementation of
-        // `Arc`, but the general idea is that this is a special pattern allowed
-        // by the C standard with atomic orderings where we "release" for all
-        // the decrements and only the final decrementer performs an acquire
-        // fence. This properly ensures that the final thread, which actually
-        // destroys the data, sees all the updates from all other threads.
-        if data.ref_count.fetch_sub(1, Ordering::Release) != 1 {
-            return;
-        }
-        atomic::fence(Ordering::Acquire);
-
-        unsafe {
-            VMExternData::drop_and_dealloc(self.0);
-        }
-    }
-}
-
-impl VMExternData {
-    /// Get the `Layout` for a value with the given size and alignment, and the
-    /// offset within that layout where the `VMExternData` footer resides.
-    ///
-    /// This doesn't take a `value: &T` because `VMExternRef::new_with` hasn't
-    /// constructed a `T` value yet, and it isn't generic over `T` because
-    /// `VMExternData::drop_and_dealloc` doesn't know what `T` to use, and has
-    /// to use `std::mem::{size,align}_of_val` instead.
-    unsafe fn layout_for(value_size: usize, value_align: usize) -> (Layout, usize) {
-        let extern_data_size = mem::size_of::<VMExternData>();
-        let extern_data_align = mem::align_of::<VMExternData>();
-
-        let value_and_padding_size = round_up_to_align(value_size, extern_data_align).unwrap();
-
-        let alloc_align = std::cmp::max(value_align, extern_data_align);
-        let alloc_size = value_and_padding_size + extern_data_size;
-
-        debug_assert!(
-            Layout::from_size_align(alloc_size, alloc_align).is_ok(),
-            "should create a `Layout` for size={} and align={} okay",
-            alloc_size,
-            alloc_align,
-        );
-        (
-            Layout::from_size_align_unchecked(alloc_size, alloc_align),
-            value_and_padding_size,
-        )
-    }
-
-    /// Drop the inner value and then free this `VMExternData` heap allocation.
-    pub(crate) unsafe fn drop_and_dealloc(mut data: SendSyncPtr<VMExternData>) {
-        log::trace!("Dropping externref data @ {:p}", data);
-
-        // Note: we introduce a block scope so that we drop the live
-        // reference to the data before we free the heap allocation it
-        // resides within after this block.
-        let (alloc_ptr, layout) = {
-            let data = data.as_mut();
-            debug_assert_eq!(data.ref_count.load(Ordering::SeqCst), 0);
-
-            // Same thing, but for the dropping the reference to `value` before
-            // we drop it itself.
-            let (layout, _) = {
-                let value = data.value_ptr.as_ref();
-                Self::layout_for(mem::size_of_val(value), mem::align_of_val(value))
-            };
-
-            ptr::drop_in_place(data.value_ptr.as_ptr());
-            let alloc_ptr = data.value_ptr.as_ptr().cast::<u8>();
-
-            (alloc_ptr, layout)
-        };
-
-        ptr::drop_in_place(data.as_ptr());
-        std::alloc::dealloc(alloc_ptr, layout);
-    }
-
-    #[inline]
-    fn increment_ref_count(&self) {
-        // This is only using during cloning operations, and like the standard
-        // library we use `Relaxed` here. The rationale is better documented in
-        // libstd's implementation of `Arc`, but the general gist is that we're
-        // creating a new pointer for our own thread, so there's no need to have
-        // any synchronization with orderings. The synchronization with other
-        // threads with respect to orderings happens when the pointer is sent to
-        // another thread.
-        self.ref_count.fetch_add(1, Ordering::Relaxed);
-    }
-}
-
-#[inline]
-fn round_up_to_align(n: usize, align: usize) -> Option<usize> {
-    debug_assert!(align.is_power_of_two());
-    let align_minus_one = align - 1;
-    Some(n.checked_add(align_minus_one)? & !align_minus_one)
-}
-
-impl VMExternRef {
-    /// Wrap the given value inside an `VMExternRef`.
-    pub fn new<T>(value: T) -> VMExternRef
-    where
-        T: 'static + Any + Send + Sync,
-    {
-        VMExternRef::new_with(|| value)
-    }
-
-    /// Construct a new `VMExternRef` in place by invoking `make_value`.
-    pub fn new_with<T>(make_value: impl FnOnce() -> T) -> VMExternRef
-    where
-        T: 'static + Any + Send + Sync,
-    {
-        unsafe {
-            let (layout, footer_offset) =
-                VMExternData::layout_for(mem::size_of::<T>(), mem::align_of::<T>());
-
-            let alloc_ptr = std::alloc::alloc(layout);
-            let alloc_ptr = NonNull::new(alloc_ptr).unwrap_or_else(|| {
-                std::alloc::handle_alloc_error(layout);
-            });
-
-            let value_ptr = alloc_ptr.cast::<T>();
-            ptr::write(value_ptr.as_ptr(), make_value());
-
-            let extern_data_ptr =
-                alloc_ptr.cast::<u8>().as_ptr().add(footer_offset) as *mut VMExternData;
-
-            ptr::write(
-                extern_data_ptr,
-                VMExternData {
-                    ref_count: AtomicUsize::new(1),
-                    // Cast from `*mut T` to `*mut dyn Any` here.
-                    value_ptr: SendSyncPtr::new(NonNull::new_unchecked(value_ptr.as_ptr())),
-                },
-            );
-
-            log::trace!("New externref data @ {:p}", extern_data_ptr);
-            VMExternRef(NonNull::new_unchecked(extern_data_ptr).into())
-        }
-    }
-
-    /// Turn this `VMExternRef` into a raw, untyped pointer.
-    ///
-    /// Unlike `into_raw`, this does not consume and forget `self`. It is *not*
-    /// safe to use `from_raw` on pointers returned from this method; only use
-    /// `clone_from_raw`!
-    ///
-    ///  Nor does this method increment the reference count. You must ensure
-    ///  that `self` (or some other clone of `self`) stays alive until
-    ///  `clone_from_raw` is called.
-    #[inline]
-    pub fn as_raw(&self) -> *mut u8 {
-        let ptr = self.0.as_ptr().cast::<u8>();
-        ptr
-    }
-
-    /// Consume this `VMExternRef` into a raw, untyped pointer.
-    ///
-    /// # Safety
-    ///
-    /// This method forgets self, so it is possible to create a leak of the
-    /// underlying reference counted data if not used carefully.
-    ///
-    /// Use `from_raw` to recreate the `VMExternRef`.
-    pub unsafe fn into_raw(self) -> *mut u8 {
-        let ptr = self.0.as_ptr().cast::<u8>();
-        std::mem::forget(self);
-        ptr
-    }
-
-    /// Recreate a `VMExternRef` from a pointer returned from a previous call to
-    /// `as_raw`.
-    ///
-    /// # Safety
-    ///
-    /// Unlike `clone_from_raw`, this does not increment the reference count of the
-    /// underlying data.  It is not safe to continue to use the pointer passed to this
-    /// function.
-    pub unsafe fn from_raw(ptr: *mut u8) -> Self {
-        debug_assert!(!ptr.is_null());
-        VMExternRef(NonNull::new_unchecked(ptr).cast().into())
-    }
-
-    /// Recreate a `VMExternRef` from a pointer returned from a previous call to
-    /// `as_raw`.
-    ///
-    /// # Safety
-    ///
-    /// Wildly unsafe to use with anything other than the result of a previous
-    /// `as_raw` call!
-    ///
-    /// Additionally, it is your responsibility to ensure that this raw
-    /// `VMExternRef`'s reference count has not dropped to zero. Failure to do
-    /// so will result in use after free!
-    pub unsafe fn clone_from_raw(ptr: *mut u8) -> Self {
-        debug_assert!(!ptr.is_null());
-        let x = VMExternRef(NonNull::new_unchecked(ptr).cast().into());
-        x.extern_data().increment_ref_count();
-        x
-    }
-
-    /// Get the strong reference count for this `VMExternRef`.
-    ///
-    /// Note that this loads with a `SeqCst` ordering to synchronize with other
-    /// threads.
-    pub fn strong_count(&self) -> usize {
-        self.extern_data().ref_count.load(Ordering::SeqCst)
-    }
-
-    #[inline]
-    fn extern_data(&self) -> &VMExternData {
-        unsafe { self.0.as_ref() }
-    }
-}
-
-/// Methods that would normally be trait implementations, but aren't to avoid
-/// potential footguns around `VMExternRef`'s pointer-equality semantics.
-///
-/// Note that none of these methods are on `&self`, they all require a
-/// fully-qualified `VMExternRef::foo(my_ref)` invocation.
-impl VMExternRef {
-    /// Check whether two `VMExternRef`s point to the same inner allocation.
-    ///
-    /// Note that this uses pointer-equality semantics, not structural-equality
-    /// semantics, and so only pointers are compared, and doesn't use any `Eq`
-    /// or `PartialEq` implementation of the pointed-to values.
-    #[inline]
-    pub fn eq(a: &Self, b: &Self) -> bool {
-        ptr::eq(a.0.as_ptr(), b.0.as_ptr())
-    }
-
-    /// Hash a given `VMExternRef`.
-    ///
-    /// Note that this just hashes the pointer to the inner value, it does *not*
-    /// use the inner value's `Hash` implementation (if any).
-    #[inline]
-    pub fn hash<H>(externref: &Self, hasher: &mut H)
-    where
-        H: Hasher,
-    {
-        ptr::hash(externref.0.as_ptr(), hasher);
-    }
-
-    /// Compare two `VMExternRef`s.
-    ///
-    /// Note that this uses pointer-equality semantics, not structural-equality
-    /// semantics, and so only pointers are compared, and doesn't use any `Cmp`
-    /// or `PartialCmp` implementation of the pointed-to values.
-    #[inline]
-    pub fn cmp(a: &Self, b: &Self) -> cmp::Ordering {
-        let a = a.0.as_ptr() as usize;
-        let b = b.0.as_ptr() as usize;
-        a.cmp(&b)
-    }
-}
-
-impl Deref for VMExternRef {
-    type Target = dyn Any;
-
-    fn deref(&self) -> &dyn Any {
-        unsafe { self.extern_data().value_ptr.as_ref() }
-    }
-}
-
-/// A wrapper around a `VMExternRef` that implements `Eq` and `Hash` with
-/// pointer semantics.
-///
-/// We use this so that we can morally put `VMExternRef`s inside of `HashSet`s
-/// even though they don't implement `Eq` and `Hash` to avoid foot guns.
-#[derive(Clone, Debug)]
-struct VMExternRefWithTraits(VMExternRef);
-
-impl Hash for VMExternRefWithTraits {
-    fn hash<H>(&self, hasher: &mut H)
-    where
-        H: Hasher,
-    {
-        VMExternRef::hash(&self.0, hasher)
-    }
-}
-
-impl PartialEq for VMExternRefWithTraits {
-    fn eq(&self, other: &Self) -> bool {
-        VMExternRef::eq(&self.0, &other.0)
-    }
-}
-
-impl Eq for VMExternRefWithTraits {}
-
-type TableElem = UnsafeCell<Option<VMExternRef>>;
-
-/// A table that over-approximizes the set of `VMExternRef`s that any Wasm
-/// activation on this thread is currently using.
-///
-/// Under the covers, this is a simple bump allocator that allows duplicate
-/// entries. Deduplication happens at GC time.
-#[repr(C)] // `alloc` must be the first member, it's accessed from JIT code.
-pub struct VMExternRefActivationsTable {
-    /// Structures used to perform fast bump allocation of storage of externref
-    /// values.
-    ///
-    /// This is the only member of this structure accessed from JIT code.
-    alloc: VMExternRefTableAlloc,
-
-    /// When unioned with `chunk`, this is an over-approximation of the GC roots
-    /// on the stack, inside Wasm frames.
-    ///
-    /// This is used by slow-path insertion, and when a GC cycle finishes, is
-    /// re-initialized to the just-discovered precise set of stack roots (which
-    /// immediately becomes an over-approximation again as soon as Wasm runs and
-    /// potentially drops references).
-    over_approximated_stack_roots: HashSet<VMExternRefWithTraits>,
-
-    /// The precise set of on-stack, inside-Wasm GC roots that we discover via
-    /// walking the stack and interpreting stack maps.
-    ///
-    /// This is *only* used inside the `gc` function, and is empty otherwise. It
-    /// is just part of this struct so that we can reuse the allocation, rather
-    /// than create a new hash set every GC.
-    precise_stack_roots: HashSet<VMExternRefWithTraits>,
-
-    /// A debug-only field for asserting that we are in a region of code where
-    /// GC is okay to preform.
-    #[cfg(debug_assertions)]
-    gc_okay: bool,
-}
-
-#[repr(C)] // This is accessed from JIT code.
-struct VMExternRefTableAlloc {
-    /// Bump-allocation finger within the `chunk`.
-    ///
-    /// NB: this is an `UnsafeCell` because it is written to by compiled Wasm
-    /// code.
-    next: UnsafeCell<NonNull<TableElem>>,
-
-    /// Pointer to just after the `chunk`.
-    ///
-    /// This is *not* within the current chunk and therefore is not a valid
-    /// place to insert a reference!
-    end: NonNull<TableElem>,
-
-    /// Bump allocation chunk that stores fast-path insertions.
-    ///
-    /// This is not accessed from JIT code.
-    chunk: Box<[TableElem]>,
-}
-
-// This gets around the usage of `UnsafeCell` throughout the internals of this
-// allocator, but the storage should all be Send/Sync and synchronization isn't
-// necessary since operations require `&mut self`.
-unsafe impl Send for VMExternRefTableAlloc {}
-unsafe impl Sync for VMExternRefTableAlloc {}
-
-fn _assert_send_sync() {
-    fn _assert<T: Send + Sync>() {}
-    _assert::<VMExternRefActivationsTable>();
-    _assert::<VMExternRef>();
-}
-
-impl VMExternRefActivationsTable {
-    const CHUNK_SIZE: usize = 4096 / mem::size_of::<usize>();
-
-    /// Create a new `VMExternRefActivationsTable`.
-    pub fn new() -> Self {
-        // Start with an empty chunk in case this activations table isn't used.
-        // This means that there's no space in the bump-allocation area which
-        // will force any path trying to use this to the slow gc path. The first
-        // time this happens, though, the slow gc path will allocate a new chunk
-        // for actual fast-bumping.
-        let mut chunk: Box<[TableElem]> = Box::new([]);
-        let next = chunk.as_mut_ptr();
-        let end = unsafe { next.add(chunk.len()) };
+#[cfg(not(feature = "gc"))]
+mod no_gc;
+#[cfg(not(feature = "gc"))]
+pub use no_gc::*;
 
-        VMExternRefActivationsTable {
-            alloc: VMExternRefTableAlloc {
-                next: UnsafeCell::new(NonNull::new(next).unwrap()),
-                end: NonNull::new(end).unwrap(),
-                chunk,
-            },
-            over_approximated_stack_roots: HashSet::new(),
-            precise_stack_roots: HashSet::new(),
-            #[cfg(debug_assertions)]
-            gc_okay: true,
-        }
-    }
-
-    fn new_chunk(size: usize) -> Box<[UnsafeCell<Option<VMExternRef>>]> {
-        assert!(size >= Self::CHUNK_SIZE);
-        (0..size).map(|_| UnsafeCell::new(None)).collect()
-    }
-
-    /// Get the available capacity in the bump allocation chunk.
-    #[inline]
-    pub fn bump_capacity_remaining(&self) -> usize {
-        let end = self.alloc.end.as_ptr() as usize;
-        let next = unsafe { *self.alloc.next.get() };
-        end - next.as_ptr() as usize
-    }
-
-    /// Try and insert a `VMExternRef` into this table.
-    ///
-    /// This is a fast path that only succeeds when the bump chunk has the
-    /// capacity for the requested insertion.
-    ///
-    /// If the insertion fails, then the `VMExternRef` is given back. Callers
-    /// may attempt a GC to free up space and try again, or may call
-    /// `insert_slow_path` to infallibly insert the reference (potentially
-    /// allocating additional space in the table to hold it).
-    #[inline]
-    pub fn try_insert(&mut self, externref: VMExternRef) -> Result<(), VMExternRef> {
-        unsafe {
-            let next = *self.alloc.next.get();
-            if next == self.alloc.end {
-                return Err(externref);
-            }
-
-            debug_assert!(
-                (*next.as_ref().get()).is_none(),
-                "slots >= the `next` bump finger are always `None`"
-            );
-            ptr::write(next.as_ptr(), UnsafeCell::new(Some(externref)));
-
-            let next = NonNull::new_unchecked(next.as_ptr().add(1));
-            debug_assert!(next <= self.alloc.end);
-            *self.alloc.next.get() = next;
-
-            Ok(())
-        }
-    }
-
-    /// Insert a reference into the table, falling back on a GC to clear up
-    /// space if the table is already full.
-    ///
-    /// # Unsafety
-    ///
-    /// The same as `gc`.
-    #[inline]
-    pub unsafe fn insert_with_gc(
-        &mut self,
-        limits: *const VMRuntimeLimits,
-        externref: VMExternRef,
-        module_info_lookup: &dyn ModuleInfoLookup,
-    ) {
-        #[cfg(debug_assertions)]
-        assert!(self.gc_okay);
-
-        if let Err(externref) = self.try_insert(externref) {
-            self.gc_and_insert_slow(limits, externref, module_info_lookup);
-        }
-    }
-
-    #[inline(never)]
-    unsafe fn gc_and_insert_slow(
-        &mut self,
-        limits: *const VMRuntimeLimits,
-        externref: VMExternRef,
-        module_info_lookup: &dyn ModuleInfoLookup,
-    ) {
-        gc(limits, module_info_lookup, self);
-
-        // Might as well insert right into the hash set, rather than the bump
-        // chunk, since we are already on a slow path and we get de-duplication
-        // this way.
-        self.over_approximated_stack_roots
-            .insert(VMExternRefWithTraits(externref));
-    }
-
-    /// Insert a reference into the table, without ever performing GC.
-    #[inline]
-    pub fn insert_without_gc(&mut self, externref: VMExternRef) {
-        if let Err(externref) = self.try_insert(externref) {
-            self.insert_slow_without_gc(externref);
-        }
-    }
-
-    #[inline(never)]
-    fn insert_slow_without_gc(&mut self, externref: VMExternRef) {
-        self.over_approximated_stack_roots
-            .insert(VMExternRefWithTraits(externref));
-    }
-
-    fn num_filled_in_bump_chunk(&self) -> usize {
-        let next = unsafe { *self.alloc.next.get() };
-        let bytes_unused = (self.alloc.end.as_ptr() as usize) - (next.as_ptr() as usize);
-        let slots_unused = bytes_unused / mem::size_of::<TableElem>();
-        self.alloc.chunk.len().saturating_sub(slots_unused)
-    }
-
-    fn elements(&self, mut f: impl FnMut(&VMExternRef)) {
-        for elem in self.over_approximated_stack_roots.iter() {
-            f(&elem.0);
-        }
-
-        // The bump chunk is not all the way full, so we only iterate over its
-        // filled-in slots.
-        let num_filled = self.num_filled_in_bump_chunk();
-        for slot in self.alloc.chunk.iter().take(num_filled) {
-            if let Some(elem) = unsafe { &*slot.get() } {
-                f(elem);
-            }
-        }
-    }
-
-    fn insert_precise_stack_root(
-        precise_stack_roots: &mut HashSet<VMExternRefWithTraits>,
-        root: NonNull<VMExternData>,
-    ) {
-        let root = unsafe { VMExternRef::clone_from_raw(root.as_ptr().cast()) };
-        log::trace!("Found externref on stack: {:p}", root);
-        precise_stack_roots.insert(VMExternRefWithTraits(root));
-    }
-
-    /// Sweep the bump allocation table after we've discovered our precise stack
-    /// roots.
-    fn sweep(&mut self) {
-        log::trace!("begin GC sweep");
-
-        // Sweep our bump chunk.
-        let num_filled = self.num_filled_in_bump_chunk();
-        unsafe {
-            *self.alloc.next.get() = self.alloc.end;
-        }
-        for slot in self.alloc.chunk.iter().take(num_filled) {
-            unsafe {
-                *slot.get() = None;
-            }
-        }
-        debug_assert!(
-            self.alloc
-                .chunk
-                .iter()
-                .all(|slot| unsafe { (*slot.get()).as_ref().is_none() }),
-            "after sweeping the bump chunk, all slots should be `None`"
-        );
-
-        // If this is the first instance of gc then the initial chunk is empty,
-        // so we lazily allocate space for fast bump-allocation in the future.
-        if self.alloc.chunk.is_empty() {
-            self.alloc.chunk = Self::new_chunk(Self::CHUNK_SIZE);
-            self.alloc.end =
-                NonNull::new(unsafe { self.alloc.chunk.as_mut_ptr().add(self.alloc.chunk.len()) })
-                    .unwrap();
-        }
-
-        // Reset our `next` finger to the start of the bump allocation chunk.
-        unsafe {
-            let next = self.alloc.chunk.as_mut_ptr();
-            debug_assert!(!next.is_null());
-            *self.alloc.next.get() = NonNull::new_unchecked(next);
-        }
-
-        // The current `precise_stack_roots` becomes our new over-appoximated
-        // set for the next GC cycle.
-        mem::swap(
-            &mut self.precise_stack_roots,
-            &mut self.over_approximated_stack_roots,
-        );
-
-        // And finally, the new `precise_stack_roots` should be cleared and
-        // remain empty until the next GC cycle.
-        //
-        // Note that this may run arbitrary code as we run externref
-        // destructors. Because of our `&mut` borrow above on this table,
-        // though, we're guaranteed that nothing will touch this table.
-        self.precise_stack_roots.clear();
-
-        log::trace!("end GC sweep");
-    }
-
-    /// Set whether it is okay to GC or not right now.
-    ///
-    /// This is provided as a helper for enabling various debug-only assertions
-    /// and checking places where the `wasmtime-runtime` user expects there not
-    /// to be any GCs.
-    #[inline]
-    pub fn set_gc_okay(&mut self, okay: bool) -> bool {
-        #[cfg(debug_assertions)]
-        {
-            return std::mem::replace(&mut self.gc_okay, okay);
-        }
-        #[cfg(not(debug_assertions))]
-        {
-            let _ = okay;
-            return true;
-        }
-    }
-}
+use wasmtime_environ::StackMap;
 
 /// Used by the runtime to lookup information about a module given a
 /// program counter value.
@@ -804,269 +22,3 @@ pub trait ModuleInfo {
     /// Lookup the stack map at a program counter value.
     fn lookup_stack_map(&self, pc: usize) -> Option<&StackMap>;
 }
-
-#[derive(Debug, Default)]
-struct DebugOnly<T> {
-    inner: T,
-}
-
-impl<T> std::ops::Deref for DebugOnly<T> {
-    type Target = T;
-
-    fn deref(&self) -> &T {
-        if cfg!(debug_assertions) {
-            &self.inner
-        } else {
-            panic!(
-                "only deref `DebugOnly` when `cfg(debug_assertions)` or \
-                 inside a `debug_assert!(..)`"
-            )
-        }
-    }
-}
-
-impl<T> std::ops::DerefMut for DebugOnly<T> {
-    fn deref_mut(&mut self) -> &mut T {
-        if cfg!(debug_assertions) {
-            &mut self.inner
-        } else {
-            panic!(
-                "only deref `DebugOnly` when `cfg(debug_assertions)` or \
-                 inside a `debug_assert!(..)`"
-            )
-        }
-    }
-}
-
-/// Perform garbage collection of `VMExternRef`s.
-///
-/// # Unsafety
-///
-/// You must have called `VMExternRefActivationsTable::set_stack_canary` for at
-/// least the oldest host-->Wasm stack frame transition on this thread's stack
-/// (it is idempotent to call it more than once) and keep its return value alive
-/// across the duration of that host-->Wasm call.
-///
-/// Additionally, you must have registered the stack maps for every Wasm module
-/// that has frames on the stack with the given `stack_maps_registry`.
-pub unsafe fn gc(
-    limits: *const VMRuntimeLimits,
-    module_info_lookup: &dyn ModuleInfoLookup,
-    externref_activations_table: &mut VMExternRefActivationsTable,
-) {
-    log::debug!("start GC");
-
-    #[cfg(debug_assertions)]
-    assert!(externref_activations_table.gc_okay);
-
-    debug_assert!({
-        // This set is only non-empty within this function. It is built up when
-        // walking the stack and interpreting stack maps, and then drained back
-        // into the activations table's bump-allocated space at the
-        // end. Therefore, it should always be empty upon entering this
-        // function.
-        externref_activations_table.precise_stack_roots.is_empty()
-    });
-
-    // This function proceeds by:
-    //
-    // * walking the stack,
-    //
-    // * finding the precise set of roots inside Wasm frames via our stack maps,
-    //   and
-    //
-    // * resetting our bump-allocated table's over-approximation to the
-    //   newly-discovered precise set.
-
-    // The `activations_table_set` is used for `debug_assert!`s checking that
-    // every reference we read out from the stack via stack maps is actually in
-    // the table. If that weren't true, than either we forgot to insert a
-    // reference in the table when passing it into Wasm (a bug) or we are
-    // reading invalid references from the stack (another bug).
-    let mut activations_table_set: DebugOnly<HashSet<_>> = Default::default();
-    if cfg!(debug_assertions) {
-        externref_activations_table.elements(|elem| {
-            activations_table_set.insert(elem.as_raw() as *mut VMExternData);
-        });
-    }
-
-    log::trace!("begin GC trace");
-    Backtrace::trace(limits, |frame| {
-        let pc = frame.pc();
-        debug_assert!(pc != 0, "we should always get a valid PC for Wasm frames");
-
-        let fp = frame.fp();
-        debug_assert!(
-            fp != 0,
-            "we should always get a valid frame pointer for Wasm frames"
-        );
-
-        let module_info = module_info_lookup
-            .lookup(pc)
-            .expect("should have module info for Wasm frame");
-
-        let stack_map = match module_info.lookup_stack_map(pc) {
-            Some(sm) => sm,
-            None => {
-                log::trace!("No stack map for this Wasm frame");
-                return std::ops::ControlFlow::Continue(());
-            }
-        };
-        log::trace!(
-            "We have a stack map that maps {} words in this Wasm frame",
-            stack_map.mapped_words()
-        );
-
-        let sp = fp - stack_map.mapped_words() as usize * mem::size_of::<usize>();
-
-        for i in 0..(stack_map.mapped_words() as usize) {
-            // Stack maps have one bit per word in the frame, and the
-            // zero^th bit is the *lowest* addressed word in the frame,
-            // i.e. the closest to the SP. So to get the `i`^th word in
-            // this frame, we add `i * sizeof(word)` to the SP.
-            let stack_slot = sp + i * mem::size_of::<usize>();
-
-            if !stack_map.get_bit(i) {
-                log::trace!(
-                    "Stack slot @ {:p} does not contain externrefs",
-                    stack_slot as *const (),
-                );
-                continue;
-            }
-
-            let stack_slot = stack_slot as *const *mut VMExternData;
-            let r = std::ptr::read(stack_slot);
-            log::trace!("Stack slot @ {:p} = {:p}", stack_slot, r);
-
-            debug_assert!(
-                r.is_null() || activations_table_set.contains(&r),
-                "every on-stack externref inside a Wasm frame should \
-                 have an entry in the VMExternRefActivationsTable; \
-                 {:?} is not in the table",
-                r
-            );
-
-            if let Some(r) = NonNull::new(r) {
-                VMExternRefActivationsTable::insert_precise_stack_root(
-                    &mut externref_activations_table.precise_stack_roots,
-                    r,
-                );
-            }
-        }
-
-        std::ops::ControlFlow::Continue(())
-    });
-    log::trace!("end GC trace");
-
-    externref_activations_table.sweep();
-
-    log::debug!("end GC");
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn extern_ref_is_pointer_sized_and_aligned() {
-        assert_eq!(mem::size_of::<VMExternRef>(), mem::size_of::<*mut ()>());
-        assert_eq!(mem::align_of::<VMExternRef>(), mem::align_of::<*mut ()>());
-        assert_eq!(
-            mem::size_of::<Option<VMExternRef>>(),
-            mem::size_of::<*mut ()>()
-        );
-        assert_eq!(
-            mem::align_of::<Option<VMExternRef>>(),
-            mem::align_of::<*mut ()>()
-        );
-    }
-
-    #[test]
-    fn ref_count_is_at_correct_offset() {
-        let s = "hi";
-        let s: &(dyn Any + Send + Sync) = &s as _;
-        let s: *const (dyn Any + Send + Sync) = s as _;
-        let s: *mut (dyn Any + Send + Sync) = s as _;
-
-        let extern_data = VMExternData {
-            ref_count: AtomicUsize::new(0),
-            value_ptr: NonNull::new(s).unwrap().into(),
-        };
-
-        let extern_data_ptr = &extern_data as *const _;
-        let ref_count_ptr = &extern_data.ref_count as *const _;
-
-        let actual_offset = (ref_count_ptr as usize) - (extern_data_ptr as usize);
-
-        let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
-            ptr: 8,
-            num_imported_functions: 0,
-            num_imported_tables: 0,
-            num_imported_memories: 0,
-            num_imported_globals: 0,
-            num_defined_tables: 0,
-            num_defined_memories: 0,
-            num_owned_memories: 0,
-            num_defined_globals: 0,
-            num_escaped_funcs: 0,
-        });
-        assert_eq!(
-            offsets.vm_extern_data_ref_count(),
-            actual_offset.try_into().unwrap(),
-        );
-    }
-
-    #[test]
-    fn table_next_is_at_correct_offset() {
-        let table = VMExternRefActivationsTable::new();
-
-        let table_ptr = &table as *const _;
-        let next_ptr = &table.alloc.next as *const _;
-
-        let actual_offset = (next_ptr as usize) - (table_ptr as usize);
-
-        let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
-            ptr: 8,
-            num_imported_functions: 0,
-            num_imported_tables: 0,
-            num_imported_memories: 0,
-            num_imported_globals: 0,
-            num_defined_tables: 0,
-            num_defined_memories: 0,
-            num_owned_memories: 0,
-            num_defined_globals: 0,
-            num_escaped_funcs: 0,
-        });
-        assert_eq!(
-            offsets.vm_extern_ref_activation_table_next() as usize,
-            actual_offset
-        );
-    }
-
-    #[test]
-    fn table_end_is_at_correct_offset() {
-        let table = VMExternRefActivationsTable::new();
-
-        let table_ptr = &table as *const _;
-        let end_ptr = &table.alloc.end as *const _;
-
-        let actual_offset = (end_ptr as usize) - (table_ptr as usize);
-
-        let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
-            ptr: 8,
-            num_imported_functions: 0,
-            num_imported_tables: 0,
-            num_imported_memories: 0,
-            num_imported_globals: 0,
-            num_defined_tables: 0,
-            num_defined_memories: 0,
-            num_owned_memories: 0,
-            num_defined_globals: 0,
-            num_escaped_funcs: 0,
-        });
-        assert_eq!(
-            offsets.vm_extern_ref_activation_table_end() as usize,
-            actual_offset
-        );
-    }
-}
diff --git a/crates/runtime/src/externref/gc.rs b/crates/runtime/src/externref/gc.rs
new file mode 100644
index 000000000..6574799f3
--- /dev/null
+++ b/crates/runtime/src/externref/gc.rs
@@ -0,0 +1,1058 @@
+//! # `VMExternRef`
+//!
+//! `VMExternRef` is a reference-counted box for any kind of data that is
+//! external and opaque to running Wasm. Sometimes it might hold a Wasmtime
+//! thing, other times it might hold something from a Wasmtime embedder and is
+//! opaque even to us. It is morally equivalent to `Rc<dyn Any>` in Rust, but
+//! additionally always fits in a pointer-sized word. `VMExternRef` is
+//! non-nullable, but `Option<VMExternRef>` is a null pointer.
+//!
+//! The one part of `VMExternRef` that can't ever be opaque to us is the
+//! reference count. Even when we don't know what's inside an `VMExternRef`, we
+//! need to be able to manipulate its reference count as we add and remove
+//! references to it. And we need to do this from compiled Wasm code, so it must
+//! be `repr(C)`!
+//!
+//! ## Memory Layout
+//!
+//! `VMExternRef` itself is just a pointer to an `VMExternData`, which holds the
+//! opaque, boxed value, its reference count, and its vtable pointer.
+//!
+//! The `VMExternData` struct is *preceded* by the dynamically-sized value boxed
+//! up and referenced by one or more `VMExternRef`s:
+//!
+//! ```text
+//!      ,-------------------------------------------------------.
+//!      |                                                       |
+//!      V                                                       |
+//!     +----------------------------+-----------+-----------+   |
+//!     | dynamically-sized value... | ref_count | value_ptr |---'
+//!     +----------------------------+-----------+-----------+
+//!                                  | VMExternData          |
+//!                                  +-----------------------+
+//!                                   ^
+//! +-------------+                   |
+//! | VMExternRef |-------------------+
+//! +-------------+                   |
+//!                                   |
+//! +-------------+                   |
+//! | VMExternRef |-------------------+
+//! +-------------+                   |
+//!                                   |
+//!   ...                            ===
+//!                                   |
+//! +-------------+                   |
+//! | VMExternRef |-------------------'
+//! +-------------+
+//! ```
+//!
+//! The `value_ptr` member always points backwards to the start of the
+//! dynamically-sized value (which is also the start of the heap allocation for
+//! this value-and-`VMExternData` pair). Because it is a `dyn` pointer, it is
+//! fat, and also points to the value's `Any` vtable.
+//!
+//! The boxed value and the `VMExternRef` footer are held a single heap
+//! allocation. The layout described above is used to make satisfying the
+//! value's alignment easy: we just need to ensure that the heap allocation used
+//! to hold everything satisfies its alignment. It also ensures that we don't
+//! need a ton of excess padding between the `VMExternData` and the value for
+//! values with large alignment.
+//!
+//! ## Reference Counting, Wasm Functions, and Garbage Collection
+//!
+//! For host VM code, we use plain reference counting, where cloning increments
+//! the reference count, and dropping decrements it. We can avoid many of the
+//! on-stack increment/decrement operations that typically plague the
+//! performance of reference counting via Rust's ownership and borrowing system.
+//! Moving a `VMExternRef` avoids mutating its reference count, and borrowing it
+//! either avoids the reference count increment or delays it until if/when the
+//! `VMExternRef` is cloned.
+//!
+//! When passing a `VMExternRef` into compiled Wasm code, we don't want to do
+//! reference count mutations for every compiled `local.{get,set}`, nor for
+//! every function call. Therefore, we use a variation of **deferred reference
+//! counting**, where we only mutate reference counts when storing
+//! `VMExternRef`s somewhere that outlives the activation: into a global or
+//! table. Simultaneously, we over-approximate the set of `VMExternRef`s that
+//! are inside Wasm function activations. Periodically, we walk the stack at GC
+//! safe points, and use stack map information to precisely identify the set of
+//! `VMExternRef`s inside Wasm activations. Then we take the difference between
+//! this precise set and our over-approximation, and decrement the reference
+//! count for each of the `VMExternRef`s that are in our over-approximation but
+//! not in the precise set. Finally, the over-approximation is replaced with the
+//! precise set.
+//!
+//! The `VMExternRefActivationsTable` implements the over-approximized set of
+//! `VMExternRef`s referenced by Wasm activations. Calling a Wasm function and
+//! passing it a `VMExternRef` moves the `VMExternRef` into the table, and the
+//! compiled Wasm function logically "borrows" the `VMExternRef` from the
+//! table. Similarly, `global.get` and `table.get` operations clone the gotten
+//! `VMExternRef` into the `VMExternRefActivationsTable` and then "borrow" the
+//! reference out of the table.
+//!
+//! When a `VMExternRef` is returned to host code from a Wasm function, the host
+//! increments the reference count (because the reference is logically
+//! "borrowed" from the `VMExternRefActivationsTable` and the reference count
+//! from the table will be dropped at the next GC).
+//!
+//! For more general information on deferred reference counting, see *An
+//! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
+//! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
+
+use crate::{Backtrace, ModuleInfoLookup, SendSyncPtr, VMRuntimeLimits};
+use std::alloc::Layout;
+use std::any::Any;
+use std::cell::UnsafeCell;
+use std::cmp;
+use std::collections::HashSet;
+use std::hash::{Hash, Hasher};
+use std::mem;
+use std::ops::Deref;
+use std::ptr::{self, NonNull};
+use std::sync::atomic::{self, AtomicUsize, Ordering};
+
+/// An external reference to some opaque data.
+///
+/// `VMExternRef`s dereference to their underlying opaque data as `dyn Any`.
+///
+/// Unlike the `externref` in the Wasm spec, `VMExternRef`s are non-nullable,
+/// and always point to a valid value. You may use `Option<VMExternRef>` to
+/// represent nullable references, and `Option<VMExternRef>` is guaranteed to
+/// have the same size and alignment as a raw pointer, with `None` represented
+/// with the null pointer.
+///
+/// `VMExternRef`s are reference counted, so cloning is a cheap, shallow
+/// operation. It also means they are inherently shared, so you may not get a
+/// mutable, exclusive reference to their inner contents, only a shared,
+/// immutable reference. You may use interior mutability with `RefCell` or
+/// `Mutex` to work around this restriction, if necessary.
+///
+/// `VMExternRef`s have pointer-equality semantics, not structural-equality
+/// semantics. Given two `VMExternRef`s `a` and `b`, `a == b` only if `a` and
+/// `b` point to the same allocation. `a` and `b` are considered not equal, even
+/// if `a` and `b` are two different identical copies of the same data, if they
+/// are in two different allocations. The hashing and ordering implementations
+/// also only operate on the pointer.
+///
+/// # Example
+///
+/// ```
+/// # fn foo() -> Result<(), Box<dyn std::error::Error>> {
+/// use std::cell::RefCell;
+/// use wasmtime_runtime::VMExternRef;
+///
+/// // Open a file. Wasm doesn't know about files, but we can let Wasm instances
+/// // work with files via opaque `externref` handles.
+/// let file = std::fs::File::create("some/file/path")?;
+///
+/// // Wrap the file up as an `VMExternRef` that can be passed to Wasm.
+/// let extern_ref_to_file = VMExternRef::new(file);
+///
+/// // `VMExternRef`s dereference to `dyn Any`, so you can use `Any` methods to
+/// // perform runtime type checks and downcasts.
+///
+/// assert!(extern_ref_to_file.is::<std::fs::File>());
+/// assert!(!extern_ref_to_file.is::<String>());
+///
+/// if let Some(mut file) = extern_ref_to_file.downcast_ref::<std::fs::File>() {
+///     use std::io::Write;
+///     writeln!(&mut file, "Hello, `VMExternRef`!")?;
+/// }
+/// # Ok(())
+/// # }
+/// ```
+#[derive(Debug)]
+#[repr(transparent)]
+pub struct VMExternRef(SendSyncPtr<VMExternData>);
+
+impl std::fmt::Pointer for VMExternRef {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        std::fmt::Pointer::fmt(&self.0, f)
+    }
+}
+
+#[repr(C)]
+pub(crate) struct VMExternData {
+    // Implicit, dynamically-sized member that always preceded an
+    // `VMExternData`.
+    //
+    // value: [u8],
+    //
+    /// The reference count for this `VMExternData` and value. When it reaches
+    /// zero, we can safely destroy the value and free this heap
+    /// allocation. This is an `UnsafeCell`, rather than plain `Cell`, because
+    /// it can be modified by compiled Wasm code.
+    ///
+    /// Note: this field's offset must be kept in sync with
+    /// `wasmtime_environ::VMOffsets::vm_extern_data_ref_count()` which is
+    /// currently always zero.
+    ref_count: AtomicUsize,
+
+    /// Always points to the implicit, dynamically-sized `value` member that
+    /// precedes this `VMExternData`.
+    value_ptr: SendSyncPtr<dyn Any + Send + Sync>,
+}
+
+impl Clone for VMExternRef {
+    #[inline]
+    fn clone(&self) -> VMExternRef {
+        self.extern_data().increment_ref_count();
+        VMExternRef(self.0)
+    }
+}
+
+impl Drop for VMExternRef {
+    #[inline]
+    fn drop(&mut self) {
+        let data = self.extern_data();
+
+        // Note that the memory orderings here also match the standard library
+        // itself. Documentation is more available in the implementation of
+        // `Arc`, but the general idea is that this is a special pattern allowed
+        // by the C standard with atomic orderings where we "release" for all
+        // the decrements and only the final decrementer performs an acquire
+        // fence. This properly ensures that the final thread, which actually
+        // destroys the data, sees all the updates from all other threads.
+        if data.ref_count.fetch_sub(1, Ordering::Release) != 1 {
+            return;
+        }
+        atomic::fence(Ordering::Acquire);
+
+        unsafe {
+            VMExternData::drop_and_dealloc(self.0);
+        }
+    }
+}
+
+impl VMExternData {
+    /// Get the `Layout` for a value with the given size and alignment, and the
+    /// offset within that layout where the `VMExternData` footer resides.
+    ///
+    /// This doesn't take a `value: &T` because `VMExternRef::new_with` hasn't
+    /// constructed a `T` value yet, and it isn't generic over `T` because
+    /// `VMExternData::drop_and_dealloc` doesn't know what `T` to use, and has
+    /// to use `std::mem::{size,align}_of_val` instead.
+    unsafe fn layout_for(value_size: usize, value_align: usize) -> (Layout, usize) {
+        let extern_data_size = mem::size_of::<VMExternData>();
+        let extern_data_align = mem::align_of::<VMExternData>();
+
+        let value_and_padding_size = round_up_to_align(value_size, extern_data_align).unwrap();
+
+        let alloc_align = std::cmp::max(value_align, extern_data_align);
+        let alloc_size = value_and_padding_size + extern_data_size;
+
+        debug_assert!(
+            Layout::from_size_align(alloc_size, alloc_align).is_ok(),
+            "should create a `Layout` for size={} and align={} okay",
+            alloc_size,
+            alloc_align,
+        );
+        (
+            Layout::from_size_align_unchecked(alloc_size, alloc_align),
+            value_and_padding_size,
+        )
+    }
+
+    /// Drop the inner value and then free this `VMExternData` heap allocation.
+    pub(crate) unsafe fn drop_and_dealloc(mut data: SendSyncPtr<VMExternData>) {
+        log::trace!("Dropping externref data @ {:p}", data);
+
+        // Note: we introduce a block scope so that we drop the live
+        // reference to the data before we free the heap allocation it
+        // resides within after this block.
+        let (alloc_ptr, layout) = {
+            let data = data.as_mut();
+            debug_assert_eq!(data.ref_count.load(Ordering::SeqCst), 0);
+
+            // Same thing, but for the dropping the reference to `value` before
+            // we drop it itself.
+            let (layout, _) = {
+                let value = data.value_ptr.as_ref();
+                Self::layout_for(mem::size_of_val(value), mem::align_of_val(value))
+            };
+
+            ptr::drop_in_place(data.value_ptr.as_ptr());
+            let alloc_ptr = data.value_ptr.as_ptr().cast::<u8>();
+
+            (alloc_ptr, layout)
+        };
+
+        ptr::drop_in_place(data.as_ptr());
+        std::alloc::dealloc(alloc_ptr, layout);
+    }
+
+    #[inline]
+    fn increment_ref_count(&self) {
+        // This is only using during cloning operations, and like the standard
+        // library we use `Relaxed` here. The rationale is better documented in
+        // libstd's implementation of `Arc`, but the general gist is that we're
+        // creating a new pointer for our own thread, so there's no need to have
+        // any synchronization with orderings. The synchronization with other
+        // threads with respect to orderings happens when the pointer is sent to
+        // another thread.
+        self.ref_count.fetch_add(1, Ordering::Relaxed);
+    }
+}
+
+#[inline]
+fn round_up_to_align(n: usize, align: usize) -> Option<usize> {
+    debug_assert!(align.is_power_of_two());
+    let align_minus_one = align - 1;
+    Some(n.checked_add(align_minus_one)? & !align_minus_one)
+}
+
+impl VMExternRef {
+    /// Wrap the given value inside an `VMExternRef`.
+    pub fn new<T>(value: T) -> VMExternRef
+    where
+        T: 'static + Any + Send + Sync,
+    {
+        VMExternRef::new_with(|| value)
+    }
+
+    /// Construct a new `VMExternRef` in place by invoking `make_value`.
+    pub fn new_with<T>(make_value: impl FnOnce() -> T) -> VMExternRef
+    where
+        T: 'static + Any + Send + Sync,
+    {
+        unsafe {
+            let (layout, footer_offset) =
+                VMExternData::layout_for(mem::size_of::<T>(), mem::align_of::<T>());
+
+            let alloc_ptr = std::alloc::alloc(layout);
+            let alloc_ptr = NonNull::new(alloc_ptr).unwrap_or_else(|| {
+                std::alloc::handle_alloc_error(layout);
+            });
+
+            let value_ptr = alloc_ptr.cast::<T>();
+            ptr::write(value_ptr.as_ptr(), make_value());
+
+            let extern_data_ptr =
+                alloc_ptr.cast::<u8>().as_ptr().add(footer_offset) as *mut VMExternData;
+
+            ptr::write(
+                extern_data_ptr,
+                VMExternData {
+                    ref_count: AtomicUsize::new(1),
+                    // Cast from `*mut T` to `*mut dyn Any` here.
+                    value_ptr: SendSyncPtr::new(NonNull::new_unchecked(value_ptr.as_ptr())),
+                },
+            );
+
+            log::trace!("New externref data @ {:p}", extern_data_ptr);
+            VMExternRef(NonNull::new_unchecked(extern_data_ptr).into())
+        }
+    }
+
+    /// Turn this `VMExternRef` into a raw, untyped pointer.
+    ///
+    /// Unlike `into_raw`, this does not consume and forget `self`. It is *not*
+    /// safe to use `from_raw` on pointers returned from this method; only use
+    /// `clone_from_raw`!
+    ///
+    ///  Nor does this method increment the reference count. You must ensure
+    ///  that `self` (or some other clone of `self`) stays alive until
+    ///  `clone_from_raw` is called.
+    #[inline]
+    pub fn as_raw(&self) -> *mut u8 {
+        let ptr = self.0.as_ptr().cast::<u8>();
+        ptr
+    }
+
+    /// Consume this `VMExternRef` into a raw, untyped pointer.
+    ///
+    /// # Safety
+    ///
+    /// This method forgets self, so it is possible to create a leak of the
+    /// underlying reference counted data if not used carefully.
+    ///
+    /// Use `from_raw` to recreate the `VMExternRef`.
+    pub unsafe fn into_raw(self) -> *mut u8 {
+        let ptr = self.0.as_ptr().cast::<u8>();
+        std::mem::forget(self);
+        ptr
+    }
+
+    /// Recreate a `VMExternRef` from a pointer returned from a previous call to
+    /// `as_raw`.
+    ///
+    /// # Safety
+    ///
+    /// Unlike `clone_from_raw`, this does not increment the reference count of the
+    /// underlying data.  It is not safe to continue to use the pointer passed to this
+    /// function.
+    #[inline]
+    pub unsafe fn from_raw(ptr: *mut u8) -> Option<Self> {
+        Some(VMExternRef(NonNull::new(ptr)?.cast().into()))
+    }
+
+    /// Recreate a `VMExternRef` from a pointer returned from a previous call to
+    /// `as_raw`.
+    ///
+    /// # Safety
+    ///
+    /// Wildly unsafe to use with anything other than the result of a previous
+    /// `as_raw` call!
+    ///
+    /// Additionally, it is your responsibility to ensure that this raw
+    /// `VMExternRef`'s reference count has not dropped to zero. Failure to do
+    /// so will result in use after free!
+    #[inline]
+    pub unsafe fn clone_from_raw(ptr: *mut u8) -> Option<Self> {
+        let x = VMExternRef(NonNull::new(ptr)?.cast::<VMExternData>().into());
+        x.extern_data().increment_ref_count();
+        Some(x)
+    }
+
+    /// Get the strong reference count for this `VMExternRef`.
+    ///
+    /// Note that this loads with a `SeqCst` ordering to synchronize with other
+    /// threads.
+    pub fn strong_count(&self) -> usize {
+        self.extern_data().ref_count.load(Ordering::SeqCst)
+    }
+
+    #[inline]
+    fn extern_data(&self) -> &VMExternData {
+        unsafe { self.0.as_ref() }
+    }
+}
+
+/// Methods that would normally be trait implementations, but aren't to avoid
+/// potential footguns around `VMExternRef`'s pointer-equality semantics.
+///
+/// Note that none of these methods are on `&self`, they all require a
+/// fully-qualified `VMExternRef::foo(my_ref)` invocation.
+impl VMExternRef {
+    /// Check whether two `VMExternRef`s point to the same inner allocation.
+    ///
+    /// Note that this uses pointer-equality semantics, not structural-equality
+    /// semantics, and so only pointers are compared, and doesn't use any `Eq`
+    /// or `PartialEq` implementation of the pointed-to values.
+    #[inline]
+    pub fn eq(a: &Self, b: &Self) -> bool {
+        ptr::eq(a.0.as_ptr(), b.0.as_ptr())
+    }
+
+    /// Hash a given `VMExternRef`.
+    ///
+    /// Note that this just hashes the pointer to the inner value, it does *not*
+    /// use the inner value's `Hash` implementation (if any).
+    #[inline]
+    pub fn hash<H>(externref: &Self, hasher: &mut H)
+    where
+        H: Hasher,
+    {
+        ptr::hash(externref.0.as_ptr(), hasher);
+    }
+
+    /// Compare two `VMExternRef`s.
+    ///
+    /// Note that this uses pointer-equality semantics, not structural-equality
+    /// semantics, and so only pointers are compared, and doesn't use any `Cmp`
+    /// or `PartialCmp` implementation of the pointed-to values.
+    #[inline]
+    pub fn cmp(a: &Self, b: &Self) -> cmp::Ordering {
+        let a = a.0.as_ptr() as usize;
+        let b = b.0.as_ptr() as usize;
+        a.cmp(&b)
+    }
+}
+
+impl Deref for VMExternRef {
+    type Target = dyn Any;
+
+    fn deref(&self) -> &dyn Any {
+        unsafe { self.extern_data().value_ptr.as_ref() }
+    }
+}
+
+/// A wrapper around a `VMExternRef` that implements `Eq` and `Hash` with
+/// pointer semantics.
+///
+/// We use this so that we can morally put `VMExternRef`s inside of `HashSet`s
+/// even though they don't implement `Eq` and `Hash` to avoid foot guns.
+#[derive(Clone, Debug)]
+struct VMExternRefWithTraits(VMExternRef);
+
+impl Hash for VMExternRefWithTraits {
+    fn hash<H>(&self, hasher: &mut H)
+    where
+        H: Hasher,
+    {
+        VMExternRef::hash(&self.0, hasher)
+    }
+}
+
+impl PartialEq for VMExternRefWithTraits {
+    fn eq(&self, other: &Self) -> bool {
+        VMExternRef::eq(&self.0, &other.0)
+    }
+}
+
+impl Eq for VMExternRefWithTraits {}
+
+type TableElem = UnsafeCell<Option<VMExternRef>>;
+
+/// A table that over-approximizes the set of `VMExternRef`s that any Wasm
+/// activation on this thread is currently using.
+///
+/// Under the covers, this is a simple bump allocator that allows duplicate
+/// entries. Deduplication happens at GC time.
+#[repr(C)] // `alloc` must be the first member, it's accessed from JIT code.
+pub struct VMExternRefActivationsTable {
+    /// Structures used to perform fast bump allocation of storage of externref
+    /// values.
+    ///
+    /// This is the only member of this structure accessed from JIT code.
+    alloc: VMExternRefTableAlloc,
+
+    /// When unioned with `chunk`, this is an over-approximation of the GC roots
+    /// on the stack, inside Wasm frames.
+    ///
+    /// This is used by slow-path insertion, and when a GC cycle finishes, is
+    /// re-initialized to the just-discovered precise set of stack roots (which
+    /// immediately becomes an over-approximation again as soon as Wasm runs and
+    /// potentially drops references).
+    over_approximated_stack_roots: HashSet<VMExternRefWithTraits>,
+
+    /// The precise set of on-stack, inside-Wasm GC roots that we discover via
+    /// walking the stack and interpreting stack maps.
+    ///
+    /// This is *only* used inside the `gc` function, and is empty otherwise. It
+    /// is just part of this struct so that we can reuse the allocation, rather
+    /// than create a new hash set every GC.
+    precise_stack_roots: HashSet<VMExternRefWithTraits>,
+
+    /// A debug-only field for asserting that we are in a region of code where
+    /// GC is okay to preform.
+    #[cfg(debug_assertions)]
+    gc_okay: bool,
+}
+
+#[repr(C)] // This is accessed from JIT code.
+struct VMExternRefTableAlloc {
+    /// Bump-allocation finger within the `chunk`.
+    ///
+    /// NB: this is an `UnsafeCell` because it is written to by compiled Wasm
+    /// code.
+    next: UnsafeCell<NonNull<TableElem>>,
+
+    /// Pointer to just after the `chunk`.
+    ///
+    /// This is *not* within the current chunk and therefore is not a valid
+    /// place to insert a reference!
+    end: NonNull<TableElem>,
+
+    /// Bump allocation chunk that stores fast-path insertions.
+    ///
+    /// This is not accessed from JIT code.
+    chunk: Box<[TableElem]>,
+}
+
+// This gets around the usage of `UnsafeCell` throughout the internals of this
+// allocator, but the storage should all be Send/Sync and synchronization isn't
+// necessary since operations require `&mut self`.
+unsafe impl Send for VMExternRefTableAlloc {}
+unsafe impl Sync for VMExternRefTableAlloc {}
+
+fn _assert_send_sync() {
+    fn _assert<T: Send + Sync>() {}
+    _assert::<VMExternRefActivationsTable>();
+    _assert::<VMExternRef>();
+}
+
+impl VMExternRefActivationsTable {
+    const CHUNK_SIZE: usize = 4096 / mem::size_of::<usize>();
+
+    /// Create a new `VMExternRefActivationsTable`.
+    pub fn new() -> Self {
+        // Start with an empty chunk in case this activations table isn't used.
+        // This means that there's no space in the bump-allocation area which
+        // will force any path trying to use this to the slow gc path. The first
+        // time this happens, though, the slow gc path will allocate a new chunk
+        // for actual fast-bumping.
+        let mut chunk: Box<[TableElem]> = Box::new([]);
+        let next = chunk.as_mut_ptr();
+        let end = unsafe { next.add(chunk.len()) };
+
+        VMExternRefActivationsTable {
+            alloc: VMExternRefTableAlloc {
+                next: UnsafeCell::new(NonNull::new(next).unwrap()),
+                end: NonNull::new(end).unwrap(),
+                chunk,
+            },
+            over_approximated_stack_roots: HashSet::new(),
+            precise_stack_roots: HashSet::new(),
+            #[cfg(debug_assertions)]
+            gc_okay: true,
+        }
+    }
+
+    fn new_chunk(size: usize) -> Box<[UnsafeCell<Option<VMExternRef>>]> {
+        assert!(size >= Self::CHUNK_SIZE);
+        (0..size).map(|_| UnsafeCell::new(None)).collect()
+    }
+
+    /// Get the available capacity in the bump allocation chunk.
+    #[inline]
+    pub fn bump_capacity_remaining(&self) -> usize {
+        let end = self.alloc.end.as_ptr() as usize;
+        let next = unsafe { *self.alloc.next.get() };
+        end - next.as_ptr() as usize
+    }
+
+    /// Try and insert a `VMExternRef` into this table.
+    ///
+    /// This is a fast path that only succeeds when the bump chunk has the
+    /// capacity for the requested insertion.
+    ///
+    /// If the insertion fails, then the `VMExternRef` is given back. Callers
+    /// may attempt a GC to free up space and try again, or may call
+    /// `insert_slow_path` to infallibly insert the reference (potentially
+    /// allocating additional space in the table to hold it).
+    #[inline]
+    pub fn try_insert(&mut self, externref: VMExternRef) -> Result<(), VMExternRef> {
+        unsafe {
+            let next = *self.alloc.next.get();
+            if next == self.alloc.end {
+                return Err(externref);
+            }
+
+            debug_assert!(
+                (*next.as_ref().get()).is_none(),
+                "slots >= the `next` bump finger are always `None`"
+            );
+            ptr::write(next.as_ptr(), UnsafeCell::new(Some(externref)));
+
+            let next = NonNull::new_unchecked(next.as_ptr().add(1));
+            debug_assert!(next <= self.alloc.end);
+            *self.alloc.next.get() = next;
+
+            Ok(())
+        }
+    }
+
+    /// Insert a reference into the table, falling back on a GC to clear up
+    /// space if the table is already full.
+    ///
+    /// # Unsafety
+    ///
+    /// The same as `gc`.
+    #[inline]
+    pub unsafe fn insert_with_gc(
+        &mut self,
+        limits: *const VMRuntimeLimits,
+        externref: VMExternRef,
+        module_info_lookup: &dyn ModuleInfoLookup,
+    ) {
+        #[cfg(debug_assertions)]
+        assert!(self.gc_okay);
+
+        if let Err(externref) = self.try_insert(externref) {
+            self.gc_and_insert_slow(limits, externref, module_info_lookup);
+        }
+    }
+
+    #[inline(never)]
+    unsafe fn gc_and_insert_slow(
+        &mut self,
+        limits: *const VMRuntimeLimits,
+        externref: VMExternRef,
+        module_info_lookup: &dyn ModuleInfoLookup,
+    ) {
+        gc(limits, module_info_lookup, self);
+
+        // Might as well insert right into the hash set, rather than the bump
+        // chunk, since we are already on a slow path and we get de-duplication
+        // this way.
+        self.over_approximated_stack_roots
+            .insert(VMExternRefWithTraits(externref));
+    }
+
+    /// Insert a reference into the table, without ever performing GC.
+    #[inline]
+    pub fn insert_without_gc(&mut self, externref: VMExternRef) {
+        if let Err(externref) = self.try_insert(externref) {
+            self.insert_slow_without_gc(externref);
+        }
+    }
+
+    #[inline(never)]
+    fn insert_slow_without_gc(&mut self, externref: VMExternRef) {
+        self.over_approximated_stack_roots
+            .insert(VMExternRefWithTraits(externref));
+    }
+
+    fn num_filled_in_bump_chunk(&self) -> usize {
+        let next = unsafe { *self.alloc.next.get() };
+        let bytes_unused = (self.alloc.end.as_ptr() as usize) - (next.as_ptr() as usize);
+        let slots_unused = bytes_unused / mem::size_of::<TableElem>();
+        self.alloc.chunk.len().saturating_sub(slots_unused)
+    }
+
+    fn elements(&self, mut f: impl FnMut(&VMExternRef)) {
+        for elem in self.over_approximated_stack_roots.iter() {
+            f(&elem.0);
+        }
+
+        // The bump chunk is not all the way full, so we only iterate over its
+        // filled-in slots.
+        let num_filled = self.num_filled_in_bump_chunk();
+        for slot in self.alloc.chunk.iter().take(num_filled) {
+            if let Some(elem) = unsafe { &*slot.get() } {
+                f(elem);
+            }
+        }
+    }
+
+    fn insert_precise_stack_root(
+        precise_stack_roots: &mut HashSet<VMExternRefWithTraits>,
+        root: NonNull<VMExternData>,
+    ) {
+        let root = unsafe { VMExternRef::clone_from_raw(root.as_ptr().cast()).unwrap() };
+        log::trace!("Found externref on stack: {:p}", root);
+        precise_stack_roots.insert(VMExternRefWithTraits(root));
+    }
+
+    /// Sweep the bump allocation table after we've discovered our precise stack
+    /// roots.
+    fn sweep(&mut self) {
+        log::trace!("begin GC sweep");
+
+        // Sweep our bump chunk.
+        let num_filled = self.num_filled_in_bump_chunk();
+        unsafe {
+            *self.alloc.next.get() = self.alloc.end;
+        }
+        for slot in self.alloc.chunk.iter().take(num_filled) {
+            unsafe {
+                *slot.get() = None;
+            }
+        }
+        debug_assert!(
+            self.alloc
+                .chunk
+                .iter()
+                .all(|slot| unsafe { (*slot.get()).as_ref().is_none() }),
+            "after sweeping the bump chunk, all slots should be `None`"
+        );
+
+        // If this is the first instance of gc then the initial chunk is empty,
+        // so we lazily allocate space for fast bump-allocation in the future.
+        if self.alloc.chunk.is_empty() {
+            self.alloc.chunk = Self::new_chunk(Self::CHUNK_SIZE);
+            self.alloc.end =
+                NonNull::new(unsafe { self.alloc.chunk.as_mut_ptr().add(self.alloc.chunk.len()) })
+                    .unwrap();
+        }
+
+        // Reset our `next` finger to the start of the bump allocation chunk.
+        unsafe {
+            let next = self.alloc.chunk.as_mut_ptr();
+            debug_assert!(!next.is_null());
+            *self.alloc.next.get() = NonNull::new_unchecked(next);
+        }
+
+        // The current `precise_stack_roots` becomes our new over-appoximated
+        // set for the next GC cycle.
+        mem::swap(
+            &mut self.precise_stack_roots,
+            &mut self.over_approximated_stack_roots,
+        );
+
+        // And finally, the new `precise_stack_roots` should be cleared and
+        // remain empty until the next GC cycle.
+        //
+        // Note that this may run arbitrary code as we run externref
+        // destructors. Because of our `&mut` borrow above on this table,
+        // though, we're guaranteed that nothing will touch this table.
+        self.precise_stack_roots.clear();
+
+        log::trace!("end GC sweep");
+    }
+
+    /// Set whether it is okay to GC or not right now.
+    ///
+    /// This is provided as a helper for enabling various debug-only assertions
+    /// and checking places where the `wasmtime-runtime` user expects there not
+    /// to be any GCs.
+    #[inline]
+    pub fn set_gc_okay(&mut self, okay: bool) -> bool {
+        #[cfg(debug_assertions)]
+        {
+            return std::mem::replace(&mut self.gc_okay, okay);
+        }
+        #[cfg(not(debug_assertions))]
+        {
+            let _ = okay;
+            return true;
+        }
+    }
+}
+
+#[derive(Debug, Default)]
+struct DebugOnly<T> {
+    inner: T,
+}
+
+impl<T> std::ops::Deref for DebugOnly<T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        if cfg!(debug_assertions) {
+            &self.inner
+        } else {
+            panic!(
+                "only deref `DebugOnly` when `cfg(debug_assertions)` or \
+                 inside a `debug_assert!(..)`"
+            )
+        }
+    }
+}
+
+impl<T> std::ops::DerefMut for DebugOnly<T> {
+    fn deref_mut(&mut self) -> &mut T {
+        if cfg!(debug_assertions) {
+            &mut self.inner
+        } else {
+            panic!(
+                "only deref `DebugOnly` when `cfg(debug_assertions)` or \
+                 inside a `debug_assert!(..)`"
+            )
+        }
+    }
+}
+
+/// Perform garbage collection of `VMExternRef`s.
+///
+/// # Unsafety
+///
+/// You must have called `VMExternRefActivationsTable::set_stack_canary` for at
+/// least the oldest host-->Wasm stack frame transition on this thread's stack
+/// (it is idempotent to call it more than once) and keep its return value alive
+/// across the duration of that host-->Wasm call.
+///
+/// Additionally, you must have registered the stack maps for every Wasm module
+/// that has frames on the stack with the given `stack_maps_registry`.
+pub unsafe fn gc(
+    limits: *const VMRuntimeLimits,
+    module_info_lookup: &dyn ModuleInfoLookup,
+    externref_activations_table: &mut VMExternRefActivationsTable,
+) {
+    log::debug!("start GC");
+
+    #[cfg(debug_assertions)]
+    assert!(externref_activations_table.gc_okay);
+
+    debug_assert!({
+        // This set is only non-empty within this function. It is built up when
+        // walking the stack and interpreting stack maps, and then drained back
+        // into the activations table's bump-allocated space at the
+        // end. Therefore, it should always be empty upon entering this
+        // function.
+        externref_activations_table.precise_stack_roots.is_empty()
+    });
+
+    // This function proceeds by:
+    //
+    // * walking the stack,
+    //
+    // * finding the precise set of roots inside Wasm frames via our stack maps,
+    //   and
+    //
+    // * resetting our bump-allocated table's over-approximation to the
+    //   newly-discovered precise set.
+
+    // The `activations_table_set` is used for `debug_assert!`s checking that
+    // every reference we read out from the stack via stack maps is actually in
+    // the table. If that weren't true, than either we forgot to insert a
+    // reference in the table when passing it into Wasm (a bug) or we are
+    // reading invalid references from the stack (another bug).
+    let mut activations_table_set: DebugOnly<HashSet<_>> = Default::default();
+    if cfg!(debug_assertions) {
+        externref_activations_table.elements(|elem| {
+            activations_table_set.insert(elem.as_raw() as *mut VMExternData);
+        });
+    }
+
+    log::trace!("begin GC trace");
+    Backtrace::trace(limits, |frame| {
+        let pc = frame.pc();
+        debug_assert!(pc != 0, "we should always get a valid PC for Wasm frames");
+
+        let fp = frame.fp();
+        debug_assert!(
+            fp != 0,
+            "we should always get a valid frame pointer for Wasm frames"
+        );
+
+        let module_info = module_info_lookup
+            .lookup(pc)
+            .expect("should have module info for Wasm frame");
+
+        let stack_map = match module_info.lookup_stack_map(pc) {
+            Some(sm) => sm,
+            None => {
+                log::trace!("No stack map for this Wasm frame");
+                return std::ops::ControlFlow::Continue(());
+            }
+        };
+        log::trace!(
+            "We have a stack map that maps {} words in this Wasm frame",
+            stack_map.mapped_words()
+        );
+
+        let sp = fp - stack_map.mapped_words() as usize * mem::size_of::<usize>();
+
+        for i in 0..(stack_map.mapped_words() as usize) {
+            // Stack maps have one bit per word in the frame, and the
+            // zero^th bit is the *lowest* addressed word in the frame,
+            // i.e. the closest to the SP. So to get the `i`^th word in
+            // this frame, we add `i * sizeof(word)` to the SP.
+            let stack_slot = sp + i * mem::size_of::<usize>();
+
+            if !stack_map.get_bit(i) {
+                log::trace!(
+                    "Stack slot @ {:p} does not contain externrefs",
+                    stack_slot as *const (),
+                );
+                continue;
+            }
+
+            let stack_slot = stack_slot as *const *mut VMExternData;
+            let r = std::ptr::read(stack_slot);
+            log::trace!("Stack slot @ {:p} = {:p}", stack_slot, r);
+
+            debug_assert!(
+                r.is_null() || activations_table_set.contains(&r),
+                "every on-stack externref inside a Wasm frame should \
+                 have an entry in the VMExternRefActivationsTable; \
+                 {:?} is not in the table",
+                r
+            );
+
+            if let Some(r) = NonNull::new(r) {
+                VMExternRefActivationsTable::insert_precise_stack_root(
+                    &mut externref_activations_table.precise_stack_roots,
+                    r,
+                );
+            }
+        }
+
+        std::ops::ControlFlow::Continue(())
+    });
+    log::trace!("end GC trace");
+
+    externref_activations_table.sweep();
+
+    log::debug!("end GC");
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn extern_ref_is_pointer_sized_and_aligned() {
+        assert_eq!(mem::size_of::<VMExternRef>(), mem::size_of::<*mut ()>());
+        assert_eq!(mem::align_of::<VMExternRef>(), mem::align_of::<*mut ()>());
+        assert_eq!(
+            mem::size_of::<Option<VMExternRef>>(),
+            mem::size_of::<*mut ()>()
+        );
+        assert_eq!(
+            mem::align_of::<Option<VMExternRef>>(),
+            mem::align_of::<*mut ()>()
+        );
+    }
+
+    #[test]
+    fn ref_count_is_at_correct_offset() {
+        let s = "hi";
+        let s: &(dyn Any + Send + Sync) = &s as _;
+        let s: *const (dyn Any + Send + Sync) = s as _;
+        let s: *mut (dyn Any + Send + Sync) = s as _;
+
+        let extern_data = VMExternData {
+            ref_count: AtomicUsize::new(0),
+            value_ptr: NonNull::new(s).unwrap().into(),
+        };
+
+        let extern_data_ptr = &extern_data as *const _;
+        let ref_count_ptr = &extern_data.ref_count as *const _;
+
+        let actual_offset = (ref_count_ptr as usize) - (extern_data_ptr as usize);
+
+        let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
+            ptr: 8,
+            num_imported_functions: 0,
+            num_imported_tables: 0,
+            num_imported_memories: 0,
+            num_imported_globals: 0,
+            num_defined_tables: 0,
+            num_defined_memories: 0,
+            num_owned_memories: 0,
+            num_defined_globals: 0,
+            num_escaped_funcs: 0,
+        });
+        assert_eq!(
+            offsets.vm_extern_data_ref_count(),
+            actual_offset.try_into().unwrap(),
+        );
+    }
+
+    #[test]
+    fn table_next_is_at_correct_offset() {
+        let table = VMExternRefActivationsTable::new();
+
+        let table_ptr = &table as *const _;
+        let next_ptr = &table.alloc.next as *const _;
+
+        let actual_offset = (next_ptr as usize) - (table_ptr as usize);
+
+        let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
+            ptr: 8,
+            num_imported_functions: 0,
+            num_imported_tables: 0,
+            num_imported_memories: 0,
+            num_imported_globals: 0,
+            num_defined_tables: 0,
+            num_defined_memories: 0,
+            num_owned_memories: 0,
+            num_defined_globals: 0,
+            num_escaped_funcs: 0,
+        });
+        assert_eq!(
+            offsets.vm_extern_ref_activation_table_next() as usize,
+            actual_offset
+        );
+    }
+
+    #[test]
+    fn table_end_is_at_correct_offset() {
+        let table = VMExternRefActivationsTable::new();
+
+        let table_ptr = &table as *const _;
+        let end_ptr = &table.alloc.end as *const _;
+
+        let actual_offset = (end_ptr as usize) - (table_ptr as usize);
+
+        let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
+            ptr: 8,
+            num_imported_functions: 0,
+            num_imported_tables: 0,
+            num_imported_memories: 0,
+            num_imported_globals: 0,
+            num_defined_tables: 0,
+            num_defined_memories: 0,
+            num_owned_memories: 0,
+            num_defined_globals: 0,
+            num_escaped_funcs: 0,
+        });
+        assert_eq!(
+            offsets.vm_extern_ref_activation_table_end() as usize,
+            actual_offset
+        );
+    }
+}
diff --git a/crates/runtime/src/externref/no_gc.rs b/crates/runtime/src/externref/no_gc.rs
new file mode 100644
index 000000000..48c7fc885
--- /dev/null
+++ b/crates/runtime/src/externref/no_gc.rs
@@ -0,0 +1,125 @@
+//! The dummy `VMExternRef` for when the `gc` cargo feature is disabled.
+//!
+//! To reduce `#[cfg(...)]`s, this provides all the same methods as the real
+//! `VMExternRef` except for constructors.
+
+#![allow(missing_docs)]
+
+use crate::{ModuleInfoLookup, VMRuntimeLimits};
+use std::any::Any;
+use std::cmp;
+use std::hash::Hasher;
+use std::ops::Deref;
+
+#[derive(Clone)]
+enum Uninhabited {}
+
+#[derive(Clone)]
+pub struct VMExternRef(Uninhabited);
+
+impl std::fmt::Pointer for VMExternRef {
+    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self.0 {}
+    }
+}
+
+impl Drop for VMExternRef {
+    fn drop(&mut self) {
+        match self.0 {}
+    }
+}
+
+impl VMExternRef {
+    /// This method is only available when the `gc` cargo feature is *disabled*.
+    pub fn assert_unreachable<T>(&self) -> T {
+        match self.0 {}
+    }
+
+    pub fn as_raw(&self) -> *mut u8 {
+        match self.0 {}
+    }
+
+    pub unsafe fn into_raw(self) -> *mut u8 {
+        match self.0 {}
+    }
+
+    pub unsafe fn from_raw(ptr: *mut u8) -> Option<Self> {
+        assert!(ptr.is_null());
+        None
+    }
+
+    pub unsafe fn clone_from_raw(ptr: *mut u8) -> Option<Self> {
+        assert!(ptr.is_null());
+        None
+    }
+
+    pub fn strong_count(&self) -> usize {
+        match self.0 {}
+    }
+
+    pub fn eq(a: &Self, _b: &Self) -> bool {
+        match a.0 {}
+    }
+
+    pub fn hash<H>(externref: &Self, _hasher: &mut H)
+    where
+        H: Hasher,
+    {
+        match externref.0 {}
+    }
+
+    pub fn cmp(a: &Self, _b: &Self) -> cmp::Ordering {
+        match a.0 {}
+    }
+}
+
+impl Deref for VMExternRef {
+    type Target = dyn Any;
+
+    fn deref(&self) -> &dyn Any {
+        match self.0 {}
+    }
+}
+
+pub struct VMExternRefActivationsTable {
+    _priv: (),
+}
+
+impl VMExternRefActivationsTable {
+    pub fn new() -> Self {
+        Self { _priv: () }
+    }
+
+    pub fn bump_capacity_remaining(&self) -> usize {
+        usize::MAX
+    }
+
+    pub fn try_insert(&mut self, externref: VMExternRef) -> Result<(), VMExternRef> {
+        match externref.0 {}
+    }
+
+    pub unsafe fn insert_with_gc(
+        &mut self,
+        _limits: *const VMRuntimeLimits,
+        externref: VMExternRef,
+        _module_info_lookup: &dyn ModuleInfoLookup,
+    ) {
+        match externref.0 {}
+    }
+
+    pub fn insert_without_gc(&mut self, externref: VMExternRef) {
+        match externref.0 {}
+    }
+
+    pub fn set_gc_okay(&mut self, _okay: bool) -> bool {
+        true
+    }
+}
+
+pub unsafe fn gc(
+    _limits: *const VMRuntimeLimits,
+    _module_info_lookup: &dyn ModuleInfoLookup,
+    _externref_activations_table: &mut VMExternRefActivationsTable,
+) {
+    // Nothing to do.
+}
diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs
index 54986e0f3..e65558420 100644
--- a/crates/runtime/src/instance.rs
+++ b/crates/runtime/src/instance.rs
@@ -476,6 +476,7 @@ impl Instance {
 
             *self.runtime_limits() = ptr::null_mut();
             *self.epoch_ptr() = ptr::null_mut();
+
             *self.externref_activations_table() = ptr::null_mut();
         }
     }
@@ -1239,6 +1240,7 @@ impl Instance {
                             heap_type: WasmHeapType::Extern,
                             ..
                         }) => *(*to).as_externref_mut() = from.as_externref().clone(),
+
                         _ => ptr::copy_nonoverlapping(from, to, 1),
                     }
                 }
@@ -1286,12 +1288,12 @@ impl Drop for Instance {
                 WasmValType::Ref(WasmRefType {
                     heap_type: WasmHeapType::Extern,
                     ..
-                }) => {}
+                }) => unsafe {
+                    drop((*self.global_ptr(idx)).as_externref_mut().take());
+                },
+
                 _ => continue,
             }
-            unsafe {
-                drop((*self.global_ptr(idx)).as_externref_mut().take());
-            }
         }
     }
 }
diff --git a/crates/runtime/src/libcalls.rs b/crates/runtime/src/libcalls.rs
index 539a1b3f5..1e2d203a6 100644
--- a/crates/runtime/src/libcalls.rs
+++ b/crates/runtime/src/libcalls.rs
@@ -62,12 +62,8 @@ use crate::{Instance, TrapReason};
 use anyhow::bail;
 use anyhow::Result;
 use cfg_if::cfg_if;
-use std::mem;
-use std::ptr::{self, NonNull};
 use std::time::{Duration, Instant};
-use wasmtime_environ::{
-    DataIndex, ElemIndex, FuncIndex, GlobalIndex, MemoryIndex, TableIndex, Trap, Unsigned,
-};
+use wasmtime_environ::{DataIndex, ElemIndex, FuncIndex, MemoryIndex, TableIndex, Trap, Unsigned};
 #[cfg(feature = "wmemcheck")]
 use wasmtime_wmemcheck::AccessError::{
     DoubleMalloc, InvalidFree, InvalidRead, InvalidWrite, OutOfBounds,
@@ -80,6 +76,10 @@ use wasmtime_wmemcheck::AccessError::{
 /// now to ensure that the fp/sp on exit are recorded for backtraces to work
 /// properly.
 pub mod trampolines {
+    // Allow these things because of the macro and how we can't differentiate
+    // between doc comments and `cfg`s.
+    #![allow(unused_doc_comments, unused_attributes)]
+
     use crate::arch::wasm_to_libcall_trampoline;
     use crate::{Instance, TrapReason, VMContext};
 
@@ -96,6 +96,7 @@ pub mod trampolines {
                 // supported platforms or otherwise in inline assembly for
                 // platforms like s390x which don't have stable `global_asm!`
                 // yet.
+                $( #[$attr] )*
                 extern "C" {
                     #[allow(missing_docs)]
                     #[allow(improper_ctypes)]
@@ -106,6 +107,7 @@ pub mod trampolines {
                     ) $(-> libcall!(@ty $result))?;
                 }
 
+                $( #[ $attr ] )*
                 wasm_to_libcall_trampoline!($name ; [<impl_ $name>]);
 
                 // This is the direct entrypoint from the inline assembly which
@@ -119,6 +121,7 @@ pub mod trampolines {
                 // like s390x need to use outlined assembly files which requires
                 // `no_mangle`.
                 #[cfg_attr(target_arch = "s390x", wasmtime_versioned_export_macros::versioned_export)]
+                $( #[ $attr ] )*
                 unsafe extern "C" fn [<impl_ $name>](
                     vmctx: *mut VMContext,
                     $( $pname : libcall!(@ty $param), )*
@@ -139,6 +142,7 @@ pub mod trampolines {
                 // in a linking failure.
                 #[allow(non_upper_case_globals)]
                 #[used]
+                $( #[ $attr ] )*
                 static [<impl_ $name _ref>]: unsafe extern "C" fn(
                     *mut VMContext,
                     $( $pname : libcall!(@ty $param), )*
@@ -221,17 +225,12 @@ unsafe fn table_grow(
     init_value: *mut u8,
 ) -> Result<u32> {
     let table_index = TableIndex::from_u32(table_index);
+
     let element = match instance.table_element_type(table_index) {
         TableElementType::Func => (init_value as *mut VMFuncRef).into(),
-        TableElementType::Extern => {
-            let init_value = if init_value.is_null() {
-                None
-            } else {
-                Some(VMExternRef::clone_from_raw(init_value))
-            };
-            init_value.into()
-        }
+        TableElementType::Extern => VMExternRef::clone_from_raw(init_value).into(),
     };
+
     Ok(match instance.table_grow(table_index, delta, element)? {
         Some(r) => r,
         None => (-1_i32).unsigned(),
@@ -239,6 +238,8 @@ unsafe fn table_grow(
 }
 
 use table_grow as table_grow_func_ref;
+
+#[cfg(feature = "gc")]
 use table_grow as table_grow_externref;
 
 // Implementation of `table.fill`.
@@ -258,18 +259,17 @@ unsafe fn table_fill(
             let val = val as *mut VMFuncRef;
             table.fill(dst, val.into(), len)
         }
+
         TableElementType::Extern => {
-            let val = if val.is_null() {
-                None
-            } else {
-                Some(VMExternRef::clone_from_raw(val))
-            };
+            let val = VMExternRef::clone_from_raw(val);
             table.fill(dst, val.into(), len)
         }
     }
 }
 
 use table_fill as table_fill_func_ref;
+
+#[cfg(feature = "gc")]
 use table_fill as table_fill_externref;
 
 // Implementation of `table.copy`.
@@ -380,16 +380,18 @@ unsafe fn table_get_lazy_init_func_ref(
 }
 
 // Drop a `VMExternRef`.
+#[cfg(feature = "gc")]
 unsafe fn drop_externref(_instance: &mut Instance, externref: *mut u8) {
     let externref = externref as *mut crate::externref::VMExternData;
-    let externref = NonNull::new(externref).unwrap().into();
+    let externref = std::ptr::NonNull::new(externref).unwrap().into();
     crate::externref::VMExternData::drop_and_dealloc(externref);
 }
 
 // Do a GC and insert the given `externref` into the
 // `VMExternRefActivationsTable`.
+#[cfg(feature = "gc")]
 unsafe fn activations_table_insert_with_gc(instance: &mut Instance, externref: *mut u8) {
-    let externref = VMExternRef::clone_from_raw(externref);
+    let externref = VMExternRef::clone_from_raw(externref).unwrap();
     let limits = *instance.runtime_limits();
     let (activations_table, module_info_lookup) = (*instance.store()).externref_activations_table();
 
@@ -406,12 +408,13 @@ unsafe fn activations_table_insert_with_gc(instance: &mut Instance, externref: *
 }
 
 // Perform a Wasm `global.get` for `externref` globals.
+#[cfg(feature = "gc")]
 unsafe fn externref_global_get(instance: &mut Instance, index: u32) -> *mut u8 {
-    let index = GlobalIndex::from_u32(index);
+    let index = wasmtime_environ::GlobalIndex::from_u32(index);
     let limits = *instance.runtime_limits();
     let global = instance.defined_or_imported_global_ptr(index);
     match (*global).as_externref().clone() {
-        None => ptr::null_mut(),
+        None => std::ptr::null_mut(),
         Some(externref) => {
             let raw = externref.as_raw();
             let (activations_table, module_info_lookup) =
@@ -423,21 +426,18 @@ unsafe fn externref_global_get(instance: &mut Instance, index: u32) -> *mut u8 {
 }
 
 // Perform a Wasm `global.set` for `externref` globals.
+#[cfg(feature = "gc")]
 unsafe fn externref_global_set(instance: &mut Instance, index: u32, externref: *mut u8) {
-    let externref = if externref.is_null() {
-        None
-    } else {
-        Some(VMExternRef::clone_from_raw(externref))
-    };
+    let externref = VMExternRef::clone_from_raw(externref);
 
-    let index = GlobalIndex::from_u32(index);
+    let index = wasmtime_environ::GlobalIndex::from_u32(index);
     let global = instance.defined_or_imported_global_ptr(index);
 
     // Swap the new `externref` value into the global before we drop the old
     // value. This protects against an `externref` with a `Drop` implementation
     // that calls back into Wasm and touches this global again (we want to avoid
     // it observing a halfway-deinitialized value).
-    let old = mem::replace((*global).as_externref_mut(), externref);
+    let old = std::mem::replace((*global).as_externref_mut(), externref);
     drop(old);
 }
 
diff --git a/crates/runtime/src/table.rs b/crates/runtime/src/table.rs
index 5740b94d2..81bd095f3 100644
--- a/crates/runtime/src/table.rs
+++ b/crates/runtime/src/table.rs
@@ -2,8 +2,11 @@
 //!
 //! `Table` is to WebAssembly tables what `LinearMemory` is to WebAssembly linear memories.
 
+#![cfg_attr(feature = "gc", allow(irrefutable_let_patterns))]
+
+use crate::externref::VMExternRef;
 use crate::vmcontext::{VMFuncRef, VMTableDefinition};
-use crate::{SendSyncPtr, Store, VMExternRef};
+use crate::{SendSyncPtr, Store};
 use anyhow::{bail, format_err, Error, Result};
 use sptr::Strict;
 use std::ops::Range;
@@ -19,8 +22,10 @@ use wasmtime_environ::{
 pub enum TableElement {
     /// A `funcref`.
     FuncRef(*mut VMFuncRef),
+
     /// An `exrernref`.
     ExternRef(Option<VMExternRef>),
+
     /// An uninitialized funcref value. This should never be exposed
     /// beyond the `wasmtime` crate boundary; the upper-level code
     /// (which has access to the info needed for lazy initialization)
@@ -57,7 +62,7 @@ impl TableElement {
             }
             (TableElementType::Extern, None) => Self::ExternRef(None),
             (TableElementType::Extern, Some(ptr)) => {
-                Self::ExternRef(Some(VMExternRef::from_raw(ptr.as_ptr())))
+                Self::ExternRef(VMExternRef::from_raw(ptr.as_ptr()))
             }
         }
     }
@@ -73,7 +78,7 @@ impl TableElement {
             TableElementType::Func => TableElement::from_table_value(ty, ptr),
 
             TableElementType::Extern => {
-                Self::ExternRef(ptr.map(|p| VMExternRef::clone_from_raw(p.as_ptr())))
+                Self::ExternRef(ptr.and_then(|p| VMExternRef::clone_from_raw(p.as_ptr())))
             }
         }
     }
@@ -112,8 +117,8 @@ impl TableElement {
     pub(crate) unsafe fn into_ref_asserting_initialized(self) -> *mut u8 {
         match self {
             Self::FuncRef(e) => e.cast(),
-            Self::ExternRef(e) => e.map_or(ptr::null_mut(), |e| e.into_raw()),
             Self::UninitFunc => panic!("Uninitialized table element value outside of table slot"),
+            Self::ExternRef(e) => e.map_or(ptr::null_mut(), |e| e.into_raw()),
         }
     }
 
@@ -579,7 +584,7 @@ impl Drop for Table {
     fn drop(&mut self) {
         let ty = self.element_type();
 
-        // funcref tables can skip this
+        // `funcref` tables don't need drops.
         if let TableElementType::Func = ty {
             return;
         }
diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs
index 05235418b..5b03966c1 100644
--- a/crates/runtime/src/vmcontext.rs
+++ b/crates/runtime/src/vmcontext.rs
@@ -390,7 +390,6 @@ pub struct VMGlobalDefinition {
 #[cfg(test)]
 mod test_vmglobal_definition {
     use super::VMGlobalDefinition;
-    use crate::externref::VMExternRef;
     use std::mem::{align_of, size_of};
     use wasmtime_environ::{Module, PtrSize, VMOffsets};
 
@@ -421,7 +420,9 @@ mod test_vmglobal_definition {
     }
 
     #[test]
+    #[cfg(feature = "gc")]
     fn check_vmglobal_can_contain_externref() {
+        use crate::externref::VMExternRef;
         assert!(size_of::<VMExternRef>() <= size_of::<VMGlobalDefinition>());
     }
 }
@@ -534,16 +535,20 @@ impl VMGlobalDefinition {
 
     /// Return a reference to the value as an externref.
     pub unsafe fn as_externref(&self) -> &Option<VMExternRef> {
-        &*(self.storage.as_ref().as_ptr().cast::<Option<VMExternRef>>())
+        let ret = &*(self.storage.as_ref().as_ptr().cast::<Option<VMExternRef>>());
+        assert!(cfg!(feature = "gc") || ret.is_none());
+        ret
     }
 
     /// Return a mutable reference to the value as an externref.
     pub unsafe fn as_externref_mut(&mut self) -> &mut Option<VMExternRef> {
-        &mut *(self
+        let ret = &mut *(self
             .storage
             .as_mut()
             .as_mut_ptr()
-            .cast::<Option<VMExternRef>>())
+            .cast::<Option<VMExternRef>>());
+        assert!(cfg!(feature = "gc") || ret.is_none());
+        ret
     }
 
     /// Return a reference to the value as a `VMFuncRef`.
@@ -707,6 +712,7 @@ macro_rules! define_builtin_array {
         #[repr(C)]
         pub struct VMBuiltinFunctionsArray {
             $(
+                $( #[ $attr ] )*
                 $name: unsafe extern "C" fn(
                     $(define_builtin_array!(@ty $param)),*
                 ) $( -> define_builtin_array!(@ty $result))?,
@@ -714,8 +720,12 @@ macro_rules! define_builtin_array {
         }
 
         impl VMBuiltinFunctionsArray {
+            #[allow(unused_doc_comments)]
             pub const INIT: VMBuiltinFunctionsArray = VMBuiltinFunctionsArray {
-                $($name: crate::libcalls::trampolines::$name,)*
+                $(
+                    $( #[ $attr ] )*
+                    $name: crate::libcalls::trampolines::$name,
+                )*
             };
         }
     };
@@ -1097,6 +1107,7 @@ impl ValRaw {
     /// Creates a WebAssembly `externref` value
     #[inline]
     pub fn externref(i: *mut c_void) -> ValRaw {
+        assert!(cfg!(feature = "gc") || i.is_null());
         ValRaw {
             externref: Strict::map_addr(i, |i| i.to_le()),
         }
@@ -1153,7 +1164,9 @@ impl ValRaw {
     /// Gets the WebAssembly `externref` value
     #[inline]
     pub fn get_externref(&self) -> *mut c_void {
-        unsafe { Strict::map_addr(self.externref, |i| usize::from_le(i)) }
+        let ptr = unsafe { Strict::map_addr(self.externref, |i| usize::from_le(i)) };
+        assert!(cfg!(feature = "gc") || ptr.is_null());
+        ptr
     }
 }
 
diff --git a/crates/slab/Cargo.toml b/crates/slab/Cargo.toml
new file mode 100644
index 000000000..22ff266e6
--- /dev/null
+++ b/crates/slab/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+authors = ["The Wasmtime Project Developers"]
+description = "Uni-typed slab with a free list for use in Wasmtime"
+edition.workspace = true
+license = "Apache-2.0 WITH LLVM-exception"
+name = "wasmtime-slab"
+repository = "https://github.com/bytecodealliance/wasmtime"
+version.workspace = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/crates/slab/src/lib.rs b/crates/slab/src/lib.rs
new file mode 100644
index 000000000..f54adbce5
--- /dev/null
+++ b/crates/slab/src/lib.rs
@@ -0,0 +1,450 @@
+//! A very simple, uniformly-typed slab arena that supports deallocation and
+//! reusing deallocated entries' space.
+//!
+//! The free list of vacant entries in the slab are stored inline in the slab's
+//! existing storage.
+//!
+//! # Example
+//!
+//! ```
+//! use wasmtime_slab::{Id, Slab};
+//!
+//! let mut slab = Slab::new();
+//!
+//! // Insert some values into the slab.
+//! let rza = slab.alloc("Robert Fitzgerald Diggs");
+//! let gza = slab.alloc("Gary Grice");
+//! let bill = slab.alloc("Bill Gates");
+//!
+//! // Alloced elements can be accessed infallibly via indexing (and missing and
+//! // deallocated entries will panic).
+//! assert_eq!(slab[rza], "Robert Fitzgerald Diggs");
+//!
+//! // Alternatively, the `get` and `get_mut` methods provide fallible lookup.
+//! if let Some(genius) = slab.get(gza) {
+//!     println!("The gza gza genius: {}", genius);
+//! }
+//! if let Some(val) = slab.get_mut(bill) {
+//!     *val = "Bill Gates doesn't belong in this set...";
+//! }
+//!
+//! // We can remove values from the slab.
+//! slab.dealloc(bill);
+//!
+//! // Allocate a new entry.
+//! let bill = slab.alloc("Bill Murray");
+//! ```
+//!
+//! # Using `Id`s with the Wrong `Slab`
+//!
+//! `Slab` does NOT check that `Id`s used to access previously-allocated values
+//! came from the current `Slab` instance (as opposed to a different `Slab`
+//! instance). Using `Id`s from a different `Slab` is safe, but will yield an
+//! unrelated value, if any at all.
+//!
+//! If you desire checking that an `Id` came from the correct `Slab` instance,
+//! it should be easy to layer that functionality on top of this crate by
+//! wrapping `Slab` and `Id` in types that additionally maintain a slab instance
+//! identifier.
+//!
+//! # The ABA Problem
+//!
+//! This `Slab` type does NOT protect against ABA bugs, such as the following
+//! sequence:
+//!
+//! * Value `A` is allocated into the slab, yielding id `i`.
+//!
+//! * `A` is deallocated, and so `i`'s associated entry is added to the slab's
+//! free list.
+//!
+//! * Value `B` is allocated into the slab, reusing `i`'s associated entry,
+//! yielding id `i`.
+//!
+//! * The "original" id `i` is used to access the arena, expecting the
+//! deallocated value `A`, but getting the new value `B`.
+//!
+//! That is, it does not detect and prevent against the memory-safe version of
+//! use-after-free bugs.
+//!
+//! If you need to protect against ABA bugs, it should be easy to layer that
+//! functionality on top of this crate by wrapping `Slab` with something like
+//! the following:
+//!
+//! ```rust
+//! pub struct GenerationalId {
+//!     id: wasmtime_slab::Id,
+//!     generation: u32,
+//! }
+//!
+//! struct GenerationalEntry<T> {
+//!     value: T,
+//!     generation: u32,
+//! }
+//!
+//! pub struct GenerationalSlab<T> {
+//!     slab: wasmtime_slab::Slab<GenerationalEntry<T>>,
+//!     generation: u32,
+//! }
+//!
+//! impl<T> GenerationalSlab<T> {
+//!     pub fn alloc(&mut self, value: T) -> GenerationalId {
+//!         let generation = self.generation;
+//!         let id = self.slab.alloc(GenerationalEntry { value, generation });
+//!         GenerationalId { id, generation }
+//!     }
+//!
+//!     pub fn get(&self, id: GenerationalId) -> Option<&T> {
+//!         let entry = self.slab.get(id.id)?;
+//!
+//!         // Check that the entry's generation matches the id's generation,
+//!         // else we have an ABA bug. (Alternatively, return `None` instead
+//!         // of panicking.)
+//!         assert_eq!(id.generation, entry.generation);
+//!
+//!         Some(&entry.value)
+//!     }
+//!
+//!     pub fn dealloc(&mut self, id: GenerationalId) {
+//!         // Check that the entry's generation matches the id's generation,
+//!         // else we have an ABA bug. (Alternatively, silently return on
+//!         // double-free instead of panicking.)
+//!         assert_eq!(id.generation, self.slab[id.id].generation);
+//!
+//!         self.slab.dealloc(id.id);
+//!
+//!         // Increment our generation whenever we deallocate so that any new
+//!         // value placed in this same entry will have a different generation
+//!         // and we can detect ABA bugs.
+//!         self.generation += 1;
+//!     }
+//! }
+//! ```
+
+#![forbid(unsafe_code)]
+#![deny(missing_docs, missing_debug_implementations)]
+
+use std::num::NonZeroU32;
+
+/// An identifier for an allocated value inside a `slab`.
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Id(EntryIndex);
+
+impl std::fmt::Debug for Id {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_tuple("Id").field(&self.0.index()).finish()
+    }
+}
+
+impl Id {
+    /// Get the raw underlying representation of this `Id`.
+    #[inline]
+    pub fn into_raw(self) -> u32 {
+        u32::try_from(self.0.index()).unwrap()
+    }
+
+    /// Construct an `Id` from its raw underlying representation.
+    ///
+    /// `raw` should be a value that was previously created via
+    /// `Id::into_raw`. May panic if given arbitrary values.
+    #[inline]
+    pub fn from_raw(raw: u32) -> Self {
+        let raw = usize::try_from(raw).unwrap();
+        Self(EntryIndex::new(raw))
+    }
+}
+
+/// A simple, uni-typed slab arena.
+pub struct Slab<T> {
+    /// The slab's entries, each is either occupied and holding a `T` or vacant
+    /// and is a link the free list.
+    entries: Vec<Entry<T>>,
+
+    /// The index of the first free entry in the free list.
+    free: Option<EntryIndex>,
+
+    /// The number of occupied entries is this slab.
+    len: u32,
+}
+
+impl<T> std::fmt::Debug for Slab<T>
+where
+    T: std::fmt::Debug,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_map().entries(self.iter()).finish()
+    }
+}
+
+enum Entry<T> {
+    /// An occupied entry holding a `T`.
+    Occupied(T),
+
+    /// A vacant entry.
+    Free {
+        /// A link in the slab's free list, pointing to the next free entry, if
+        /// any.
+        next_free: Option<EntryIndex>,
+    },
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+struct EntryIndex(NonZeroU32);
+
+impl EntryIndex {
+    #[inline]
+    fn new(index: usize) -> Self {
+        assert!(index <= Slab::<()>::MAX_CAPACITY);
+        let x = u32::try_from(index + 1).unwrap();
+        Self(NonZeroU32::new(x).unwrap())
+    }
+
+    #[inline]
+    fn index(&self) -> usize {
+        let index = self.0.get() - 1;
+        usize::try_from(index).unwrap()
+    }
+}
+
+impl<T> Default for Slab<T> {
+    #[inline]
+    fn default() -> Self {
+        Self {
+            entries: Vec::default(),
+            free: None,
+            len: 0,
+        }
+    }
+}
+
+impl<T> std::ops::Index<Id> for Slab<T> {
+    type Output = T;
+
+    #[inline]
+    fn index(&self, id: Id) -> &Self::Output {
+        self.get(id)
+            .expect("id from different slab or value was deallocated")
+    }
+}
+
+impl<T> std::ops::IndexMut<Id> for Slab<T> {
+    #[inline]
+    fn index_mut(&mut self, id: Id) -> &mut Self::Output {
+        self.get_mut(id)
+            .expect("id from different slab or value was deallocated")
+    }
+}
+
+impl<T> Slab<T> {
+    /// The maximum capacity any `Slab` can have: `u32::MAX - 1`.
+    pub const MAX_CAPACITY: usize = (u32::MAX - 1) as usize;
+
+    /// Construct a new, empty slab.
+    #[inline]
+    pub fn new() -> Self {
+        Slab::default()
+    }
+
+    /// Construct a new, empty slab, pre-reserving space for at least `capacity`
+    /// elements.
+    #[inline]
+    pub fn with_capacity(capacity: usize) -> Self {
+        let mut slab = Self::new();
+        slab.reserve(capacity);
+        slab
+    }
+
+    /// Ensure that there is space for at least `additional` elements in this
+    /// slab.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the new capacity exceeds `Self::MAX_CAPACITY`.
+    pub fn reserve(&mut self, additional: usize) {
+        let cap = self.capacity();
+        let len = self.len();
+        assert!(cap >= len);
+        if cap - len >= additional {
+            // Already have `additional` capacity available.
+            return;
+        }
+
+        self.entries.reserve(additional);
+
+        // Maintain the invariant that `i <= MAX_CAPACITY` for all indices `i`
+        // in `self.entries`.
+        assert!(self.entries.capacity() <= Self::MAX_CAPACITY);
+    }
+
+    fn double_capacity(&mut self) {
+        // Double our capacity to amortize the cost of resizing. But make sure
+        // we add some amount of minimum additional capacity, since doubling
+        // zero capacity isn't useful.
+        const MIN_CAPACITY: usize = 16;
+        let additional = std::cmp::max(self.entries.capacity(), MIN_CAPACITY);
+        self.reserve(additional);
+    }
+
+    /// What is the capacity of this slab? That is, how many entries can it
+    /// contain within its current underlying storage?
+    #[inline]
+    pub fn capacity(&self) -> usize {
+        self.entries.capacity()
+    }
+
+    /// How many values are currently allocated within this slab?
+    #[inline]
+    pub fn len(&self) -> usize {
+        usize::try_from(self.len).unwrap()
+    }
+
+    /// Are there zero allocated values within this slab?
+    #[inline]
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+
+    /// Try to allocate a `T` value within this slab.
+    ///
+    /// If there is no available capacity, ownership of the given value is
+    /// returned via `Err(value)`.
+    #[inline]
+    pub fn try_alloc(&mut self, value: T) -> Result<Id, T> {
+        if let Some(index) = self.try_alloc_index() {
+            let next_free = match self.entries[index.index()] {
+                Entry::Free { next_free } => next_free,
+                Entry::Occupied { .. } => unreachable!(),
+            };
+            self.free = next_free;
+            self.entries[index.index()] = Entry::Occupied(value);
+            self.len += 1;
+            Ok(Id(index))
+        } else {
+            Err(value)
+        }
+    }
+
+    #[inline]
+    fn try_alloc_index(&mut self) -> Option<EntryIndex> {
+        self.free.take().or_else(|| {
+            if self.entries.len() < self.entries.capacity() {
+                let index = EntryIndex::new(self.entries.len());
+                self.entries.push(Entry::Free { next_free: None });
+                Some(index)
+            } else {
+                None
+            }
+        })
+    }
+
+    /// Allocate a `T` value within this slab, allocating additional underlying
+    /// storage if there is no available capacity.
+    ///
+    /// # Panics
+    ///
+    /// Panics if allocating this value requires reallocating the underlying
+    /// storage, and the new capacity exceeds `Slab::MAX_CAPACITY`.
+    #[inline]
+    pub fn alloc(&mut self, value: T) -> Id {
+        self.try_alloc(value)
+            .unwrap_or_else(|value| self.alloc_slow(value))
+    }
+
+    /// Get the `Id` that will be returned for the next allocation in this slab.
+    #[inline]
+    pub fn next_id(&self) -> Id {
+        let index = self.free.unwrap_or_else(|| EntryIndex::new(self.len()));
+        Id(index)
+    }
+
+    #[inline(never)]
+    #[cold]
+    fn alloc_slow(&mut self, value: T) -> Id {
+        // Reserve additional capacity, since we didn't have space for the
+        // allocation.
+        self.double_capacity();
+        // After which the allocation will succeed.
+        self.try_alloc(value).ok().unwrap()
+    }
+
+    /// Get a shared borrow of the value associated with `id`.
+    ///
+    /// Returns `None` if the value has since been deallocated.
+    ///
+    /// If `id` comes from a different `Slab` instance, this method may panic,
+    /// return `None`, or return an arbitrary value.
+    #[inline]
+    pub fn get(&self, id: Id) -> Option<&T> {
+        match self
+            .entries
+            .get(id.0.index())
+            .expect("id from different slab")
+        {
+            Entry::Occupied(x) => Some(x),
+            Entry::Free { .. } => None,
+        }
+    }
+
+    /// Get an exclusive borrow of the value associated with `id`.
+    ///
+    /// Returns `None` if the value has since been deallocated.
+    ///
+    /// If `id` comes from a different `Slab` instance, this method may panic,
+    /// return `None`, or return an arbitrary value.
+    #[inline]
+    pub fn get_mut(&mut self, id: Id) -> Option<&mut T> {
+        match self
+            .entries
+            .get_mut(id.0.index())
+            .expect("id from different slab")
+        {
+            Entry::Occupied(x) => Some(x),
+            Entry::Free { .. } => None,
+        }
+    }
+
+    /// Does this slab contain an allocated value for `id`?
+    #[inline]
+    pub fn contains(&self, id: Id) -> bool {
+        match self.entries.get(id.0.index()) {
+            Some(Entry::Occupied(_)) => true,
+            None | Some(Entry::Free { .. }) => false,
+        }
+    }
+
+    /// Iterate over all values currently allocated within this `Slab`.
+    ///
+    /// Yields pairs of an `Id` and the `Id`'s associated value.
+    ///
+    /// Iteration order is undefined.
+    #[inline]
+    pub fn iter(&self) -> impl Iterator<Item = (Id, &T)> + '_ {
+        assert!(self.entries.len() <= Self::MAX_CAPACITY);
+        self.entries
+            .iter()
+            .enumerate()
+            .filter_map(|(i, e)| match e {
+                Entry::Occupied(x) => Some((Id(EntryIndex::new(i)), x)),
+                Entry::Free { .. } => None,
+            })
+    }
+
+    /// Deallocate the value associated with the given `id`.
+    ///
+    /// If `id` comes from a different `Slab` instance, this method may panic,
+    /// do nothing, or deallocate an arbitrary value.
+    #[inline]
+    pub fn dealloc(&mut self, id: Id) {
+        match self
+            .entries
+            .get_mut(id.0.index())
+            .expect("id from a different slab")
+        {
+            Entry::Free { .. } => panic!("attempt to deallocate an entry that is already vacant"),
+            e @ Entry::Occupied(_) => {
+                let next_free = std::mem::replace(&mut self.free, Some(id.0));
+                *e = Entry::Free { next_free };
+                self.len -= 1;
+            }
+        }
+    }
+}
diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs
index 3c86683f3..00f898bc2 100644
--- a/crates/wasi-preview1-component-adapter/src/lib.rs
+++ b/crates/wasi-preview1-component-adapter/src/lib.rs
@@ -79,7 +79,7 @@ pub mod bindings {
 
     #[cfg(feature = "proxy")]
     wit_bindgen::generate!({
-        path: "./crates/wasi/wit",
+        path: "../wasi-http/wit",
         inline: r#"
             package wasmtime:adapter;
 
diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml
index 7dae1fbfa..d923863fa 100644
--- a/crates/wasmtime/Cargo.toml
+++ b/crates/wasmtime/Cargo.toml
@@ -32,6 +32,7 @@ wasmtime-cranelift = { workspace = true, optional = true }
 wasmtime-winch = { workspace = true, optional = true }
 wasmtime-component-macro = { workspace = true, optional = true }
 wasmtime-component-util = { workspace = true, optional = true }
+wasmtime-slab = { workspace = true, optional = true }
 target-lexicon = { workspace = true }
 wasmparser = { workspace = true }
 wasm-encoder = { workspace = true, optional = true }
@@ -85,6 +86,7 @@ wasi-common = { path = "../wasi-common", default-features = true }
 default = [
   'async',
   'cache',
+  'gc',
   'wat',
   'profiling',
   'parallel-compilation',
@@ -170,4 +172,20 @@ coredump = ["dep:wasm-encoder", "runtime"]
 debug-builtins = ["wasmtime-runtime?/debug-builtins"]
 
 # Enable support for executing compiled Wasm modules.
-runtime = ["dep:wasmtime-runtime", "dep:wasmtime-jit-icache-coherence"]
+runtime = ["dep:wasmtime-runtime", "dep:wasmtime-jit-icache-coherence", "dep:wasmtime-slab"]
+
+# Enable support for garbage collection-related things.
+#
+# This Cargo feature is required to compile or run Wasm that uses any of the
+# following Wasm proposals:
+#
+# * Reference types: https://github.com/WebAssembly/reference-types/
+# * Typed function references: https://github.com/WebAssembly/function-references/
+# * Garbage collection: https://github.com/WebAssembly/gc
+#
+# When a compiler Cargo feature (`cranelift` or `winch`) is enabled, this
+# feature gates the ability to compile Wasm that uses those proposals.
+#
+# When the `runtime` Cargo feature is enabled, this feature gates the ability to
+# load and run Wasm that uses those proposals.
+gc = ["wasmtime-environ/gc", "wasmtime-runtime?/gc", "wasmtime-cranelift?/gc"]
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index 386300406..51d8258c1 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -252,7 +252,15 @@ impl Config {
             ret.cranelift_opt_level(OptLevel::Speed);
         }
 
+        #[cfg(feature = "gc")]
         ret.wasm_reference_types(true);
+        #[cfg(not(feature = "gc"))]
+        {
+            ret.features.reference_types = false;
+            ret.features.function_references = false;
+            ret.features.gc = false;
+        }
+
         ret.wasm_multi_value(true);
         ret.wasm_bulk_memory(true);
         ret.wasm_simd(true);
@@ -729,6 +737,8 @@ impl Config {
     /// and thus may cause `Engine::new` fail if the `bulk_memory` feature is disabled.
     ///
     /// [proposal]: https://github.com/webassembly/reference-types
+    #[cfg(feature = "gc")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn wasm_reference_types(&mut self, enable: bool) -> &mut Self {
         self.features.reference_types = enable;
         self
@@ -747,6 +757,8 @@ impl Config {
     /// This feature is `false` by default.
     ///
     /// [proposal]: https://github.com/WebAssembly/function-references
+    #[cfg(feature = "gc")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn wasm_function_references(&mut self, enable: bool) -> &mut Self {
         self.features.function_references = enable;
         self
@@ -767,6 +779,8 @@ impl Config {
     /// progress and generally not ready for primetime.**
     ///
     /// [proposal]: https://github.com/WebAssembly/gc
+    #[cfg(feature = "gc")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn wasm_gc(&mut self, enable: bool) -> &mut Self {
         self.features.gc = enable;
         self
diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs
index a26cf28cb..8c854f2e7 100644
--- a/crates/wasmtime/src/engine/serialization.rs
+++ b/crates/wasmtime/src/engine/serialization.rs
@@ -22,7 +22,7 @@
 //! using wasmtime artifacts across versions.
 
 use crate::{Engine, ModuleVersionStrategy, Precompiled};
-use anyhow::{anyhow, bail, Context, Result};
+use anyhow::{anyhow, bail, ensure, Context, Result};
 use object::write::{Object, StandardSegment};
 use object::{File, FileFlags, Object as _, ObjectSection, SectionKind};
 use serde_derive::{Deserialize, Serialize};
@@ -406,6 +406,27 @@ impl Metadata<'_> {
         Ok(())
     }
 
+    fn check_cfg_bool(
+        cfg: bool,
+        cfg_str: &str,
+        found: bool,
+        expected: bool,
+        feature: &str,
+    ) -> Result<()> {
+        if cfg {
+            Self::check_bool(found, expected, feature)
+        } else {
+            assert!(!expected);
+            ensure!(
+                !found,
+                "Module was compiled with {feature} but support in the host \
+                 was disabled at compile time because the `{cfg_str}` Cargo \
+                 feature was not enabled",
+            );
+            Ok(())
+        }
+    }
+
     fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
         let WasmFeatures {
             reference_types,
@@ -424,11 +445,28 @@ impl Metadata<'_> {
             gc,
         } = self.features;
 
-        Self::check_bool(
+        Self::check_cfg_bool(
+            cfg!(feature = "gc"),
+            "gc",
             reference_types,
             other.reference_types,
             "WebAssembly reference types support",
         )?;
+        Self::check_cfg_bool(
+            cfg!(feature = "gc"),
+            "gc",
+            function_references,
+            other.function_references,
+            "WebAssembly function-references support",
+        )?;
+        Self::check_cfg_bool(
+            cfg!(feature = "gc"),
+            "gc",
+            gc,
+            other.gc,
+            "WebAssembly garbage collection support",
+        )?;
+
         Self::check_bool(
             multi_value,
             other.multi_value,
@@ -472,12 +510,6 @@ impl Metadata<'_> {
             other.relaxed_simd,
             "WebAssembly relaxed-simd support",
         )?;
-        Self::check_bool(
-            function_references,
-            other.function_references,
-            "WebAssembly function-references support",
-        )?;
-        Self::check_bool(gc, other.gc, "WebAssembly garbage collection support")?;
 
         Ok(())
     }
diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs
index aeab6ca51..db012948f 100644
--- a/crates/wasmtime/src/runtime.rs
+++ b/crates/wasmtime/src/runtime.rs
@@ -21,6 +21,7 @@ pub(crate) mod trampoline;
 pub(crate) mod trap;
 pub(crate) mod type_registry;
 pub(crate) mod types;
+pub(crate) mod uninhabited;
 pub(crate) mod v128;
 pub(crate) mod values;
 
@@ -66,6 +67,8 @@ pub use types::*;
 pub use v128::V128;
 pub use values::*;
 
+pub(crate) use uninhabited::*;
+
 #[cfg(feature = "profiling")]
 pub use profiling::GuestProfiler;
 
diff --git a/crates/wasmtime/src/runtime/externals/global.rs b/crates/wasmtime/src/runtime/externals/global.rs
index c09b2124a..d191e3f07 100644
--- a/crates/wasmtime/src/runtime/externals/global.rs
+++ b/crates/wasmtime/src/runtime/externals/global.rs
@@ -110,16 +110,18 @@ impl Global {
                 ValType::V128 => Val::V128((*definition.as_u128()).into()),
                 ValType::Ref(ref_ty) => {
                     let reference = match ref_ty.heap_type() {
+                        HeapType::Func | HeapType::Concrete(_) => {
+                            Ref::Func(Func::from_raw(store, definition.as_func_ref().cast()))
+                        }
+
+                        HeapType::NoFunc => Ref::Func(None),
+
                         HeapType::Extern => Ref::Extern(
                             definition
                                 .as_externref()
                                 .clone()
-                                .map(|inner| ExternRef { inner }),
+                                .map(|inner| ExternRef::from_vm_extern_ref(inner)),
                         ),
-                        HeapType::Func | HeapType::Concrete(_) => {
-                            Ref::Func(Func::from_raw(store, definition.as_func_ref().cast()))
-                        }
-                        HeapType::NoFunc => Ref::Func(None),
                     };
                     debug_assert!(
                         ref_ty.is_nullable() || !reference.is_null(),
@@ -165,7 +167,10 @@ impl Global {
                 Val::ExternRef(e) => {
                     // Take care to invoke the `Drop` implementation of the
                     // existing `ExternRef` so that it doesn't leak.
-                    let old = mem::replace(definition.as_externref_mut(), e.map(|e| e.inner));
+                    let old = mem::replace(
+                        definition.as_externref_mut(),
+                        e.map(|e| e.into_vm_extern_ref()),
+                    );
                     drop(old);
                 }
             }
diff --git a/crates/wasmtime/src/runtime/externals/table.rs b/crates/wasmtime/src/runtime/externals/table.rs
index d3537dce6..247e39e98 100644
--- a/crates/wasmtime/src/runtime/externals/table.rs
+++ b/crates/wasmtime/src/runtime/externals/table.rs
@@ -154,14 +154,18 @@ impl Table {
                     let func = Func::from_vm_func_ref(store, f);
                     Some(func.into())
                 }
+
+                runtime::TableElement::UninitFunc => {
+                    unreachable!("lazy init above should have converted UninitFunc")
+                }
+
                 runtime::TableElement::ExternRef(None) => Some(Ref::Extern(None)),
+
+                #[cfg_attr(not(feature = "gc"), allow(unreachable_code, unused_variables))]
                 runtime::TableElement::ExternRef(Some(x)) => {
-                    let x = ExternRef { inner: x };
+                    let x = ExternRef::from_vm_extern_ref(x);
                     Some(x.into())
                 }
-                runtime::TableElement::UninitFunc => {
-                    unreachable!("lazy init above should have converted UninitFunc")
-                }
             }
         }
     }
diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs
index a5cf6df63..dd539c819 100644
--- a/crates/wasmtime/src/runtime/func.rs
+++ b/crates/wasmtime/src/runtime/func.rs
@@ -1,3 +1,4 @@
+use crate::runtime::Uninhabited;
 use crate::store::{StoreData, StoreOpaque, Stored};
 use crate::type_registry::RegisteredType;
 use crate::{
@@ -78,9 +79,6 @@ pub struct NoFunc {
     _inner: Uninhabited,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-enum Uninhabited {}
-
 impl NoFunc {
     /// Get the null `(ref null nofunc)` (aka `nullfuncref`) reference.
     #[inline]
@@ -1158,20 +1156,23 @@ impl Func {
 
         let values_vec_size = params.len().max(ty.results().len());
 
-        // Whenever we pass `externref`s from host code to Wasm code, they
-        // go into the `VMExternRefActivationsTable`. But the table might be
-        // at capacity already, so check for that. If it is at capacity
-        // (unlikely) then do a GC to free up space. This is necessary
-        // because otherwise we would either keep filling up the bump chunk
-        // and making it larger and larger or we would always take the slow
-        // path when inserting references into the table.
-        if ty.as_wasm_func_type().externref_params_count()
-            > store
-                .0
-                .externref_activations_table()
-                .bump_capacity_remaining()
+        #[cfg(feature = "gc")]
         {
-            store.gc();
+            // Whenever we pass `externref`s from host code to Wasm code, they
+            // go into the `VMExternRefActivationsTable`. But the table might be
+            // at capacity already, so check for that. If it is at capacity
+            // (unlikely) then do a GC to free up space. This is necessary
+            // because otherwise we would either keep filling up the bump chunk
+            // and making it larger and larger or we would always take the slow
+            // path when inserting references into the table.
+            if ty.as_wasm_func_type().externref_params_count()
+                > store
+                    .0
+                    .externref_activations_table()
+                    .bump_capacity_remaining()
+            {
+                store.gc();
+            }
         }
 
         // Store the argument values into `values_vec`.
@@ -1307,15 +1308,18 @@ impl Func {
         let (params, results) = val_vec.split_at_mut(nparams);
         func(caller.sub_caller(), params, results)?;
 
-        // See the comment in `Func::call_impl`'s `write_params` function.
-        if ty.as_wasm_func_type().externref_returns_count()
-            > caller
-                .store
-                .0
-                .externref_activations_table()
-                .bump_capacity_remaining()
+        #[cfg(feature = "gc")]
         {
-            caller.store.gc();
+            // See the comment in `Func::call_impl`'s `write_params` function.
+            if ty.as_wasm_func_type().externref_returns_count()
+                > caller
+                    .store
+                    .0
+                    .externref_activations_table()
+                    .bump_capacity_remaining()
+            {
+                caller.store.gc();
+            }
         }
 
         // Unlike our arguments we need to dynamically check that the return
@@ -2023,6 +2027,8 @@ impl<T> Caller<'_, T> {
     /// Perform garbage collection of `ExternRef`s.
     ///
     /// Same as [`Store::gc`](crate::Store::gc).
+    #[cfg(feature = "gc")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "gc")))]
     pub fn gc(&mut self) {
         self.store.gc()
     }
diff --git a/crates/wasmtime/src/runtime/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs
index d2832bea1..4cf926061 100644
--- a/crates/wasmtime/src/runtime/func/typed.rs
+++ b/crates/wasmtime/src/runtime/func/typed.rs
@@ -156,14 +156,17 @@ where
             Self::debug_typecheck(store.0, func.as_ref().type_index);
         }
 
-        // See the comment in `Func::call_impl`'s `write_params` function.
-        if params.externrefs_count()
-            > store
-                .0
-                .externref_activations_table()
-                .bump_capacity_remaining()
+        #[cfg(feature = "gc")]
         {
-            store.gc();
+            // See the comment in `Func::call_impl`'s `write_params` function.
+            if params.externrefs_count()
+                > store
+                    .0
+                    .externref_activations_table()
+                    .bump_capacity_remaining()
+            {
+                store.gc();
+            }
         }
 
         // Validate that all runtime values flowing into this store indeed
@@ -423,6 +426,7 @@ floats! {
     f64/u64/get_f64 => F64
 }
 
+#[cfg(feature = "gc")]
 unsafe impl WasmTy for ExternRef {
     type Abi = NonNull<u8>;
 
@@ -460,7 +464,8 @@ unsafe impl WasmTy for ExternRef {
 
     #[inline]
     fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi {
-        let abi = self.inner.as_raw();
+        let inner = self.into_vm_extern_ref();
+        let abi = inner.as_raw();
         unsafe {
             // NB: We _must not_ trigger a GC when passing refs from host
             // code into Wasm (e.g. returned from a host function or passed
@@ -491,7 +496,7 @@ unsafe impl WasmTy for ExternRef {
             // In conclusion, to prevent uses after free, we cannot GC
             // during this insertion.
             let mut store = AutoAssertNoGc::new(store);
-            store.insert_vmexternref_without_gc(self.inner);
+            store.insert_vmexternref_without_gc(inner);
 
             debug_assert!(!abi.is_null());
             NonNull::new_unchecked(abi)
@@ -500,12 +505,12 @@ unsafe impl WasmTy for ExternRef {
 
     #[inline]
     unsafe fn from_abi(abi: Self::Abi, _store: &mut StoreOpaque) -> Self {
-        ExternRef {
-            inner: wasmtime_runtime::VMExternRef::clone_from_raw(abi.as_ptr()),
-        }
+        let inner = wasmtime_runtime::VMExternRef::clone_from_raw(abi.as_ptr()).unwrap();
+        ExternRef::from_vm_extern_ref(inner)
     }
 }
 
+#[cfg(feature = "gc")]
 unsafe impl WasmTy for Option<ExternRef> {
     type Abi = *mut u8;
 
@@ -550,13 +555,8 @@ unsafe impl WasmTy for Option<ExternRef> {
 
     #[inline]
     unsafe fn from_abi(abi: Self::Abi, _store: &mut StoreOpaque) -> Self {
-        if abi.is_null() {
-            None
-        } else {
-            Some(ExternRef {
-                inner: wasmtime_runtime::VMExternRef::clone_from_raw(abi),
-            })
-        }
+        let inner = wasmtime_runtime::VMExternRef::clone_from_raw(abi)?;
+        Some(ExternRef::from_vm_extern_ref(inner))
     }
 }
 
diff --git a/crates/wasmtime/src/runtime/module/registry.rs b/crates/wasmtime/src/runtime/module/registry.rs
index 1b6e286e3..9c8b50528 100644
--- a/crates/wasmtime/src/runtime/module/registry.rs
+++ b/crates/wasmtime/src/runtime/module/registry.rs
@@ -11,7 +11,7 @@ use std::{
     ptr::NonNull,
     sync::{Arc, RwLock},
 };
-use wasmtime_runtime::{ModuleInfo, VMSharedTypeIndex, VMWasmCallFunction};
+use wasmtime_runtime::{VMSharedTypeIndex, VMWasmCallFunction};
 
 /// Used for registering modules with a store.
 ///
@@ -67,7 +67,7 @@ impl ModuleRegistry {
     }
 
     /// Fetches information about a registered module given a program counter value.
-    pub fn lookup_module_info(&self, pc: usize) -> Option<&dyn ModuleInfo> {
+    pub fn lookup_module_info(&self, pc: usize) -> Option<&dyn wasmtime_runtime::ModuleInfo> {
         let (module, _) = self.module_and_offset(pc)?;
         Some(module.module_info())
     }
diff --git a/crates/wasmtime/src/runtime/ref.rs b/crates/wasmtime/src/runtime/ref.rs
index e04cbea2a..1c4ca1110 100644
--- a/crates/wasmtime/src/runtime/ref.rs
+++ b/crates/wasmtime/src/runtime/ref.rs
@@ -1,109 +1,9 @@
-#![allow(missing_docs)]
-
-use crate::AsContextMut;
-use std::any::Any;
-use std::ffi::c_void;
-use wasmtime_runtime::VMExternRef;
-
-/// Represents an opaque reference to any data within WebAssembly.
-#[derive(Clone, Debug)]
-#[repr(transparent)]
-pub struct ExternRef {
-    pub(crate) inner: VMExternRef,
-}
-
-impl ExternRef {
-    /// Creates a new instance of `ExternRef` wrapping the given value.
-    pub fn new<T>(value: T) -> ExternRef
-    where
-        T: 'static + Any + Send + Sync,
-    {
-        let inner = VMExternRef::new(value);
-        ExternRef { inner }
-    }
-
-    /// Get the underlying data for this `ExternRef`.
-    pub fn data(&self) -> &dyn Any {
-        &*self.inner
-    }
-
-    /// Get the strong reference count for this `ExternRef`.
-    ///
-    /// Note that this loads the reference count with a `SeqCst` ordering to
-    /// synchronize with other threads.
-    pub fn strong_count(&self) -> usize {
-        self.inner.strong_count()
-    }
-
-    /// Does this `ExternRef` point to the same inner value as `other`?
-    ///
-    /// This is *only* pointer equality, and does *not* run any inner value's
-    /// `Eq` implementation.
-    pub fn ptr_eq(&self, other: &ExternRef) -> bool {
-        VMExternRef::eq(&self.inner, &other.inner)
-    }
-
-    /// Creates a new strongly-owned [`ExternRef`] from the raw value provided.
-    ///
-    /// This is intended to be used in conjunction with [`Func::new_unchecked`],
-    /// [`Func::call_unchecked`], and [`ValRaw`] with its `externref` field.
-    ///
-    /// This function assumes that `raw` is an externref value which is
-    /// currently rooted within the [`Store`].
-    ///
-    /// # Unsafety
-    ///
-    /// This function is particularly `unsafe` because `raw` not only must be a
-    /// valid externref value produced prior by `to_raw` but it must also be
-    /// correctly rooted within the store. When arguments are provided to a
-    /// callback with [`Func::new_unchecked`], for example, or returned via
-    /// [`Func::call_unchecked`], if a GC is performed within the store then
-    /// floating externref values are not rooted and will be GC'd, meaning that
-    /// this function will no longer be safe to call with the values cleaned up.
-    /// This function must be invoked *before* possible GC operations can happen
-    /// (such as calling wasm).
-    ///
-    /// When in doubt try to not use this. Instead use the safe Rust APIs of
-    /// [`TypedFunc`] and friends.
-    ///
-    /// [`Func::call_unchecked`]: crate::Func::call_unchecked
-    /// [`Func::new_unchecked`]: crate::Func::new_unchecked
-    /// [`Store`]: crate::Store
-    /// [`TypedFunc`]: crate::TypedFunc
-    /// [`ValRaw`]: crate::ValRaw
-    pub unsafe fn from_raw(raw: *mut c_void) -> Option<ExternRef> {
-        let raw = raw.cast::<u8>();
-        if raw.is_null() {
-            None
-        } else {
-            Some(ExternRef {
-                inner: VMExternRef::clone_from_raw(raw),
-            })
-        }
-    }
-
-    /// Converts this [`ExternRef`] to a raw value suitable to store within a
-    /// [`ValRaw`].
-    ///
-    /// # Unsafety
-    ///
-    /// Produces a raw value which is only safe to pass into a store if a GC
-    /// doesn't happen between when the value is produce and when it's passed
-    /// into the store.
-    ///
-    /// [`ValRaw`]: crate::ValRaw
-    pub unsafe fn to_raw(&self, mut store: impl AsContextMut) -> *mut c_void {
-        let externref_ptr = self.inner.as_raw();
-        store
-            .as_context_mut()
-            .0
-            .insert_vmexternref_without_gc(self.inner.clone());
-        externref_ptr.cast()
-    }
-}
-
-impl std::fmt::Pointer for ExternRef {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        std::fmt::Pointer::fmt(&self.inner, f)
-    }
-}
+#[cfg(feature = "gc")]
+mod gc_ref;
+#[cfg(feature = "gc")]
+pub use gc_ref::*;
+
+#[cfg(not(feature = "gc"))]
+mod no_gc_ref;
+#[cfg(not(feature = "gc"))]
+pub use no_gc_ref::*;
diff --git a/crates/wasmtime/src/runtime/ref/gc_ref.rs b/crates/wasmtime/src/runtime/ref/gc_ref.rs
new file mode 100644
index 000000000..50cad8b67
--- /dev/null
+++ b/crates/wasmtime/src/runtime/ref/gc_ref.rs
@@ -0,0 +1,110 @@
+use crate::AsContextMut;
+use std::any::Any;
+use std::ffi::c_void;
+use wasmtime_runtime::VMExternRef;
+
+/// Represents an opaque reference to any data within WebAssembly.
+#[derive(Clone, Debug)]
+#[repr(transparent)]
+pub struct ExternRef {
+    inner: VMExternRef,
+}
+
+impl ExternRef {
+    /// Creates a new instance of `ExternRef` wrapping the given value.
+    pub fn new<T>(value: T) -> ExternRef
+    where
+        T: 'static + Any + Send + Sync,
+    {
+        let inner = VMExternRef::new(value);
+        ExternRef { inner }
+    }
+
+    pub(crate) fn from_vm_extern_ref(inner: VMExternRef) -> Self {
+        ExternRef { inner }
+    }
+
+    pub(crate) fn into_vm_extern_ref(self) -> VMExternRef {
+        self.inner
+    }
+
+    /// Get the underlying data for this `ExternRef`.
+    pub fn data(&self) -> &dyn Any {
+        &*self.inner
+    }
+
+    /// Get the strong reference count for this `ExternRef`.
+    ///
+    /// Note that this loads the reference count with a `SeqCst` ordering to
+    /// synchronize with other threads.
+    pub fn strong_count(&self) -> usize {
+        self.inner.strong_count()
+    }
+
+    /// Does this `ExternRef` point to the same inner value as `other`?
+    ///
+    /// This is *only* pointer equality, and does *not* run any inner value's
+    /// `Eq` implementation.
+    pub fn ptr_eq(&self, other: &ExternRef) -> bool {
+        VMExternRef::eq(&self.inner, &other.inner)
+    }
+
+    /// Creates a new strongly-owned [`ExternRef`] from the raw value provided.
+    ///
+    /// This is intended to be used in conjunction with [`Func::new_unchecked`],
+    /// [`Func::call_unchecked`], and [`ValRaw`] with its `externref` field.
+    ///
+    /// This function assumes that `raw` is an externref value which is
+    /// currently rooted within the [`Store`].
+    ///
+    /// # Unsafety
+    ///
+    /// This function is particularly `unsafe` because `raw` not only must be a
+    /// valid externref value produced prior by `to_raw` but it must also be
+    /// correctly rooted within the store. When arguments are provided to a
+    /// callback with [`Func::new_unchecked`], for example, or returned via
+    /// [`Func::call_unchecked`], if a GC is performed within the store then
+    /// floating externref values are not rooted and will be GC'd, meaning that
+    /// this function will no longer be safe to call with the values cleaned up.
+    /// This function must be invoked *before* possible GC operations can happen
+    /// (such as calling wasm).
+    ///
+    /// When in doubt try to not use this. Instead use the safe Rust APIs of
+    /// [`TypedFunc`] and friends.
+    ///
+    /// [`Func::call_unchecked`]: crate::Func::call_unchecked
+    /// [`Func::new_unchecked`]: crate::Func::new_unchecked
+    /// [`Store`]: crate::Store
+    /// [`TypedFunc`]: crate::TypedFunc
+    /// [`ValRaw`]: crate::ValRaw
+    pub unsafe fn from_raw(raw: *mut c_void) -> Option<ExternRef> {
+        let raw = raw.cast::<u8>();
+        let inner = VMExternRef::clone_from_raw(raw)?;
+        Some(ExternRef { inner })
+    }
+
+    /// Converts this [`ExternRef`] to a raw value suitable to store within a
+    /// [`ValRaw`].
+    ///
+    /// # Unsafety
+    ///
+    /// Produces a raw value which is only safe to pass into a store if a GC
+    /// doesn't happen between when the value is produce and when it's passed
+    /// into the store.
+    ///
+    /// [`ValRaw`]: crate::ValRaw
+    pub unsafe fn to_raw(&self, mut store: impl AsContextMut) -> *mut c_void {
+        let externref_ptr = self.inner.as_raw();
+        store
+            .as_context_mut()
+            .0
+            .insert_vmexternref_without_gc(self.inner.clone());
+        externref_ptr.cast()
+    }
+}
+
+impl std::fmt::Pointer for ExternRef {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        std::fmt::Pointer::fmt(&self.inner, f)
+    }
+}
diff --git a/crates/wasmtime/src/runtime/ref/no_gc_ref.rs b/crates/wasmtime/src/runtime/ref/no_gc_ref.rs
new file mode 100644
index 000000000..b83e08dd5
--- /dev/null
+++ b/crates/wasmtime/src/runtime/ref/no_gc_ref.rs
@@ -0,0 +1,60 @@
+//! The dummy `ExternRef` type used when the `gc` cargo feature is disabled.
+//!
+//! Providing a dummy type means that downstream users need to do fewer
+//! `#[cfg(...)]`s versus if this type or its methods simply didn't exist. The
+//! only methods that are left missing are constructors.
+
+#![allow(missing_docs)]
+
+use crate::runtime::Uninhabited;
+use crate::AsContextMut;
+use std::any::Any;
+use std::ffi::c_void;
+use wasmtime_runtime::VMExternRef;
+
+/// Represents an opaque reference to any data within WebAssembly.
+///
+/// Due to compilation configuration, this is an uninhabited type: enable the
+/// `gc` cargo feature to properly use this type.
+#[derive(Clone, Debug)]
+pub struct ExternRef {
+    _inner: Uninhabited,
+}
+
+impl ExternRef {
+    pub(crate) fn from_vm_extern_ref(inner: VMExternRef) -> Self {
+        inner.assert_unreachable()
+    }
+
+    pub(crate) fn into_vm_extern_ref(self) -> VMExternRef {
+        match self._inner {}
+    }
+
+    pub fn data(&self) -> &dyn Any {
+        match self._inner {}
+    }
+
+    pub fn strong_count(&self) -> usize {
+        match self._inner {}
+    }
+
+    pub fn ptr_eq(&self, _other: &ExternRef) -> bool {
+        match self._inner {}
+    }
+
+    pub unsafe fn from_raw(raw: *mut c_void) -> Option<ExternRef> {
+        assert!(raw.is_null());
+        None
+    }
+
+    pub unsafe fn to_raw(&self, mut store: impl AsContextMut) -> *mut c_void {
+        let _ = &mut store;
+        match self._inner {}
+    }
+}
+
+impl std::fmt::Pointer for ExternRef {
+    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self._inner {}
+    }
+}
diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs
index 40df11637..ca0fc73c1 100644
--- a/crates/wasmtime/src/runtime/store.rs
+++ b/crates/wasmtime/src/runtime/store.rs
@@ -97,9 +97,9 @@ use std::sync::Arc;
 use std::task::{Context, Poll};
 use wasmtime_runtime::mpk::{self, ProtectionKey, ProtectionMask};
 use wasmtime_runtime::{
-    ExportGlobal, InstanceAllocationRequest, InstanceAllocator, InstanceHandle, ModuleInfo,
-    OnDemandInstanceAllocator, SignalHandler, StoreBox, StorePtr, VMContext, VMExternRef,
-    VMExternRefActivationsTable, VMFuncRef, VMRuntimeLimits, WasmFault,
+    ExportGlobal, InstanceAllocationRequest, InstanceAllocator, InstanceHandle,
+    OnDemandInstanceAllocator, SignalHandler, StoreBox, StorePtr, VMContext, VMFuncRef,
+    VMRuntimeLimits, WasmFault,
 };
 
 mod context;
@@ -307,7 +307,7 @@ pub struct StoreOpaque {
     #[cfg(feature = "component-model")]
     num_component_instances: usize,
     signal_handler: Option<Box<SignalHandler<'static>>>,
-    externref_activations_table: VMExternRefActivationsTable,
+    externref_activations_table: wasmtime_runtime::VMExternRefActivationsTable,
     modules: ModuleRegistry,
     func_refs: FuncRefs,
     host_globals: Vec<StoreBox<VMHostGlobalContext>>,
@@ -392,7 +392,7 @@ pub(crate) struct AutoAssertNoGc<T>
 where
     T: std::ops::DerefMut<Target = StoreOpaque>,
 {
-    #[cfg(debug_assertions)]
+    #[cfg(all(debug_assertions, feature = "gc"))]
     prev_okay: bool,
     store: T,
 }
@@ -404,12 +404,12 @@ where
     #[inline]
     pub fn new(mut store: T) -> Self {
         let _ = &mut store;
-        #[cfg(debug_assertions)]
+        #[cfg(all(debug_assertions, feature = "gc"))]
         {
             let prev_okay = store.externref_activations_table.set_gc_okay(false);
             return AutoAssertNoGc { store, prev_okay };
         }
-        #[cfg(not(debug_assertions))]
+        #[cfg(not(all(debug_assertions, feature = "gc")))]
         {
             return AutoAssertNoGc { store };
         }
@@ -441,7 +441,7 @@ where
     T: std::ops::DerefMut<Target = StoreOpaque>,
 {
     fn drop(&mut self) {
-        #[cfg(debug_assertions)]
+        #[cfg(all(debug_assertions, feature = "gc"))]
         {
             self.store
                 .externref_activations_table
@@ -497,7 +497,7 @@ impl<T> Store<T> {
                 #[cfg(feature = "component-model")]
                 num_component_instances: 0,
                 signal_handler: None,
-                externref_activations_table: VMExternRefActivationsTable::new(),
+                externref_activations_table: wasmtime_runtime::VMExternRefActivationsTable::new(),
                 modules: ModuleRegistry::default(),
                 func_refs: FuncRefs::default(),
                 host_globals: Vec::new(),
@@ -794,6 +794,9 @@ impl<T> Store<T> {
     /// Note that it is not required to actively call this function. GC will
     /// automatically happen when internal buffers fill up. This is provided if
     /// fine-grained control over the GC is desired.
+    ///
+    /// This method is only available when the `gc` Cargo feature is enabled.
+    #[cfg(feature = "gc")]
     pub fn gc(&mut self) {
         self.inner.gc()
     }
@@ -1036,6 +1039,9 @@ impl<'a, T> StoreContextMut<'a, T> {
     /// Perform garbage collection of `ExternRef`s.
     ///
     /// Same as [`Store::gc`].
+    ///
+    /// This method is only available when the `gc` Cargo feature is enabled.
+    #[cfg(feature = "gc")]
     pub fn gc(&mut self) {
         self.0.gc()
     }
@@ -1374,7 +1380,9 @@ impl StoreOpaque {
     }
 
     #[inline]
-    pub fn externref_activations_table(&mut self) -> &mut VMExternRefActivationsTable {
+    pub fn externref_activations_table(
+        &mut self,
+    ) -> &mut wasmtime_runtime::VMExternRefActivationsTable {
         &mut self.externref_activations_table
     }
 
@@ -1523,7 +1531,7 @@ impl StoreOpaque {
         &self.runtime_limits as *const VMRuntimeLimits as *mut VMRuntimeLimits
     }
 
-    pub unsafe fn insert_vmexternref_without_gc(&mut self, r: VMExternRef) {
+    pub unsafe fn insert_vmexternref_without_gc(&mut self, r: wasmtime_runtime::VMExternRef) {
         self.externref_activations_table.insert_without_gc(r);
     }
 
@@ -2042,7 +2050,7 @@ unsafe impl<T> wasmtime_runtime::Store for StoreInner<T> {
     fn externref_activations_table(
         &mut self,
     ) -> (
-        &mut VMExternRefActivationsTable,
+        &mut wasmtime_runtime::VMExternRefActivationsTable,
         &dyn wasmtime_runtime::ModuleInfoLookup,
     ) {
         let inner = &mut self.inner;
@@ -2299,7 +2307,7 @@ impl Drop for StoreOpaque {
 }
 
 impl wasmtime_runtime::ModuleInfoLookup for ModuleRegistry {
-    fn lookup(&self, pc: usize) -> Option<&dyn ModuleInfo> {
+    fn lookup(&self, pc: usize) -> Option<&dyn wasmtime_runtime::ModuleInfo> {
         self.lookup_module_info(pc)
     }
 }
diff --git a/crates/wasmtime/src/runtime/trampoline/global.rs b/crates/wasmtime/src/runtime/trampoline/global.rs
index b44c714cc..89dc230a2 100644
--- a/crates/wasmtime/src/runtime/trampoline/global.rs
+++ b/crates/wasmtime/src/runtime/trampoline/global.rs
@@ -20,10 +20,10 @@ impl Drop for VMHostGlobalContext {
                 // Nothing to drop.
             }
             crate::ValType::Ref(r) => match r.heap_type() {
-                HeapType::Extern => unsafe { ptr::drop_in_place(self.global.as_externref_mut()) },
                 HeapType::Func | HeapType::Concrete(_) | HeapType::NoFunc => {
                     // Nothing to drop.
                 }
+                HeapType::Extern => unsafe { ptr::drop_in_place(self.global.as_externref_mut()) },
             },
         }
     }
@@ -59,7 +59,7 @@ pub fn generate_global_export(
                     f.map_or(ptr::null_mut(), |f| f.vm_func_ref(store).as_ptr());
             }
             Val::ExternRef(x) => {
-                *global.as_externref_mut() = x.map(|x| x.inner);
+                *global.as_externref_mut() = x.map(|x| x.into_vm_extern_ref());
             }
         }
         global
diff --git a/crates/wasmtime/src/runtime/type_registry.rs b/crates/wasmtime/src/runtime/type_registry.rs
index 912e239de..de7a64311 100644
--- a/crates/wasmtime/src/runtime/type_registry.rs
+++ b/crates/wasmtime/src/runtime/type_registry.rs
@@ -23,6 +23,7 @@ use wasmtime_environ::{
     WasmFuncType,
 };
 use wasmtime_runtime::VMSharedTypeIndex;
+use wasmtime_slab::{Id as SlabId, Slab};
 
 // ### Notes on the Lifetime Management of Types
 //
@@ -154,8 +155,13 @@ impl Drop for TypeCollection {
 }
 
 #[inline]
-fn entry_index(index: VMSharedTypeIndex) -> usize {
-    usize::try_from(index.bits()).unwrap()
+fn shared_type_index_to_slab_id(index: VMSharedTypeIndex) -> SlabId {
+    SlabId::from_raw(index.bits())
+}
+
+#[inline]
+fn slab_id_to_shared_type_index(id: SlabId) -> VMSharedTypeIndex {
+    VMSharedTypeIndex::new(id.into_raw())
 }
 
 /// A Wasm type that has been registered in the engine's `TypeRegistry`.
@@ -168,7 +174,7 @@ fn entry_index(index: VMSharedTypeIndex) -> usize {
 /// Dereferences to its underlying `WasmFuncType`.
 pub struct RegisteredType {
     engine: Engine,
-    entry: OccupiedEntry,
+    entry: Entry,
 }
 
 impl Debug for RegisteredType {
@@ -271,9 +277,9 @@ impl RegisteredType {
     /// registry.
     pub fn root(engine: &Engine, index: VMSharedTypeIndex) -> Option<RegisteredType> {
         let entry = {
-            let i = entry_index(index);
+            let id = shared_type_index_to_slab_id(index);
             let inner = engine.signatures().0.read().unwrap();
-            let e = inner.entries.get(i)?.as_occupied()?;
+            let e = inner.entries.get(id)?;
 
             // NB: make sure to incref while the lock is held to prevent:
             //
@@ -293,7 +299,7 @@ impl RegisteredType {
     ///
     /// It is the caller's responsibility to ensure that the entry's reference
     /// count has already been incremented.
-    fn from_parts(engine: Engine, entry: OccupiedEntry) -> Self {
+    fn from_parts(engine: Engine, entry: Entry) -> Self {
         debug_assert!(entry.0.registrations.load(Acquire) != 0);
         RegisteredType { engine, entry }
     }
@@ -311,7 +317,7 @@ impl RegisteredType {
 
 /// A Wasm function type, its `VMSharedTypeIndex`, and its registration count.
 #[derive(Debug)]
-struct OccupiedEntryInner {
+struct EntryInner {
     ty: WasmFuncType,
     index: VMSharedTypeIndex,
     registrations: AtomicUsize,
@@ -321,9 +327,9 @@ struct OccupiedEntryInner {
 /// function type, so that this can be a hash consing key in
 /// `TypeRegistryInner::map`.
 #[derive(Clone, Debug)]
-struct OccupiedEntry(Arc<OccupiedEntryInner>);
+struct Entry(Arc<EntryInner>);
 
-impl Deref for OccupiedEntry {
+impl Deref for Entry {
     type Target = WasmFuncType;
 
     fn deref(&self) -> &Self::Target {
@@ -331,27 +337,27 @@ impl Deref for OccupiedEntry {
     }
 }
 
-impl PartialEq for OccupiedEntry {
+impl PartialEq for Entry {
     fn eq(&self, other: &Self) -> bool {
         self.0.ty == other.0.ty
     }
 }
 
-impl Eq for OccupiedEntry {}
+impl Eq for Entry {}
 
-impl Hash for OccupiedEntry {
+impl Hash for Entry {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.0.ty.hash(state);
     }
 }
 
-impl Borrow<WasmFuncType> for OccupiedEntry {
+impl Borrow<WasmFuncType> for Entry {
     fn borrow(&self) -> &WasmFuncType {
         &self.0.ty
     }
 }
 
-impl OccupiedEntry {
+impl Entry {
     /// Increment the registration count.
     fn incref(&self, why: &str) {
         let old_count = self.0.registrations.fetch_add(1, AcqRel);
@@ -377,69 +383,15 @@ impl OccupiedEntry {
     }
 }
 
-#[derive(Debug)]
-enum RegistryEntry {
-    /// An occupied entry containing a registered type.
-    Occupied(OccupiedEntry),
-
-    /// A vacant entry that is additionally a link in the free list of all
-    /// vacant entries.
-    Vacant {
-        /// The next link in the free list of all vacant entries, if any.
-        next_vacant: Option<VMSharedTypeIndex>,
-    },
-}
-
-impl RegistryEntry {
-    fn is_vacant(&self) -> bool {
-        matches!(self, Self::Vacant { .. })
-    }
-
-    fn is_occupied(&self) -> bool {
-        matches!(self, Self::Occupied(_))
-    }
-
-    fn as_occupied(&self) -> Option<&OccupiedEntry> {
-        match self {
-            Self::Occupied(o) => Some(o),
-            Self::Vacant { .. } => None,
-        }
-    }
-
-    fn unwrap_occupied(&self) -> &OccupiedEntry {
-        match self {
-            Self::Occupied(o) => o,
-            Self::Vacant { .. } => panic!("unwrap_occupied on vacant entry"),
-        }
-    }
-
-    fn unwrap_next_vacant(&self) -> Option<VMSharedTypeIndex> {
-        match self {
-            Self::Vacant { next_vacant } => *next_vacant,
-            Self::Occupied(_) => panic!("unwrap_next_vacant on occupied entry"),
-        }
-    }
-}
-
 #[derive(Debug, Default)]
 struct TypeRegistryInner {
     // A map from the Wasm function type to a `VMSharedTypeIndex`, for all
     // the Wasm function types we have already registered.
-    map: HashSet<OccupiedEntry>,
+    map: HashSet<Entry>,
 
     // A map from `VMSharedTypeIndex::bits()` to the type index's associated
     // Wasm type.
-    entries: Vec<RegistryEntry>,
-
-    // The head of the free list of the entries that are vacant and can
-    // therefore (along with their associated `VMSharedTypeIndex`) be reused.
-    //
-    // This is a size optimization, and arguably not strictly necessary for
-    // correctness, but is necessary to avoid unbounded memory growth: if we did
-    // not reuse entries/indices, we would have holes in our `self.entries` list
-    // and, as we load and unload new Wasm modules, `self.entries` would keep
-    // growing indefinitely.
-    first_vacant: Option<VMSharedTypeIndex>,
+    entries: Slab<Entry>,
 
     // An explicit stack of entries that we are in the middle of dropping. Used
     // to avoid recursion when dropping a type that is holding the last
@@ -470,9 +422,9 @@ impl TypeRegistryInner {
             EngineOrModuleTypeIndex::Module(_) => Err(()),
             EngineOrModuleTypeIndex::Engine(id) => {
                 let id = VMSharedTypeIndex::new(id);
-                let i = entry_index(id);
+                let id = shared_type_index_to_slab_id(id);
                 assert!(
-                    self.entries[i].is_occupied(),
+                    self.entries.contains(id),
                     "canonicalized in a different engine? {ty:?}"
                 );
                 Ok(())
@@ -507,42 +459,6 @@ impl TypeRegistryInner {
         debug_assert!(self.is_canonicalized(ty))
     }
 
-    /// Allocate a vacant entry, either from the free list, or creating a new
-    /// entry.
-    fn alloc_vacant_entry(&mut self) -> VMSharedTypeIndex {
-        match self.first_vacant.take() {
-            // Pop a vacant entry off the free list when we can.
-            Some(index) => {
-                let i = entry_index(index);
-                let entry = &mut self.entries[i];
-                self.first_vacant = entry.unwrap_next_vacant();
-                index
-            }
-
-            // Otherwise, allocate a new entry.
-            None => {
-                debug_assert_eq!(self.entries.len(), self.map.len());
-
-                let len = self.entries.len();
-                let len = u32::try_from(len).unwrap();
-
-                // Keep `index_map`'s length under `u32::MAX` because
-                // `u32::MAX` is reserved for `VMSharedTypeIndex`'s
-                // default value.
-                assert!(
-                    len < std::u32::MAX,
-                    "Invariant check: self.entries.len() < std::u32::MAX"
-                );
-
-                let index = VMSharedTypeIndex::new(len);
-                self.entries
-                    .push(RegistryEntry::Vacant { next_vacant: None });
-
-                index
-            }
-        }
-    }
-
     /// Add a new type to this registry.
     ///
     /// The type must be canonicalized and must not already exist in the
@@ -550,7 +466,7 @@ impl TypeRegistryInner {
     ///
     /// Initializes the new entry's registration count to one, and callers
     /// should not further increment the registration count.
-    fn register_new(&mut self, ty: WasmFuncType) -> OccupiedEntry {
+    fn register_new(&mut self, ty: WasmFuncType) -> Entry {
         assert!(
             self.is_canonicalized(&ty),
             "ty is not already canonicalized: {ty:?}"
@@ -562,8 +478,8 @@ impl TypeRegistryInner {
         ty.trace::<_, ()>(&mut |idx| match idx {
             EngineOrModuleTypeIndex::Engine(id) => {
                 let id = VMSharedTypeIndex::new(id);
-                let i = entry_index(id);
-                let e = self.entries[i].unwrap_occupied();
+                let i = shared_type_index_to_slab_id(id);
+                let e = &self.entries[i];
                 e.incref("new type references existing type in TypeRegistryInner::register_new");
                 Ok(())
             }
@@ -571,9 +487,10 @@ impl TypeRegistryInner {
         })
         .unwrap();
 
-        let index = self.alloc_vacant_entry();
+        let id = self.entries.next_id();
+        let index = slab_id_to_shared_type_index(id);
         log::trace!("create {index:?} = {ty:?} (registrations -> 1)");
-        let entry = OccupiedEntry(Arc::new(OccupiedEntryInner {
+        let entry = Entry(Arc::new(EntryInner {
             ty,
             index,
             registrations: AtomicUsize::new(1),
@@ -581,15 +498,14 @@ impl TypeRegistryInner {
         let is_new_entry = self.map.insert(entry.clone());
         assert!(is_new_entry);
 
-        let i = entry_index(index);
-        assert!(self.entries[i].is_vacant());
-        self.entries[i] = RegistryEntry::Occupied(entry.clone());
+        let id = self.entries.alloc(entry.clone());
+        assert_eq!(id, shared_type_index_to_slab_id(index));
 
         entry
     }
 
     /// Register the given canonicalized type, incrementing its reference count.
-    fn register_canonicalized(&mut self, ty: WasmFuncType) -> OccupiedEntry {
+    fn register_canonicalized(&mut self, ty: WasmFuncType) -> Entry {
         assert!(
             self.is_canonicalized(&ty),
             "type is not already canonicalized: {ty:?}"
@@ -607,8 +523,8 @@ impl TypeRegistryInner {
 
     fn unregister_type_collection(&mut self, collection: &TypeCollection) {
         for (_, id) in collection.types.iter() {
-            let i = entry_index(*id);
-            let e = self.entries[i].unwrap_occupied();
+            let i = shared_type_index_to_slab_id(*id);
+            let e = &self.entries[i];
             if e.decref("TypeRegistryInner::unregister_type_collection") {
                 self.unregister_entry(*id);
             }
@@ -626,9 +542,9 @@ impl TypeRegistryInner {
         debug_assert!(self.drop_stack.is_empty());
         self.drop_stack.push(index);
 
-        while let Some(id) = self.drop_stack.pop() {
-            let i = entry_index(id);
-            let entry = self.entries[i].unwrap_occupied();
+        while let Some(index) = self.drop_stack.pop() {
+            let slab_id = shared_type_index_to_slab_id(index);
+            let entry = &self.entries[slab_id];
 
             // We need to double check whether the entry is still at zero
             // registrations: Between the time that we observed a zero and
@@ -643,7 +559,7 @@ impl TypeRegistryInner {
             let registrations = entry.0.registrations.load(Acquire);
             if registrations != 0 {
                 log::trace!(
-                    "{id:?} was concurrently resurrected and no longer has zero \
+                    "{index:?} was concurrently resurrected and no longer has zero \
                      registrations (registrations -> {registrations})"
                 );
                 continue;
@@ -654,15 +570,15 @@ impl TypeRegistryInner {
             entry
                 .0
                 .ty
-                .trace::<_, ()>(&mut |idx| match idx {
-                    EngineOrModuleTypeIndex::Engine(child_id) => {
-                        let child_id = VMSharedTypeIndex::new(child_id);
-                        let child_index = entry_index(child_id);
-                        let child_entry = self.entries[child_index].unwrap_occupied();
+                .trace::<_, ()>(&mut |child_index| match child_index {
+                    EngineOrModuleTypeIndex::Engine(child_index) => {
+                        let child_index = VMSharedTypeIndex::new(child_index);
+                        let child_slab_id = shared_type_index_to_slab_id(child_index);
+                        let child_entry = &self.entries[child_slab_id];
                         if child_entry.decref(
                             "referenced by unregistered type in TypeCollection::unregister_entry",
                         ) {
-                            self.drop_stack.push(child_id);
+                            self.drop_stack.push(child_index);
                         }
                         Ok(())
                     }
@@ -672,12 +588,9 @@ impl TypeRegistryInner {
                 })
                 .unwrap();
 
-            log::trace!("removing {id:?} from registry");
+            log::trace!("removing {index:?} from registry");
             self.map.remove(entry);
-            self.entries[i] = RegistryEntry::Vacant {
-                next_vacant: self.first_vacant.take(),
-            };
-            self.first_vacant = Some(id);
+            self.entries.dealloc(slab_id);
         }
     }
 }
@@ -692,7 +605,7 @@ impl Drop for TypeRegistryInner {
             "type registry not empty: still have registered types in self.map"
         );
         assert!(
-            self.entries.iter().all(|e| e.is_vacant()),
+            self.entries.is_empty(),
             "type registry not empty: not all entries are vacant"
         );
     }
@@ -720,9 +633,8 @@ impl TypeRegistry {
     /// constructor if you need to ensure that property and you don't have some
     /// other mechanism already keeping the type registered.
     pub fn borrow(&self, index: VMSharedTypeIndex) -> Option<impl Deref<Target = WasmFuncType>> {
-        let i = entry_index(index);
+        let id = shared_type_index_to_slab_id(index);
         let inner = self.0.read().unwrap();
-        let e = inner.entries.get(i)?;
-        e.as_occupied().cloned()
+        inner.entries.get(id).cloned()
     }
 }
diff --git a/crates/wasmtime/src/runtime/uninhabited.rs b/crates/wasmtime/src/runtime/uninhabited.rs
new file mode 100644
index 000000000..6b368c35c
--- /dev/null
+++ b/crates/wasmtime/src/runtime/uninhabited.rs
@@ -0,0 +1,5 @@
+/// An uninhabited type.
+///
+/// Useful for modeling Wasm's bottom types or `cfg`'d off features.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub(crate) enum Uninhabited {}
diff --git a/crates/wasmtime/src/runtime/values.rs b/crates/wasmtime/src/runtime/values.rs
index 9ae98876d..8bb41362b 100644
--- a/crates/wasmtime/src/runtime/values.rs
+++ b/crates/wasmtime/src/runtime/values.rs
@@ -178,8 +178,8 @@ impl Val {
             Val::V128(b) => ValRaw::v128(b.as_u128()),
             Val::ExternRef(e) => {
                 let externref = match e {
-                    Some(e) => e.to_raw(store),
                     None => ptr::null_mut(),
+                    Some(e) => e.to_raw(store),
                 };
                 ValRaw::externref(externref)
             }
@@ -209,11 +209,11 @@ impl Val {
             ValType::V128 => Val::V128(raw.get_v128().into()),
             ValType::Ref(ref_ty) => {
                 let ref_ = match ref_ty.heap_type() {
-                    HeapType::Extern => ExternRef::from_raw(raw.get_externref()).into(),
                     HeapType::Func | HeapType::Concrete(_) => {
                         Func::from_raw(store, raw.get_funcref()).into()
                     }
                     HeapType::NoFunc => Ref::Func(None),
+                    HeapType::Extern => ExternRef::from_raw(raw.get_externref()).into(),
                 };
                 assert!(
                     ref_ty.is_nullable() || !ref_.is_null(),
@@ -689,13 +689,15 @@ impl Ref {
                 );
                 Ok(TableElement::FuncRef(f.vm_func_ref(store).as_ptr()))
             }
+
             (Ref::Extern(e), HeapType::Extern) => match e {
                 None => {
                     assert!(ty.is_nullable());
                     Ok(TableElement::ExternRef(None))
                 }
-                Some(e) => Ok(TableElement::ExternRef(Some(e.inner))),
+                Some(e) => Ok(TableElement::ExternRef(Some(e.into_vm_extern_ref()))),
             },
+
             _ => unreachable!("checked that the value matches the type above"),
         }
     }
diff --git a/crates/wast/Cargo.toml b/crates/wast/Cargo.toml
index b326466d6..a752481c0 100644
--- a/crates/wast/Cargo.toml
+++ b/crates/wast/Cargo.toml
@@ -14,7 +14,7 @@ workspace = true
 
 [dependencies]
 anyhow = { workspace = true }
-wasmtime = { workspace = true, features = ['cranelift', 'wat', 'runtime'] }
+wasmtime = { workspace = true, features = ['cranelift', 'wat', 'runtime', 'gc'] }
 wast = { workspace = true }
 log = { workspace = true }
 
diff --git a/scripts/publish.rs b/scripts/publish.rs
index 7cb67613e..7e5d339c1 100644
--- a/scripts/publish.rs
+++ b/scripts/publish.rs
@@ -46,6 +46,7 @@ const CRATES_TO_PUBLISH: &[&str] = &[
     // wasmtime
     "wasmtime-asm-macros",
     "wasmtime-versioned-export-macros",
+    "wasmtime-slab",
     "wasmtime-component-util",
     "wasmtime-wit-bindgen",
     "wasmtime-component-macro",
diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml
index 430f8c31e..505b56aa4 100644
--- a/supply-chain/audits.toml
+++ b/supply-chain/audits.toml
@@ -3690,6 +3690,30 @@ user-id = 6825 # Dan Gohman (sunfishcode)
 start = "2019-08-20"
 end = "2024-07-14"
 
+[[trusted.wit-bindgen]]
+criteria = "safe-to-deploy"
+user-id = 73222 # wasmtime-publish
+start = "2024-02-23"
+end = "2025-02-23"
+
+[[trusted.wit-bindgen-core]]
+criteria = "safe-to-deploy"
+user-id = 73222 # wasmtime-publish
+start = "2024-02-23"
+end = "2025-02-23"
+
+[[trusted.wit-bindgen-rust]]
+criteria = "safe-to-deploy"
+user-id = 73222 # wasmtime-publish
+start = "2024-02-23"
+end = "2025-02-23"
+
+[[trusted.wit-bindgen-rust-macro]]
+criteria = "safe-to-deploy"
+user-id = 73222 # wasmtime-publish
+start = "2024-02-23"
+end = "2025-02-23"
+
 [[trusted.wit-component]]
 criteria = "safe-to-deploy"
 user-id = 73222 # wasmtime-publish
diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock
index c7d51588e..5ad0c81a8 100644
--- a/supply-chain/imports.lock
+++ b/supply-chain/imports.lock
@@ -1275,6 +1275,12 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wit-bindgen]]
+version = "0.19.1"
+when = "2024-02-23"
+user-id = 73222
+user-login = "wasmtime-publish"
+
 [[publisher.wit-bindgen-core]]
 version = "0.17.0"
 when = "2024-02-05"
@@ -1282,6 +1288,12 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wit-bindgen-core]]
+version = "0.19.1"
+when = "2024-02-23"
+user-id = 73222
+user-login = "wasmtime-publish"
+
 [[publisher.wit-bindgen-rust]]
 version = "0.17.0"
 when = "2024-02-05"
@@ -1289,6 +1301,12 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wit-bindgen-rust]]
+version = "0.19.1"
+when = "2024-02-23"
+user-id = 73222
+user-login = "wasmtime-publish"
+
 [[publisher.wit-bindgen-rust-macro]]
 version = "0.17.0"
 when = "2024-02-05"
@@ -1296,6 +1314,12 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wit-bindgen-rust-macro]]
+version = "0.19.1"
+when = "2024-02-23"
+user-id = 73222
+user-login = "wasmtime-publish"
+
 [[publisher.wit-component]]
 version = "0.20.1"
 when = "2024-01-29"
diff --git a/winch/codegen/src/codegen/builtin.rs b/winch/codegen/src/codegen/builtin.rs
index 0294c2ffe..5c508daa2 100644
--- a/winch/codegen/src/codegen/builtin.rs
+++ b/winch/codegen/src/codegen/builtin.rs
@@ -93,6 +93,7 @@ macro_rules! declare_function_sig {
             /// F64 Nearest.
             nearest_f64: Option<BuiltinFunction>,
             $(
+                $( #[ $attr ] )*
                 $name: Option<BuiltinFunction>,
             )*
         }
@@ -102,6 +103,7 @@ macro_rules! declare_function_sig {
         impl BuiltinFunctions {
             pub fn new<P: PtrSize>(vmoffsets: &VMOffsets<P>, call_conv: CallingConvention) -> Self {
                 let size = vmoffsets.ptr.size();
+                #[allow(unused_doc_comments)]
                 Self {
                     ptr_size: size,
                     call_conv,
@@ -116,6 +118,7 @@ macro_rules! declare_function_sig {
                     nearest_f32: None,
                     nearest_f64: None,
                     $(
+                        $( #[ $attr ] )*
                         $name: None,
                     )*
                 }
@@ -246,6 +249,7 @@ macro_rules! declare_function_sig {
             }
 
             $(
+                $( #[ $attr ] )*
                 pub(crate) fn $name<A: ABI, P: PtrSize>(&mut self) -> BuiltinFunction {
                     if self.$name.is_none() {
                         let params = vec![ $(self.$param() ),* ];


Test patch:
diff --git a/docs/contributing-testing.md b/docs/contributing-testing.md
index 188a0d06a..acb8841d9 100644
--- a/docs/contributing-testing.md
+++ b/docs/contributing-testing.md
@@ -6,26 +6,23 @@ Before continuing, make sure you can [build
 Wasmtime](./contributing-building.md) successfully. Can't run the tests if you
 can't build it!
 
-## Running All Tests
+## Installing `wasm32` Targets
 
-To run all of Wasmtime's tests (excluding WASI integration tests), execute this command:
+To compile the tests, you'll need the `wasm32-wasi` and
+`wasm32-unknown-unknown` targets installed, which, assuming you're using
+[rustup.rs](https://rustup.rs) to manage your Rust versions, can be done as
+follows:
 
 ```shell
-cargo test --all
+rustup target add wasm32-wasi wasm32-unknown-unknown
 ```
 
-To include WASI integration tests, you'll need `wasm32-wasi` target installed, which,
-assuming you're using [rustup.rs](https://rustup.rs) to manage your Rust versions,
-can be done as follows:
-
-```shell
-rustup target add wasm32-wasi
-```
+## Running All Tests
 
-Next, to run all tests including the WASI integration tests, execute this command:
+To run all of Wasmtime's tests, execute this command:
 
 ```shell
-cargo test --features test-programs/test_programs --all
+cargo test --workspace
 ```
 
 You can also exclude a particular crate from testing with `--exclude`. For
@@ -34,7 +31,13 @@ requires that `libclang` is installed on your system, and for some reason maybe
 you don't have it — you can run:
 
 ```shell
-cargo test --all --exclude wasmtime-fuzzing
+cargo test --workspace --exclude wasmtime-fuzzing
+```
+
+Similarly, to skip WASI integration tests, run:
+
+```shell
+cargo test --workspace --exclude test-programs
 ```
 
 ## Testing a Specific Crate
@@ -78,7 +81,7 @@ can be useful when working on the `wasi-common` crate. This can be done by
 executing this command:
 
 ```shell
-cargo test --features test-programs/test_programs -p test-programs
+cargo test -p test-programs
 ```
 
 ## Adding New Tests
diff --git a/winch/filetests/filetests/x64/table/fill.wat b/winch/filetests/filetests/x64/table/fill.wat
index d7cd14519..0ed7d6a0c 100644
--- a/winch/filetests/filetests/x64/table/fill.wat
+++ b/winch/filetests/filetests/x64/table/fill.wat
@@ -118,7 +118,7 @@
 ;;   c1:	 4883e0fe             	and	rax, 0xfffffffffffffffe
 ;;      	 4889442404           	mov	qword ptr [rsp + 4], rax
 ;;      	 4d8b5e38             	mov	r11, qword ptr [r14 + 0x38]
-;;      	 498b4368             	mov	rax, qword ptr [r11 + 0x68]
+;;      	 498b4358             	mov	rax, qword ptr [r11 + 0x58]
 ;;      	 448b5c2414           	mov	r11d, dword ptr [rsp + 0x14]
 ;;      	 4883ec04             	sub	rsp, 4
 ;;      	 44891c24             	mov	dword ptr [rsp], r11d