Repository: bytecodealliance/wasmtime
Pull request #8260 (2024-03-28 20:47:20)

Gate support for the wasm `threads` proposal behind a Cargo feature
This commit moves support for the `threads` proposal behind a new on-by-default Cargo feature: `threads`. This is intended to support building Wasmtime with fewer runtime dependencies such as those required for the atomic operations on memories.

This additionally adds the `gc` feature in a few missing places too.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->


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

cc @peterhuene

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

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

* peterhuene: wasmtime:api, 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>
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>

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2c75044a9..3ea4fbf8c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -300,6 +300,9 @@ jobs:
     - 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 -p wasmtime --no-default-features --features threads
+    - run: cargo check -p wasmtime --no-default-features --features runtime,threads
+    - run: cargo check -p wasmtime --no-default-features --features cranelift,threads
     - run: cargo check --features component-model
     - run: cargo check -p wasmtime --features incremental-cache
 
diff --git a/Cargo.toml b/Cargo.toml
index e18af8278..58a359d77 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -330,6 +330,8 @@ default = [
   "addr2line",
   "debug-builtins",
   "component-model",
+  "threads",
+  "gc",
 
   # Enable some nice features of clap by default, but they come at a binary size
   # cost, so allow disabling this through disabling of our own `default`
@@ -359,7 +361,7 @@ disable-logging = ["log/max_level_off", "tracing/max_level_off"]
 # These features are all included in the `default` set above and this is
 # the internal mapping for what they enable in Wasmtime itself.
 wasi-nn = ["dep:wasmtime-wasi-nn"]
-wasi-threads = ["dep:wasmtime-wasi-threads"]
+wasi-threads = ["dep:wasmtime-wasi-threads", "threads"]
 wasi-http = ["component-model", "dep:wasmtime-wasi-http", "dep:tokio", "dep:hyper"]
 pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"]
 component-model = [
@@ -377,6 +379,8 @@ profiling = ["wasmtime/profiling"]
 coredump = ["wasmtime-cli-flags/coredump"]
 addr2line = ["wasmtime/addr2line"]
 debug-builtins = ["wasmtime/debug-builtins"]
+threads = ["wasmtime-cli-flags/threads"]
+gc = ["wasmtime-cli-flags/gc"]
 
 # Enables compatibility shims with Wasmtime 13 and prior's CLI.
 old-cli = []
diff --git a/cranelift/codegen/src/egraph.rs b/cranelift/codegen/src/egraph.rs
index 73f98a9b9..1f3f2a882 100644
--- a/cranelift/codegen/src/egraph.rs
+++ b/cranelift/codegen/src/egraph.rs
@@ -566,6 +566,10 @@ impl<'a> EgraphPass<'a> {
     /// only refer to its subset that exists at this stage, to
     /// maintain acyclicity.)
     fn remove_pure_and_optimize(&mut self) {
+        // This pass relies on every value having a unique name, so first
+        // eliminate any value aliases.
+        self.func.dfg.resolve_all_aliases();
+
         let mut cursor = FuncCursor::new(self.func);
         let mut value_to_opt_value: SecondaryMap<Value, Value> =
             SecondaryMap::with_default(Value::reserved_value());
@@ -661,7 +665,6 @@ impl<'a> EgraphPass<'a> {
 
                         // Rewrite args of *all* instructions using the
                         // value-to-opt-value map.
-                        cursor.func.dfg.resolve_aliases_in_arguments(inst);
                         cursor.func.dfg.map_inst_values(inst, |arg| {
                             let new_value = value_to_opt_value[arg];
                             trace!("rewriting arg {} of inst {} to {}", arg, inst, new_value);
diff --git a/cranelift/codegen/src/egraph/elaborate.rs b/cranelift/codegen/src/egraph/elaborate.rs
index f7aa002b1..2aca8bf64 100644
--- a/cranelift/codegen/src/egraph/elaborate.rs
+++ b/cranelift/codegen/src/egraph/elaborate.rs
@@ -416,8 +416,7 @@ impl<'a> Elaborator<'a> {
         while let Some(entry) = self.elab_stack.pop() {
             match entry {
                 ElabStackEntry::Start { value, before } => {
-                    debug_assert_ne!(value, Value::reserved_value());
-                    let value = self.func.dfg.resolve_aliases(value);
+                    debug_assert!(self.func.dfg.value_is_real(value));
 
                     self.stats.elaborate_visit_node += 1;
 
diff --git a/cranelift/codegen/src/ir/dfg.rs b/cranelift/codegen/src/ir/dfg.rs
index 2ac1152ce..d65f15ed7 100644
--- a/cranelift/codegen/src/ir/dfg.rs
+++ b/cranelift/codegen/src/ir/dfg.rs
@@ -333,6 +333,13 @@ impl DataFlowGraph {
         self.values.is_valid(v)
     }
 
+    /// Check whether a value is valid and not an alias.
+    pub fn value_is_real(&self, value: Value) -> bool {
+        // Deleted or unused values are also stored as aliases so this excludes
+        // those as well.
+        self.value_is_valid(value) && !matches!(self.values[value].into(), ValueData::Alias { .. })
+    }
+
     /// Get the type of a value.
     pub fn value_type(&self, v: Value) -> Type {
         self.values[v].ty()
@@ -419,6 +426,8 @@ impl DataFlowGraph {
 
         // Now aliases don't point to other aliases, so we can replace any use
         // of an alias with the final value in constant time.
+
+        // Rewrite InstructionData in `self.insts`.
         for inst in self.insts.0.values_mut() {
             inst.map_values(&mut self.value_lists, &mut self.jump_tables, |arg| {
                 if let ValueData::Alias { original, .. } = self.values[arg].into() {
@@ -429,6 +438,50 @@ impl DataFlowGraph {
             });
         }
 
+        // - `results` and block-params in `blocks` are not aliases, by
+        //   definition.
+        // - `dynamic_types` has no values.
+        // - `value_lists` can only be accessed via references from elsewhere.
+        // - `values` only has value references in aliases (which we've
+        //   removed), and unions (but the egraph pass ensures there are no
+        //   aliases before creating unions).
+
+        // Merge `facts` from any alias onto the aliased value. Note that if
+        // there was a chain of aliases, at this point every alias that was in
+        // the chain points to the same final value, so their facts will all be
+        // merged together.
+        for value in self.facts.keys() {
+            if let ValueData::Alias { original, .. } = self.values[value].into() {
+                if let Some(new_fact) = self.facts[value].take() {
+                    match &mut self.facts[original] {
+                        Some(old_fact) => *old_fact = Fact::intersect(old_fact, &new_fact),
+                        old_fact => *old_fact = Some(new_fact),
+                    }
+                }
+            }
+        }
+
+        // - `signatures`, `old_signatures`, and `ext_funcs` have no values.
+
+        if let Some(values_labels) = &mut self.values_labels {
+            // Debug info is best-effort. If any is attached to value aliases,
+            // just discard it.
+            values_labels.retain(|&k, _| !matches!(self.values[k].into(), ValueData::Alias { .. }));
+
+            // If debug-info says a value should have the same labels as another
+            // value, then make sure that target is not a value alias.
+            for value_label in values_labels.values_mut() {
+                if let ValueLabelAssignments::Alias { value, .. } = value_label {
+                    if let ValueData::Alias { original, .. } = self.values[*value].into() {
+                        *value = original;
+                    }
+                }
+            }
+        }
+
+        // - `constants` and `immediates` have no values.
+        // - `jump_tables` is updated together with instruction-data above.
+
         // Delete all aliases now that there are no uses left.
         for value in self.values.values_mut() {
             if let ValueData::Alias { .. } = ValueData::from(*value) {
@@ -437,16 +490,6 @@ impl DataFlowGraph {
         }
     }
 
-    /// Resolve all aliases among inst's arguments.
-    ///
-    /// For each argument of inst which is defined by an alias, replace the
-    /// alias with the aliased value.
-    pub fn resolve_aliases_in_arguments(&mut self, inst: Inst) {
-        self.insts[inst].map_values(&mut self.value_lists, &mut self.jump_tables, |arg| {
-            resolve_aliases(&self.values, arg)
-        });
-    }
-
     /// Turn a value into an alias of another.
     ///
     /// Change the `dest` value to behave as an alias of `src`. This means that all uses of `dest`
diff --git a/cranelift/codegen/src/opts.rs b/cranelift/codegen/src/opts.rs
index 2d0819a43..f6bdcfb38 100644
--- a/cranelift/codegen/src/opts.rs
+++ b/cranelift/codegen/src/opts.rs
@@ -83,8 +83,7 @@ where
 
     fn next(&mut self, ctx: &mut IsleContext<'a, 'b, 'c>) -> Option<Self::Output> {
         while let Some(value) = self.stack.pop() {
-            debug_assert_ne!(value, Value::reserved_value());
-            let value = ctx.ctx.func.dfg.resolve_aliases(value);
+            debug_assert!(ctx.ctx.func.dfg.value_is_real(value));
             trace!("iter: value {:?}", value);
             match ctx.ctx.func.dfg.value_def(value) {
                 ValueDef::Union(x, y) => {
diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml
index c54b64d89..162d67338 100644
--- a/crates/c-api/Cargo.toml
+++ b/crates/c-api/Cargo.toml
@@ -48,3 +48,4 @@ disable-logging = ["log/max_level_off", "tracing/max_level_off"]
 coredump = ["wasmtime/coredump"]
 addr2line = ["wasmtime/addr2line"]
 demangle = ["wasmtime/demangle"]
+threads = ["wasmtime/threads"]
diff --git a/crates/c-api/artifact/Cargo.toml b/crates/c-api/artifact/Cargo.toml
index 0848f8b57..4589d2b90 100644
--- a/crates/c-api/artifact/Cargo.toml
+++ b/crates/c-api/artifact/Cargo.toml
@@ -30,6 +30,7 @@ default = [
   'coredump',
   'addr2line',
   'demangle',
+  'threads',
 ]
 async = ['wasmtime-c-api/async']
 profiling = ["wasmtime-c-api/profiling"]
@@ -42,3 +43,4 @@ coredump = ["wasmtime-c-api/coredump"]
 addr2line = ["wasmtime-c-api/addr2line"]
 demangle = ["wasmtime-c-api/demangle"]
 wat = ["wasmtime-c-api/wat"]
+threads = ["wasmtime-c-api/threads"]
diff --git a/crates/c-api/src/config.rs b/crates/c-api/src/config.rs
index 19fcda036..df554d9f7 100644
--- a/crates/c-api/src/config.rs
+++ b/crates/c-api/src/config.rs
@@ -71,6 +71,7 @@ pub extern "C" fn wasmtime_config_max_wasm_stack_set(c: &mut wasm_config_t, size
 }
 
 #[no_mangle]
+#[cfg(feature = "threads")]
 pub extern "C" fn wasmtime_config_wasm_threads_set(c: &mut wasm_config_t, enable: bool) {
     c.config.wasm_threads(enable);
 }
diff --git a/crates/c-api/src/sharedmemory.rs b/crates/c-api/src/sharedmemory.rs
index 1521373c4..80aaf58c1 100644
--- a/crates/c-api/src/sharedmemory.rs
+++ b/crates/c-api/src/sharedmemory.rs
@@ -1,4 +1,4 @@
-use crate::{handle_result, wasm_engine_t, wasm_memorytype_t, wasmtime_error_t, wasmtime_extern_t};
+use crate::{handle_result, wasm_memorytype_t, wasmtime_error_t, wasmtime_extern_t};
 use std::cell::UnsafeCell;
 use std::mem::MaybeUninit;
 use wasmtime::{Extern, SharedMemory};
@@ -8,8 +8,9 @@ type wasmtime_sharedmemory_t = SharedMemory;
 wasmtime_c_api_macros::declare_own!(wasmtime_sharedmemory_t);
 
 #[no_mangle]
+#[cfg(feature = "threads")]
 pub extern "C" fn wasmtime_sharedmemory_new(
-    engine: &wasm_engine_t,
+    engine: &crate::wasm_engine_t,
     ty: &wasm_memorytype_t,
     ret: &mut *mut wasmtime_sharedmemory_t,
 ) -> Option<Box<wasmtime_error_t>> {
diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml
index 543a50876..a77d40508 100644
--- a/crates/cli-flags/Cargo.toml
+++ b/crates/cli-flags/Cargo.toml
@@ -28,3 +28,5 @@ parallel-compilation = ["wasmtime/parallel-compilation", "dep:rayon"]
 logging = ["dep:file-per-thread-logger", "dep:tracing-subscriber"]
 cranelift = ["wasmtime/cranelift"]
 coredump = ["wasmtime/coredump"]
+gc = ["wasmtime/gc"]
+threads = ["wasmtime/threads"]
diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs
index d76dc9b23..d51e7bd96 100644
--- a/crates/cli-flags/src/lib.rs
+++ b/crates/cli-flags/src/lib.rs
@@ -242,6 +242,8 @@ wasmtime_option_group! {
         pub component_model: Option<bool>,
         /// Configure support for the function-references proposal.
         pub function_references: Option<bool>,
+        /// Configure support for the GC proposal.
+        pub gc: Option<bool>,
     }
 
     enum Wasm {
@@ -614,34 +616,38 @@ impl CommonOptions {
         if let Some(enable) = self.wasm.bulk_memory.or(all) {
             config.wasm_bulk_memory(enable);
         }
-        if let Some(enable) = self.wasm.reference_types.or(all) {
-            config.wasm_reference_types(enable);
-        }
-        if let Some(enable) = self.wasm.function_references.or(all) {
-            config.wasm_function_references(enable);
-        }
         if let Some(enable) = self.wasm.multi_value.or(all) {
             config.wasm_multi_value(enable);
         }
         if let Some(enable) = self.wasm.tail_call.or(all) {
             config.wasm_tail_call(enable);
         }
-        if let Some(enable) = self.wasm.threads.or(all) {
-            config.wasm_threads(enable);
-        }
         if let Some(enable) = self.wasm.multi_memory.or(all) {
             config.wasm_multi_memory(enable);
         }
         if let Some(enable) = self.wasm.memory64.or(all) {
             config.wasm_memory64(enable);
         }
-        if let Some(enable) = self.wasm.component_model.or(all) {
-            #[cfg(feature = "component-model")]
-            config.wasm_component_model(enable);
-            #[cfg(not(feature = "component-model"))]
-            if enable && all.is_none() {
-                anyhow::bail!("support for the component model was disabled at compile-time");
-            }
+
+        macro_rules! handle_conditionally_compiled {
+            ($(($feature:tt, $field:tt, $method:tt))*) => ($(
+                if let Some(enable) = self.wasm.$field.or(all) {
+                    #[cfg(feature = $feature)]
+                    config.$method(enable);
+                    #[cfg(not(feature = $feature))]
+                    if enable && all.is_none() {
+                        anyhow::bail!("support for {} was disabled at compile-time", $feature);
+                    }
+                }
+            )*)
+        }
+
+        handle_conditionally_compiled! {
+            ("component-model", component_model, wasm_component_model)
+            ("threads", threads, wasm_threads)
+            ("gc", gc, wasm_gc)
+            ("gc", reference_types, wasm_reference_types)
+            ("gc", function_references, wasm_function_references)
         }
         Ok(())
     }
diff --git a/crates/cranelift/Cargo.toml b/crates/cranelift/Cargo.toml
index 47a1a8636..2d3eac7c8 100644
--- a/crates/cranelift/Cargo.toml
+++ b/crates/cranelift/Cargo.toml
@@ -38,3 +38,4 @@ component-model = ["wasmtime-environ/component-model"]
 incremental-cache = ["cranelift-codegen/incremental-cache"]
 wmemcheck = []
 gc = ["wasmtime-environ/gc"]
+threads = ["wasmtime-environ/threads"]
diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs
index aec8000dd..e609da354 100644
--- a/crates/cranelift/src/func_environ.rs
+++ b/crates/cranelift/src/func_environ.rs
@@ -229,6 +229,7 @@ impl<'module_environment> FuncEnvironment<'module_environment> {
         )
     }
 
+    #[cfg(feature = "threads")]
     fn get_memory_atomic_wait(
         &mut self,
         func: &mut Function,
@@ -2602,21 +2603,31 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         expected: ir::Value,
         timeout: ir::Value,
     ) -> WasmResult<ir::Value> {
-        let addr = self.cast_memory_index_to_i64(&mut pos, addr, memory_index);
-        let implied_ty = pos.func.dfg.value_type(expected);
-        let (wait_func, memory_index) =
-            self.get_memory_atomic_wait(&mut pos.func, memory_index, implied_ty);
+        #[cfg(feature = "threads")]
+        {
+            let addr = self.cast_memory_index_to_i64(&mut pos, addr, memory_index);
+            let implied_ty = pos.func.dfg.value_type(expected);
+            let (wait_func, memory_index) =
+                self.get_memory_atomic_wait(&mut pos.func, memory_index, implied_ty);
 
-        let memory_index_arg = pos.ins().iconst(I32, memory_index as i64);
+            let memory_index_arg = pos.ins().iconst(I32, memory_index as i64);
 
-        let vmctx = self.vmctx_val(&mut pos);
+            let vmctx = self.vmctx_val(&mut pos);
 
-        let call_inst = pos.ins().call(
-            wait_func,
-            &[vmctx, memory_index_arg, addr, expected, timeout],
-        );
+            let call_inst = pos.ins().call(
+                wait_func,
+                &[vmctx, memory_index_arg, addr, expected, timeout],
+            );
 
-        Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
+            Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
+        }
+        #[cfg(not(feature = "threads"))]
+        {
+            let _ = (&mut pos, memory_index, addr, expected, timeout);
+            Err(wasmtime_environ::WasmError::Unsupported(
+                "threads support disabled at compile time".to_string(),
+            ))
+        }
     }
 
     fn translate_atomic_notify(
@@ -2627,16 +2638,26 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
         addr: ir::Value,
         count: ir::Value,
     ) -> WasmResult<ir::Value> {
-        let addr = self.cast_memory_index_to_i64(&mut pos, addr, memory_index);
-        let atomic_notify = self.builtin_functions.memory_atomic_notify(&mut pos.func);
+        #[cfg(feature = "threads")]
+        {
+            let addr = self.cast_memory_index_to_i64(&mut pos, addr, memory_index);
+            let atomic_notify = self.builtin_functions.memory_atomic_notify(&mut pos.func);
 
-        let memory_index_arg = pos.ins().iconst(I32, memory_index.index() as i64);
-        let vmctx = self.vmctx_val(&mut pos);
-        let call_inst = pos
-            .ins()
-            .call(atomic_notify, &[vmctx, memory_index_arg, addr, count]);
+            let memory_index_arg = pos.ins().iconst(I32, memory_index.index() as i64);
+            let vmctx = self.vmctx_val(&mut pos);
+            let call_inst = pos
+                .ins()
+                .call(atomic_notify, &[vmctx, memory_index_arg, addr, count]);
 
-        Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
+            Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
+        }
+        #[cfg(not(feature = "threads"))]
+        {
+            let _ = (&mut pos, memory_index, addr, count);
+            Err(wasmtime_environ::WasmError::Unsupported(
+                "threads support disabled at compile time".to_string(),
+            ))
+        }
     }
 
     fn translate_loop_header(&mut self, builder: &mut FunctionBuilder) -> WasmResult<()> {
diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml
index 39e536fc0..748a17aef 100644
--- a/crates/environ/Cargo.toml
+++ b/crates/environ/Cargo.toml
@@ -51,3 +51,4 @@ component-model = [
 demangle = ['dep:rustc-demangle', 'dep:cpp_demangle']
 gc = []
 compile = ['gimli/write', 'object/write_core']
+threads = []
diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs
index 5d2973c72..6c873d2fb 100644
--- a/crates/environ/src/builtin.rs
+++ b/crates/environ/src/builtin.rs
@@ -29,10 +29,13 @@ macro_rules! foreach_builtin_function {
             // 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 for wasm's `memory.atomic.notify` instruction.
+            #[cfg(feature = "threads")]
             memory_atomic_notify(vmctx: vmctx, memory: i32, addr: i64, count: i32) -> i32;
             // Returns an index for wasm's `memory.atomic.wait32` instruction.
+            #[cfg(feature = "threads")]
             memory_atomic_wait32(vmctx: vmctx, memory: i32, addr: i64, expected: i32, timeout: i64) -> i32;
             // Returns an index for wasm's `memory.atomic.wait64` instruction.
+            #[cfg(feature = "threads")]
             memory_atomic_wait64(vmctx: vmctx, memory: i32, addr: i64, expected: i64, timeout: i64) -> i32;
             // Invoked when fuel has run out while executing a function.
             out_of_gas(vmctx: vmctx);
diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml
index 278c58366..f53a5d7ab 100644
--- a/crates/runtime/Cargo.toml
+++ b/crates/runtime/Cargo.toml
@@ -72,3 +72,4 @@ wmemcheck = ['dep:wasmtime-wmemcheck']
 debug-builtins = ['dep:wasmtime-jit-debug']
 gc = ["wasmtime-environ/gc"]
 coredump = ["dep:wasm-encoder"]
+threads = ["wasmtime-environ/threads"]
diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs
index 118699e9b..cf81701fd 100644
--- a/crates/runtime/src/instance.rs
+++ b/crates/runtime/src/instance.rs
@@ -317,6 +317,7 @@ impl Instance {
     }
 
     /// Get a locally defined or imported memory.
+    #[cfg(feature = "threads")]
     pub(crate) fn get_runtime_memory(&mut self, index: MemoryIndex) -> &mut Memory {
         if let Some(defined_index) = self.module().defined_memory_index(index) {
             unsafe { &mut *self.get_defined_memory(defined_index) }
diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs
index ba92324b8..3c6d4f3fc 100644
--- a/crates/runtime/src/lib.rs
+++ b/crates/runtime/src/lib.rs
@@ -20,7 +20,6 @@ mod instance;
 mod memory;
 mod mmap;
 mod mmap_vec;
-mod parking_spot;
 mod send_sync_ptr;
 mod store_box;
 mod sys;
@@ -28,6 +27,9 @@ mod table;
 mod traphandlers;
 mod vmcontext;
 
+mod threads;
+pub use self::threads::*;
+
 #[cfg(feature = "debug-builtins")]
 pub mod debug_builtins;
 pub mod libcalls;
@@ -48,9 +50,7 @@ pub use crate::instance::{
 pub use crate::instance::{
     InstanceLimits, PoolingInstanceAllocator, PoolingInstanceAllocatorConfig,
 };
-pub use crate::memory::{
-    DefaultMemoryCreator, Memory, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory,
-};
+pub use crate::memory::{DefaultMemoryCreator, Memory, RuntimeLinearMemory, RuntimeMemoryCreator};
 pub use crate::mmap::Mmap;
 pub use crate::mmap_vec::MmapVec;
 pub use crate::mpk::MpkEnabled;
diff --git a/crates/runtime/src/libcalls.rs b/crates/runtime/src/libcalls.rs
index 8b263c763..2d8677021 100644
--- a/crates/runtime/src/libcalls.rs
+++ b/crates/runtime/src/libcalls.rs
@@ -61,6 +61,7 @@ use crate::{Instance, TrapReason};
 #[cfg(feature = "wmemcheck")]
 use anyhow::bail;
 use anyhow::Result;
+#[cfg(feature = "threads")]
 use std::time::{Duration, Instant};
 use wasmtime_environ::{DataIndex, ElemIndex, FuncIndex, MemoryIndex, TableIndex, Trap, Unsigned};
 #[cfg(feature = "wmemcheck")]
@@ -435,6 +436,7 @@ unsafe fn externref_global_set(instance: &mut Instance, index: u32, externref: *
 }
 
 // Implementation of `memory.atomic.notify` for locally defined memories.
+#[cfg(feature = "threads")]
 fn memory_atomic_notify(
     instance: &mut Instance,
     memory_index: u32,
@@ -448,6 +450,7 @@ fn memory_atomic_notify(
 }
 
 // Implementation of `memory.atomic.wait32` for locally defined memories.
+#[cfg(feature = "threads")]
 fn memory_atomic_wait32(
     instance: &mut Instance,
     memory_index: u32,
@@ -464,6 +467,7 @@ fn memory_atomic_wait32(
 }
 
 // Implementation of `memory.atomic.wait64` for locally defined memories.
+#[cfg(feature = "threads")]
 fn memory_atomic_wait64(
     instance: &mut Instance,
     memory_index: u32,
diff --git a/crates/runtime/src/memory.rs b/crates/runtime/src/memory.rs
index 578e6c2a7..d0e5fd87a 100644
--- a/crates/runtime/src/memory.rs
+++ b/crates/runtime/src/memory.rs
@@ -3,16 +3,13 @@
 //! `RuntimeLinearMemory` is to WebAssembly linear memories what `Table` is to WebAssembly tables.
 
 use crate::mmap::Mmap;
-use crate::parking_spot::{ParkingSpot, Waiter};
 use crate::vmcontext::VMMemoryDefinition;
-use crate::{MemoryImage, MemoryImageSlot, SendSyncPtr, Store, WaitResult};
+use crate::{MemoryImage, MemoryImageSlot, SendSyncPtr, SharedMemory, Store, WaitResult};
 use anyhow::Error;
 use anyhow::{bail, format_err, Result};
-use std::cell::RefCell;
 use std::ops::Range;
 use std::ptr::NonNull;
-use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
-use std::sync::{Arc, RwLock};
+use std::sync::Arc;
 use std::time::Instant;
 use wasmtime_environ::{MemoryPlan, MemoryStyle, Trap, WASM32_MAX_PAGES, WASM64_MAX_PAGES};
 
@@ -460,226 +457,8 @@ impl RuntimeLinearMemory for StaticMemory {
     }
 }
 
-/// For shared memory (and only for shared memory), this lock-version restricts
-/// access when growing the memory or checking its size. This is to conform with
-/// the [thread proposal]: "When `IsSharedArrayBuffer(...)` is true, the return
-/// value should be the result of an atomic read-modify-write of the new size to
-/// the internal `length` slot."
-///
-/// [thread proposal]:
-///     https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md#webassemblymemoryprototypegrow
-#[derive(Clone)]
-pub struct SharedMemory(Arc<SharedMemoryInner>);
-
-struct SharedMemoryInner {
-    memory: RwLock<Box<dyn RuntimeLinearMemory>>,
-    spot: ParkingSpot,
-    ty: wasmtime_environ::Memory,
-    def: LongTermVMMemoryDefinition,
-}
-
-impl SharedMemory {
-    /// Construct a new [`SharedMemory`].
-    pub fn new(plan: MemoryPlan) -> Result<Self> {
-        let (minimum_bytes, maximum_bytes) = Memory::limit_new(&plan, None)?;
-        let mmap_memory = MmapMemory::new(&plan, minimum_bytes, maximum_bytes, None)?;
-        Self::wrap(&plan, Box::new(mmap_memory), plan.memory)
-    }
-
-    /// Wrap an existing [Memory] with the locking provided by a [SharedMemory].
-    pub fn wrap(
-        plan: &MemoryPlan,
-        mut memory: Box<dyn RuntimeLinearMemory>,
-        ty: wasmtime_environ::Memory,
-    ) -> Result<Self> {
-        if !ty.shared {
-            bail!("shared memory must have a `shared` memory type");
-        }
-        if !matches!(plan.style, MemoryStyle::Static { .. }) {
-            bail!("shared memory can only be built from a static memory allocation")
-        }
-        assert!(
-            memory.as_any_mut().type_id() != std::any::TypeId::of::<SharedMemory>(),
-            "cannot re-wrap a shared memory"
-        );
-        Ok(Self(Arc::new(SharedMemoryInner {
-            ty,
-            spot: ParkingSpot::default(),
-            def: LongTermVMMemoryDefinition(memory.vmmemory()),
-            memory: RwLock::new(memory),
-        })))
-    }
-
-    /// Return the memory type for this [`SharedMemory`].
-    pub fn ty(&self) -> wasmtime_environ::Memory {
-        self.0.ty
-    }
-
-    /// Convert this shared memory into a [`Memory`].
-    pub fn as_memory(self) -> Memory {
-        Memory(Box::new(self))
-    }
-
-    /// Return a pointer to the shared memory's [VMMemoryDefinition].
-    pub fn vmmemory_ptr(&self) -> *const VMMemoryDefinition {
-        &self.0.def.0
-    }
-
-    /// Same as `RuntimeLinearMemory::grow`, except with `&self`.
-    pub fn grow(
-        &self,
-        delta_pages: u64,
-        store: Option<&mut dyn Store>,
-    ) -> Result<Option<(usize, usize)>, Error> {
-        let mut memory = self.0.memory.write().unwrap();
-        let result = memory.grow(delta_pages, store)?;
-        if let Some((_old_size_in_bytes, new_size_in_bytes)) = result {
-            // Store the new size to the `VMMemoryDefinition` for JIT-generated
-            // code (and runtime functions) to access. No other code can be
-            // growing this memory due to the write lock, but code in other
-            // threads could have access to this shared memory and we want them
-            // to see the most consistent version of the `current_length`; a
-            // weaker consistency is possible if we accept them seeing an older,
-            // smaller memory size (assumption: memory only grows) but presently
-            // we are aiming for accuracy.
-            //
-            // Note that it could be possible to access a memory address that is
-            // now-valid due to changes to the page flags in `grow` above but
-            // beyond the `memory.size` that we are about to assign to. In these
-            // and similar cases, discussion in the thread proposal concluded
-            // that: "multiple accesses in one thread racing with another
-            // thread's `memory.grow` that are in-bounds only after the grow
-            // commits may independently succeed or trap" (see
-            // https://github.com/WebAssembly/threads/issues/26#issuecomment-433930711).
-            // In other words, some non-determinism is acceptable when using
-            // `memory.size` on work being done by `memory.grow`.
-            self.0
-                .def
-                .0
-                .current_length
-                .store(new_size_in_bytes, Ordering::SeqCst);
-        }
-        Ok(result)
-    }
-
-    /// Implementation of `memory.atomic.notify` for this shared memory.
-    pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result<u32, Trap> {
-        let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?;
-        log::trace!("memory.atomic.notify(addr={addr_index:#x}, count={count})");
-        let ptr = unsafe { &*ptr };
-        Ok(self.0.spot.notify(ptr, count))
-    }
-
-    /// Implementation of `memory.atomic.wait32` for this shared memory.
-    pub fn atomic_wait32(
-        &self,
-        addr_index: u64,
-        expected: u32,
-        timeout: Option<Instant>,
-    ) -> Result<WaitResult, Trap> {
-        let addr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?;
-        log::trace!(
-            "memory.atomic.wait32(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
-        );
-
-        // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
-        assert!(std::mem::size_of::<AtomicU32>() == 4);
-        assert!(std::mem::align_of::<AtomicU32>() <= 4);
-        let atomic = unsafe { AtomicU32::from_ptr(addr.cast()) };
-
-        WAITER.with(|waiter| {
-            let mut waiter = waiter.borrow_mut();
-            Ok(self.0.spot.wait32(atomic, expected, timeout, &mut waiter))
-        })
-    }
-
-    /// Implementation of `memory.atomic.wait64` for this shared memory.
-    pub fn atomic_wait64(
-        &self,
-        addr_index: u64,
-        expected: u64,
-        timeout: Option<Instant>,
-    ) -> Result<WaitResult, Trap> {
-        let addr = validate_atomic_addr(&self.0.def.0, addr_index, 8, 8)?;
-        log::trace!(
-            "memory.atomic.wait64(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
-        );
-
-        // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
-        assert!(std::mem::size_of::<AtomicU64>() == 8);
-        assert!(std::mem::align_of::<AtomicU64>() <= 8);
-        let atomic = unsafe { AtomicU64::from_ptr(addr.cast()) };
-
-        WAITER.with(|waiter| {
-            let mut waiter = waiter.borrow_mut();
-            Ok(self.0.spot.wait64(atomic, expected, timeout, &mut waiter))
-        })
-    }
-}
-
-thread_local! {
-    /// Structure used in conjunction with `ParkingSpot` to block the current
-    /// thread if necessary. Note that this is lazily initialized.
-    static WAITER: RefCell<Waiter> = const { RefCell::new(Waiter::new()) };
-}
-
-/// Shared memory needs some representation of a `VMMemoryDefinition` for
-/// JIT-generated code to access. This structure owns the base pointer and
-/// length to the actual memory and we share this definition across threads by:
-/// - never changing the base pointer; according to the specification, shared
-///   memory must be created with a known maximum size so it can be allocated
-///   once and never moved
-/// - carefully changing the length, using atomic accesses in both the runtime
-///   and JIT-generated code.
-struct LongTermVMMemoryDefinition(VMMemoryDefinition);
-unsafe impl Send for LongTermVMMemoryDefinition {}
-unsafe impl Sync for LongTermVMMemoryDefinition {}
-
-/// Proxy all calls through the [`RwLock`].
-impl RuntimeLinearMemory for SharedMemory {
-    fn byte_size(&self) -> usize {
-        self.0.memory.read().unwrap().byte_size()
-    }
-
-    fn maximum_byte_size(&self) -> Option<usize> {
-        self.0.memory.read().unwrap().maximum_byte_size()
-    }
-
-    fn grow(
-        &mut self,
-        delta_pages: u64,
-        store: Option<&mut dyn Store>,
-    ) -> Result<Option<(usize, usize)>, Error> {
-        SharedMemory::grow(self, delta_pages, store)
-    }
-
-    fn grow_to(&mut self, size: usize) -> Result<()> {
-        self.0.memory.write().unwrap().grow_to(size)
-    }
-
-    fn vmmemory(&mut self) -> VMMemoryDefinition {
-        // `vmmemory()` is used for writing the `VMMemoryDefinition` of a memory
-        // into its `VMContext`; this should never be possible for a shared
-        // memory because the only `VMMemoryDefinition` for it should be stored
-        // in its own `def` field.
-        unreachable!()
-    }
-
-    fn needs_init(&self) -> bool {
-        self.0.memory.read().unwrap().needs_init()
-    }
-
-    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
-        self
-    }
-
-    fn wasm_accessible(&self) -> Range<usize> {
-        self.0.memory.read().unwrap().wasm_accessible()
-    }
-}
-
 /// Representation of a runtime wasm linear memory.
-pub struct Memory(Box<dyn RuntimeLinearMemory>);
+pub struct Memory(pub(crate) Box<dyn RuntimeLinearMemory>);
 
 impl Memory {
     /// Create a new dynamic (movable) memory instance for the specified plan.
@@ -734,7 +513,7 @@ impl Memory {
     ///
     /// Returns the minimum size and optional maximum size of the memory, in
     /// bytes.
-    fn limit_new(
+    pub(crate) fn limit_new(
         plan: &MemoryPlan,
         store: Option<&mut dyn Store>,
     ) -> Result<(usize, Option<usize>)> {
@@ -952,7 +731,7 @@ impl Memory {
 /// we are using static memories with virtual memory guard pages) this manual
 /// check is here so we don't segfault from Rust. For other configurations,
 /// these checks are required anyways.
-fn validate_atomic_addr(
+pub fn validate_atomic_addr(
     def: &VMMemoryDefinition,
     addr: u64,
     access_size: u64,
diff --git a/crates/runtime/src/threads/mod.rs b/crates/runtime/src/threads/mod.rs
new file mode 100644
index 000000000..dad96f471
--- /dev/null
+++ b/crates/runtime/src/threads/mod.rs
@@ -0,0 +1,12 @@
+#[cfg(feature = "threads")]
+mod parking_spot;
+
+#[cfg(feature = "threads")]
+mod shared_memory;
+#[cfg(feature = "threads")]
+pub use shared_memory::SharedMemory;
+
+#[cfg(not(feature = "threads"))]
+mod shared_memory_disabled;
+#[cfg(not(feature = "threads"))]
+pub use shared_memory_disabled::SharedMemory;
diff --git a/crates/runtime/src/parking_spot.rs b/crates/runtime/src/threads/parking_spot.rs
similarity index 100%
rename from crates/runtime/src/parking_spot.rs
rename to crates/runtime/src/threads/parking_spot.rs
diff --git a/crates/runtime/src/threads/shared_memory.rs b/crates/runtime/src/threads/shared_memory.rs
new file mode 100644
index 000000000..0837d1530
--- /dev/null
+++ b/crates/runtime/src/threads/shared_memory.rs
@@ -0,0 +1,230 @@
+use crate::memory::{validate_atomic_addr, MmapMemory};
+use crate::threads::parking_spot::{ParkingSpot, Waiter};
+use crate::vmcontext::VMMemoryDefinition;
+use crate::{Memory, RuntimeLinearMemory, Store, WaitResult};
+use anyhow::Error;
+use anyhow::{bail, Result};
+use std::cell::RefCell;
+use std::ops::Range;
+use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
+use std::sync::{Arc, RwLock};
+use std::time::Instant;
+use wasmtime_environ::{MemoryPlan, MemoryStyle, Trap};
+
+/// For shared memory (and only for shared memory), this lock-version restricts
+/// access when growing the memory or checking its size. This is to conform with
+/// the [thread proposal]: "When `IsSharedArrayBuffer(...)` is true, the return
+/// value should be the result of an atomic read-modify-write of the new size to
+/// the internal `length` slot."
+///
+/// [thread proposal]:
+///     https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md#webassemblymemoryprototypegrow
+#[derive(Clone)]
+pub struct SharedMemory(Arc<SharedMemoryInner>);
+
+struct SharedMemoryInner {
+    memory: RwLock<Box<dyn RuntimeLinearMemory>>,
+    spot: ParkingSpot,
+    ty: wasmtime_environ::Memory,
+    def: LongTermVMMemoryDefinition,
+}
+
+impl SharedMemory {
+    /// Construct a new [`SharedMemory`].
+    pub fn new(plan: MemoryPlan) -> Result<Self> {
+        let (minimum_bytes, maximum_bytes) = Memory::limit_new(&plan, None)?;
+        let mmap_memory = MmapMemory::new(&plan, minimum_bytes, maximum_bytes, None)?;
+        Self::wrap(&plan, Box::new(mmap_memory), plan.memory)
+    }
+
+    /// Wrap an existing [Memory] with the locking provided by a [SharedMemory].
+    pub fn wrap(
+        plan: &MemoryPlan,
+        mut memory: Box<dyn RuntimeLinearMemory>,
+        ty: wasmtime_environ::Memory,
+    ) -> Result<Self> {
+        if !ty.shared {
+            bail!("shared memory must have a `shared` memory type");
+        }
+        if !matches!(plan.style, MemoryStyle::Static { .. }) {
+            bail!("shared memory can only be built from a static memory allocation")
+        }
+        assert!(
+            memory.as_any_mut().type_id() != std::any::TypeId::of::<SharedMemory>(),
+            "cannot re-wrap a shared memory"
+        );
+        Ok(Self(Arc::new(SharedMemoryInner {
+            ty,
+            spot: ParkingSpot::default(),
+            def: LongTermVMMemoryDefinition(memory.vmmemory()),
+            memory: RwLock::new(memory),
+        })))
+    }
+
+    /// Return the memory type for this [`SharedMemory`].
+    pub fn ty(&self) -> wasmtime_environ::Memory {
+        self.0.ty
+    }
+
+    /// Convert this shared memory into a [`Memory`].
+    pub fn as_memory(self) -> Memory {
+        Memory(Box::new(self))
+    }
+
+    /// Return a pointer to the shared memory's [VMMemoryDefinition].
+    pub fn vmmemory_ptr(&self) -> *const VMMemoryDefinition {
+        &self.0.def.0
+    }
+
+    /// Same as `RuntimeLinearMemory::grow`, except with `&self`.
+    pub fn grow(
+        &self,
+        delta_pages: u64,
+        store: Option<&mut dyn Store>,
+    ) -> Result<Option<(usize, usize)>, Error> {
+        let mut memory = self.0.memory.write().unwrap();
+        let result = memory.grow(delta_pages, store)?;
+        if let Some((_old_size_in_bytes, new_size_in_bytes)) = result {
+            // Store the new size to the `VMMemoryDefinition` for JIT-generated
+            // code (and runtime functions) to access. No other code can be
+            // growing this memory due to the write lock, but code in other
+            // threads could have access to this shared memory and we want them
+            // to see the most consistent version of the `current_length`; a
+            // weaker consistency is possible if we accept them seeing an older,
+            // smaller memory size (assumption: memory only grows) but presently
+            // we are aiming for accuracy.
+            //
+            // Note that it could be possible to access a memory address that is
+            // now-valid due to changes to the page flags in `grow` above but
+            // beyond the `memory.size` that we are about to assign to. In these
+            // and similar cases, discussion in the thread proposal concluded
+            // that: "multiple accesses in one thread racing with another
+            // thread's `memory.grow` that are in-bounds only after the grow
+            // commits may independently succeed or trap" (see
+            // https://github.com/WebAssembly/threads/issues/26#issuecomment-433930711).
+            // In other words, some non-determinism is acceptable when using
+            // `memory.size` on work being done by `memory.grow`.
+            self.0
+                .def
+                .0
+                .current_length
+                .store(new_size_in_bytes, Ordering::SeqCst);
+        }
+        Ok(result)
+    }
+
+    /// Implementation of `memory.atomic.notify` for this shared memory.
+    pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result<u32, Trap> {
+        let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?;
+        log::trace!("memory.atomic.notify(addr={addr_index:#x}, count={count})");
+        let ptr = unsafe { &*ptr };
+        Ok(self.0.spot.notify(ptr, count))
+    }
+
+    /// Implementation of `memory.atomic.wait32` for this shared memory.
+    pub fn atomic_wait32(
+        &self,
+        addr_index: u64,
+        expected: u32,
+        timeout: Option<Instant>,
+    ) -> Result<WaitResult, Trap> {
+        let addr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?;
+        log::trace!(
+            "memory.atomic.wait32(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
+        );
+
+        // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
+        assert!(std::mem::size_of::<AtomicU32>() == 4);
+        assert!(std::mem::align_of::<AtomicU32>() <= 4);
+        let atomic = unsafe { AtomicU32::from_ptr(addr.cast()) };
+
+        WAITER.with(|waiter| {
+            let mut waiter = waiter.borrow_mut();
+            Ok(self.0.spot.wait32(atomic, expected, timeout, &mut waiter))
+        })
+    }
+
+    /// Implementation of `memory.atomic.wait64` for this shared memory.
+    pub fn atomic_wait64(
+        &self,
+        addr_index: u64,
+        expected: u64,
+        timeout: Option<Instant>,
+    ) -> Result<WaitResult, Trap> {
+        let addr = validate_atomic_addr(&self.0.def.0, addr_index, 8, 8)?;
+        log::trace!(
+            "memory.atomic.wait64(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
+        );
+
+        // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
+        assert!(std::mem::size_of::<AtomicU64>() == 8);
+        assert!(std::mem::align_of::<AtomicU64>() <= 8);
+        let atomic = unsafe { AtomicU64::from_ptr(addr.cast()) };
+
+        WAITER.with(|waiter| {
+            let mut waiter = waiter.borrow_mut();
+            Ok(self.0.spot.wait64(atomic, expected, timeout, &mut waiter))
+        })
+    }
+}
+
+thread_local! {
+    /// Structure used in conjunction with `ParkingSpot` to block the current
+    /// thread if necessary. Note that this is lazily initialized.
+    static WAITER: RefCell<Waiter> = const { RefCell::new(Waiter::new()) };
+}
+
+/// Shared memory needs some representation of a `VMMemoryDefinition` for
+/// JIT-generated code to access. This structure owns the base pointer and
+/// length to the actual memory and we share this definition across threads by:
+/// - never changing the base pointer; according to the specification, shared
+///   memory must be created with a known maximum size so it can be allocated
+///   once and never moved
+/// - carefully changing the length, using atomic accesses in both the runtime
+///   and JIT-generated code.
+struct LongTermVMMemoryDefinition(VMMemoryDefinition);
+unsafe impl Send for LongTermVMMemoryDefinition {}
+unsafe impl Sync for LongTermVMMemoryDefinition {}
+
+/// Proxy all calls through the [`RwLock`].
+impl RuntimeLinearMemory for SharedMemory {
+    fn byte_size(&self) -> usize {
+        self.0.memory.read().unwrap().byte_size()
+    }
+
+    fn maximum_byte_size(&self) -> Option<usize> {
+        self.0.memory.read().unwrap().maximum_byte_size()
+    }
+
+    fn grow(
+        &mut self,
+        delta_pages: u64,
+        store: Option<&mut dyn Store>,
+    ) -> Result<Option<(usize, usize)>, Error> {
+        SharedMemory::grow(self, delta_pages, store)
+    }
+
+    fn grow_to(&mut self, size: usize) -> Result<()> {
+        self.0.memory.write().unwrap().grow_to(size)
+    }
+
+    fn vmmemory(&mut self) -> VMMemoryDefinition {
+        // `vmmemory()` is used for writing the `VMMemoryDefinition` of a memory
+        // into its `VMContext`; this should never be possible for a shared
+        // memory because the only `VMMemoryDefinition` for it should be stored
+        // in its own `def` field.
+        unreachable!()
+    }
+
+    fn needs_init(&self) -> bool {
+        self.0.memory.read().unwrap().needs_init()
+    }
+
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
+        self
+    }
+
+    fn wasm_accessible(&self) -> Range<usize> {
+        self.0.memory.read().unwrap().wasm_accessible()
+    }
+}
diff --git a/crates/runtime/src/threads/shared_memory_disabled.rs b/crates/runtime/src/threads/shared_memory_disabled.rs
new file mode 100644
index 000000000..f102444c4
--- /dev/null
+++ b/crates/runtime/src/threads/shared_memory_disabled.rs
@@ -0,0 +1,100 @@
+#![allow(missing_docs)]
+
+use crate::{RuntimeLinearMemory, Store, VMMemoryDefinition, WaitResult};
+use anyhow::{bail, Result};
+use std::ops::Range;
+use std::time::Instant;
+use wasmtime_environ::{MemoryPlan, Trap};
+
+#[derive(Clone)]
+pub enum SharedMemory {}
+
+impl SharedMemory {
+    pub fn wrap(
+        _plan: &MemoryPlan,
+        _memory: Box<dyn RuntimeLinearMemory>,
+        _ty: wasmtime_environ::Memory,
+    ) -> Result<Self> {
+        bail!("support for shared memories was disabled at compile time");
+    }
+
+    pub fn ty(&self) -> wasmtime_environ::Memory {
+        match *self {}
+    }
+
+    pub fn as_memory(self) -> crate::Memory {
+        match self {}
+    }
+
+    pub fn vmmemory_ptr(&self) -> *const VMMemoryDefinition {
+        match *self {}
+    }
+
+    pub fn grow(
+        &self,
+        _delta_pages: u64,
+        _store: Option<&mut dyn Store>,
+    ) -> Result<Option<(usize, usize)>> {
+        match *self {}
+    }
+
+    pub fn atomic_notify(&self, _addr_index: u64, _count: u32) -> Result<u32, Trap> {
+        match *self {}
+    }
+
+    pub fn atomic_wait32(
+        &self,
+        _addr_index: u64,
+        _expected: u32,
+        _timeout: Option<Instant>,
+    ) -> Result<WaitResult, Trap> {
+        match *self {}
+    }
+
+    pub fn atomic_wait64(
+        &self,
+        _addr_index: u64,
+        _expected: u64,
+        _timeout: Option<Instant>,
+    ) -> Result<WaitResult, Trap> {
+        match *self {}
+    }
+}
+
+impl RuntimeLinearMemory for SharedMemory {
+    fn byte_size(&self) -> usize {
+        match *self {}
+    }
+
+    fn maximum_byte_size(&self) -> Option<usize> {
+        match *self {}
+    }
+
+    fn grow(
+        &mut self,
+        _delta_pages: u64,
+        _store: Option<&mut dyn Store>,
+    ) -> Result<Option<(usize, usize)>> {
+        match *self {}
+    }
+
+    fn grow_to(&mut self, _size: usize) -> Result<()> {
+        match *self {}
+    }
+
+    fn vmmemory(&mut self) -> VMMemoryDefinition {
+        match *self {}
+    }
+
+    fn needs_init(&self) -> bool {
+        match *self {}
+    }
+
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
+        match *self {}
+    }
+
+    fn wasm_accessible(&self) -> Range<usize> {
+        match *self {}
+    }
+}
diff --git a/crates/wasi-threads/Cargo.toml b/crates/wasi-threads/Cargo.toml
index 1f42ba9f3..84b055385 100644
--- a/crates/wasi-threads/Cargo.toml
+++ b/crates/wasi-threads/Cargo.toml
@@ -19,4 +19,4 @@ anyhow = { workspace = true }
 log = { workspace = true }
 rand = "0.8"
 wasi-common = { workspace = true, features = ["exit"]}
-wasmtime = { workspace = true }
+wasmtime = { workspace = true, features = ['threads'] }
diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml
index b64bc9520..367c4fcf5 100644
--- a/crates/wasmtime/Cargo.toml
+++ b/crates/wasmtime/Cargo.toml
@@ -99,6 +99,7 @@ default = [
   'debug-builtins',
   'runtime',
   'component-model',
+  'threads',
 ]
 
 # An on-by-default feature enabling runtime compilation of WebAssembly modules
@@ -191,3 +192,6 @@ runtime = ["dep:wasmtime-runtime", "dep:wasmtime-jit-icache-coherence", "dep:was
 # 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"]
+
+# Enable runtime support for the WebAssembly threads proposal.
+threads = ["wasmtime-runtime?/threads", "wasmtime-cranelift?/threads"]
diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs
index f4f2e091d..1159a9628 100644
--- a/crates/wasmtime/src/config.rs
+++ b/crates/wasmtime/src/config.rs
@@ -254,11 +254,17 @@ 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;
+        // Conditionally enabled features depending on compile-time crate
+        // features. Note that if these features are disabled then `Config` has
+        // no way of re-enabling them.
+        ret.features.reference_types = cfg!(feature = "gc");
+        ret.features.threads = cfg!(feature = "threads");
+        ret.features.component_model = cfg!(feature = "component-model");
+
+        // If GC is disabled at compile time also disable it in features
+        // forcibly irrespective of `wasmparser` defaults. Note that these also
+        // aren't yet fully implemented in Wasmtime.
+        if !cfg!(feature = "gc") {
             ret.features.function_references = false;
             ret.features.gc = false;
         }
@@ -266,8 +272,6 @@ impl Config {
         ret.wasm_multi_value(true);
         ret.wasm_bulk_memory(true);
         ret.wasm_simd(true);
-        #[cfg(feature = "component-model")]
-        ret.wasm_component_model(true);
         ret.wasm_backtrace_details(WasmBacktraceDetails::Environment);
 
         // This is on-by-default in `wasmparser` since it's a stage 4+ proposal
@@ -718,6 +722,8 @@ impl Config {
     ///
     /// [threads]: https://github.com/webassembly/threads
     /// [wasi-threads]: https://github.com/webassembly/wasi-threads
+    #[cfg(feature = "threads")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "threads")))]
     pub fn wasm_threads(&mut self, enable: bool) -> &mut Self {
         self.features.threads = enable;
         self
diff --git a/crates/wasmtime/src/lib.rs b/crates/wasmtime/src/lib.rs
index 78002951c..2c0e23be8 100644
--- a/crates/wasmtime/src/lib.rs
+++ b/crates/wasmtime/src/lib.rs
@@ -246,6 +246,14 @@
 //! * `component-model` - Enabled by default, this enables support for the
 //!   [`wasmtime::component`](component) API for working with components.
 //!
+//! * `gc` - Enabled by default, this enables support for a number of
+//!   WebAssembly proposals such as `reference-types`, `function-references`,
+//!   and `gc`. Note that the implementation of the `gc` proposal itself is not
+//!   yet complete at this time.
+//!
+//! * `threads` - Enabled by default, this enables compile-time support for the
+//!   WebAssembly `threads` proposal, notably shared memories.
+//!
 //! More crate features can be found in the [manifest] of Wasmtime itself for
 //! seeing what can be enabled and disabled.
 //!
diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs
index 3aa6d507c..1bb48bcc7 100644
--- a/crates/wasmtime/src/runtime/memory.rs
+++ b/crates/wasmtime/src/runtime/memory.rs
@@ -725,6 +725,8 @@ impl SharedMemory {
     /// Construct a [`SharedMemory`] by providing both the `minimum` and
     /// `maximum` number of 64K-sized pages. This call allocates the necessary
     /// pages on the system.
+    #[cfg(feature = "threads")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "threads")))]
     pub fn new(engine: &Engine, ty: MemoryType) -> Result<Self> {
         if !ty.is_shared() {
             bail!("shared memory must have the `shared` flag enabled on its memory type")
@@ -921,11 +923,11 @@ impl SharedMemory {
                 .get_defined_memory(wasmtime_export.index)
                 .as_mut()
                 .unwrap();
-            let shared_memory = memory
-                .as_shared_memory()
-                .expect("unable to convert from a shared memory")
-                .clone();
-            Self(shared_memory, store.engine().clone())
+            match memory.as_shared_memory() {
+                #[cfg_attr(not(feature = "threads"), allow(unreachable_code))]
+                Some(mem) => Self(mem.clone(), store.engine().clone()),
+                None => panic!("unable to convert from a shared memory"),
+            }
         })
     }
 }


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/licm.clif b/cranelift/filetests/filetests/egraph/licm.clif
index 51fdf38a5..8409f3dc0 100644
--- a/cranelift/filetests/filetests/egraph/licm.clif
+++ b/cranelift/filetests/filetests/egraph/licm.clif
@@ -58,7 +58,6 @@ block2(v8: i64x2):
 ; check:      v9 = iconst.i32 1
 ; check:      v10 = isub v3, v9
 ; check:      v5 = iadd v2, v4
-; check:      v8 -> v5
 ; check:      brif v10, block1(v5, v10), block2
 ; check:  block2:
 ; check:      return v5