Repository: bytecodealliance/wasmtime
Pull request #7975 (2024-02-22 00:04:51)

Wasmtime: Add a `gc` cargo feature
This controls whether support for `ExternRef` and its associated deferred, reference-counting garbage collector is enabled at compile time or not. It will also be used for similarly for Wasmtime's full Wasm GC support as that gets added.

<!--
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
-->


fitzgen: I didn't add a smoke test build without the `gc` cargo feature to CI, but I can do that if we decide we can afford the CI time.
github-actions[bot]: #### Subscribe to Label Action

cc @peterhuene

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

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

* peterhuene: wasmtime:api

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

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>
github-actions[bot]: #### Label Messager: wasmtime:config

It looks like you are changing Wasmtime's configuration options. Make sure to
complete this check list:

* [ ] If you added a new `Config` method, you wrote extensive documentation for
      it.

  <details>

  Our documentation should be of the following form:

  ```text
  Short, simple summary sentence.

  More details. These details can be multiple paragraphs. There should be
  information about not just the method, but its parameters and results as
  well.

  Is this method fallible? If so, when can it return an error?

  Can this method panic? If so, when does it panic?

  # Example

  Optional example here.
  ```

  </details>

* [ ] If you added a new `Config` method, or modified an existing one, you
  ensured that this configuration is exercised by the fuzz targets.

  <details>

  For example, if you expose a new strategy for allocating the next instance
  slot inside the pooling allocator, you should ensure that at least one of our
  fuzz targets exercises that new strategy.

  Often, all that is required of you is to ensure that there is a knob for this
  configuration option in [`wasmtime_fuzzing::Config`][fuzzing-config] (or one
  of its nested `struct`s).

  Rarely, this may require authoring a new fuzz target to specifically test this
  configuration. See [our docs on fuzzing][fuzzing-docs] for more details.

  </details>

* [ ] If you are enabling a configuration option by default, make sure that it
  has been fuzzed for at least two weeks before turning it on by default.

[fuzzing-config]: https://github.com/bytecodealliance/wasmtime/blob/ca0e8d0a1d8cefc0496dba2f77a670571d8fdcab/crates/fuzzing/src/generators.rs#L182-L194
[fuzzing-docs]: https://docs.wasmtime.dev/contributing-fuzzing.html


--------------------------------------------------------------------------------

<details>

To modify this label's message, edit the <code>.github/label-messager/wasmtime-config.md</code> file.

To add new label messages or remove existing label messages, edit the
<code>.github/label-messager.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/label-messager-action)

</details>
fitzgen: Okay, I've updated this PR to stub out more `VMExternRef` things and methods when GC is disabled at compile time and removed a bunch of `cfg`s from the rest of the code base. Not all of them in `store.rs`, as mentioned inline above. I think this is ready for another round of review.
github-actions[bot]: #### Subscribe to Label Action

cc @fitzgen, @saulecabrera

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

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

* fitzgen: wasmtime:ref-types
* saulecabrera: winch

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

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>
github-actions[bot]: #### Subscribe to Label Action

cc @peterhuene

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

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

* peterhuene: wasmtime:c-api

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

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

Patch:
diff --git a/.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/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/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml
index 2b8ef78d5..d923863fa 100644
--- a/crates/wasmtime/Cargo.toml
+++ b/crates/wasmtime/Cargo.toml
@@ -86,6 +86,7 @@ wasi-common = { path = "../wasi-common", default-features = true }
 default = [
   'async',
   'cache',
+  'gc',
   'wat',
   'profiling',
   'parallel-compilation',
@@ -172,3 +173,19 @@ debug-builtins = ["wasmtime-runtime?/debug-builtins"]
 
 # Enable support for executing compiled Wasm modules.
 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/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/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/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