diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 37679b5051a2..6f795b6568d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -369,6 +369,12 @@ jobs: -p wasmtime --no-default-features --features gc -p wasmtime --no-default-features --features runtime,gc -p wasmtime --no-default-features --features cranelift,gc + -p wasmtime --no-default-features --features gc-drc + -p wasmtime --no-default-features --features runtime,gc-drc + -p wasmtime --no-default-features --features cranelift,gc-drc + -p wasmtime --no-default-features --features gc-null + -p wasmtime --no-default-features --features runtime,gc-null + -p wasmtime --no-default-features --features cranelift,gc-null -p wasmtime --no-default-features --features runtime -p wasmtime --no-default-features --features threads -p wasmtime --no-default-features --features runtime,threads diff --git a/Cargo.toml b/Cargo.toml index f1122821b28d..8337e60f914e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -393,6 +393,8 @@ default = [ "component-model", "threads", "gc", + "gc-drc", + "gc-null", "winch", # Enable some nice features of clap by default, but they come at a binary size @@ -443,7 +445,9 @@ coredump = ["wasmtime-cli-flags/coredump"] addr2line = ["wasmtime/addr2line"] debug-builtins = ["wasmtime/debug-builtins"] threads = ["wasmtime-cli-flags/threads"] -gc = ["wasmtime-cli-flags/gc"] +gc = ["wasmtime-cli-flags/gc", "wasmtime/gc"] +gc-drc = ["gc", "wasmtime/gc-drc"] +gc-null = ["gc", "wasmtime/gc-null"] # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help` # for more information on each subcommand. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index 832b86214da8..9cf810122915 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -31,5 +31,7 @@ logging = ["dep:file-per-thread-logger", "dep:tracing-subscriber"] cranelift = ["wasmtime/cranelift"] coredump = ["wasmtime/coredump"] gc = ["wasmtime/gc"] +gc-drc = ["gc", "wasmtime/gc-drc"] +gc-null = ["gc", "wasmtime/gc-null"] threads = ["wasmtime/threads"] memory-protection-keys = ["wasmtime/memory-protection-keys"] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 592ebd1f40e2..c6e5cedf816f 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1,6 +1,6 @@ //! Contains the common Wasmtime command line interface (CLI) flags. -use anyhow::Result; +use anyhow::{bail, Result}; use clap::Parser; use std::time::Duration; use wasmtime::Config; @@ -182,6 +182,16 @@ wasmtime_option_group! { /// Currently only `cranelift` and `winch` are supported, but not all /// builds of Wasmtime have both built in. pub compiler: Option, + /// Which garbage collector to use: `drc` or `null`. + /// + /// `drc` is the deferred reference-counting collector. + /// + /// `null` is the null garbage collector, which does not collect any + /// garbage. + /// + /// Note that not all builds of Wasmtime will have support for garbage + /// collection included. + pub collector: Option, /// Enable Cranelift's internal debug verifier (expensive) pub cranelift_debug_verifier: Option, /// Whether or not to enable caching of compiled modules. @@ -532,6 +542,23 @@ impl CommonOptions { strategy => config.strategy(strategy), _ => err, } + match_feature! { + ["gc" : self.codegen.collector] + collector => match collector { + #[cfg(not(feature = "gc-drc"))] + wasmtime::Collector::DeferredReferenceCounting => bail!( + "support for the `drc` collector is unavailable because the \ + `gc-drc` feature was disabled at compile time", + ), + #[cfg(not(feature = "gc-null"))] + wasmtime::Collector::Null => bail!( + "support for the `null` collector is unavailable because the \ + `gc-null` feature was disabled at compile time", + ), + _ => config.collector(collector), + }, + _ => err, + } match_feature! { ["cranelift" : target] target => config.target(target)?, diff --git a/crates/cli-flags/src/opt.rs b/crates/cli-flags/src/opt.rs index a448fc1e380e..1588bbfcfd1d 100644 --- a/crates/cli-flags/src/opt.rs +++ b/crates/cli-flags/src/opt.rs @@ -385,6 +385,17 @@ impl WasmtimeOptionValue for wasmtime::Strategy { } } +impl WasmtimeOptionValue for wasmtime::Collector { + const VAL_HELP: &'static str = "=drc|null"; + fn parse(val: Option<&str>) -> Result { + match String::parse(val)?.as_str() { + "drc" => Ok(wasmtime::Collector::DeferredReferenceCounting), + "null" => Ok(wasmtime::Collector::Null), + other => bail!("unknown collector `{other}` only `drc` and `null` accepted",), + } + } +} + impl WasmtimeOptionValue for WasiNnGraph { const VAL_HELP: &'static str = "=::"; fn parse(val: Option<&str>) -> Result { diff --git a/crates/cranelift/Cargo.toml b/crates/cranelift/Cargo.toml index 8fcc96cfa5f4..37232589ef2a 100644 --- a/crates/cranelift/Cargo.toml +++ b/crates/cranelift/Cargo.toml @@ -41,4 +41,6 @@ component-model = ["wasmtime-environ/component-model"] incremental-cache = ["cranelift-codegen/incremental-cache"] wmemcheck = ["wasmtime-environ/wmemcheck"] gc = ["wasmtime-environ/gc"] +gc-drc = ["gc", "wasmtime-environ/gc-drc"] +gc-null = ["gc", "wasmtime-environ/gc-null"] threads = ["wasmtime-environ/threads"] diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index d634a3a33560..9fa2826c232e 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -118,7 +118,7 @@ pub struct FuncEnvironment<'module_environment> { /// Offsets to struct fields accessed by JIT code. pub(crate) offsets: VMOffsets, - tunables: &'module_environment Tunables, + pub(crate) tunables: &'module_environment Tunables, /// A function-local variable which stores the cached value of the amount of /// fuel remaining to execute. If used this is modified frequently so it's diff --git a/crates/cranelift/src/gc/enabled.rs b/crates/cranelift/src/gc/enabled.rs index c756406c2209..68db4ddd03e5 100644 --- a/crates/cranelift/src/gc/enabled.rs +++ b/crates/cranelift/src/gc/enabled.rs @@ -10,16 +10,41 @@ use cranelift_codegen::{ use cranelift_entity::packed_option::ReservedValue; use cranelift_frontend::FunctionBuilder; use wasmtime_environ::{ - GcArrayLayout, GcLayout, GcStructLayout, ModuleInternedTypeIndex, PtrSize, TypeIndex, VMGcKind, - WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, WasmStorageType, - WasmValType, I31_DISCRIMINANT, NON_NULL_NON_I31_MASK, + wasm_unsupported, Collector, GcArrayLayout, GcLayout, GcStructLayout, ModuleInternedTypeIndex, + PtrSize, TypeIndex, VMGcKind, WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, + WasmResult, WasmStorageType, WasmValType, I31_DISCRIMINANT, NON_NULL_NON_I31_MASK, }; +#[cfg(feature = "gc-drc")] mod drc; +#[cfg(feature = "gc-null")] +mod null; /// Get the default GC compiler. -pub fn gc_compiler(_func_env: &FuncEnvironment<'_>) -> WasmResult> { - Ok(Box::new(drc::DrcCompiler::default())) +pub fn gc_compiler(func_env: &FuncEnvironment<'_>) -> WasmResult> { + match func_env.tunables.collector { + #[cfg(feature = "gc-drc")] + Some(Collector::DeferredReferenceCounting) => Ok(Box::new(drc::DrcCompiler::default())), + #[cfg(not(feature = "gc-drc"))] + Some(Collector::DeferredReferenceCounting) => Err(wasm_unsupported!( + "the null collector is unavailable because the `gc-drc` feature \ + was disabled at compile time", + )), + + #[cfg(feature = "gc-null")] + Some(Collector::Null) => Ok(Box::new(null::NullCompiler::default())), + #[cfg(not(feature = "gc-null"))] + Some(Collector::Null) => Err(wasm_unsupported!( + "the null collector is unavailable because the `gc-null` feature \ + was disabled at compile time", + )), + + None => Err(wasm_unsupported!( + "support for Wasm GC disabled because no collector implementation \ + was selected at compile time; enable one of the `gc-drc` or \ + `gc-null` features", + )), + } } fn unbarriered_load_gc_ref( diff --git a/crates/cranelift/src/gc/enabled/null.rs b/crates/cranelift/src/gc/enabled/null.rs new file mode 100644 index 000000000000..1b66e9be824f --- /dev/null +++ b/crates/cranelift/src/gc/enabled/null.rs @@ -0,0 +1,71 @@ +//! Compiler for the null collector. + +#![allow(unused)] // TODO FITZGEN + +use super::*; +use crate::gc::{gc_compiler, ArrayInit}; +use crate::translate::TargetEnvironment; +use crate::{func_environ::FuncEnvironment, gc::GcCompiler, TRAP_INTERNAL_ASSERT}; +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::{self, InstBuilder}; +use cranelift_frontend::FunctionBuilder; +use smallvec::SmallVec; +use wasmtime_environ::{ + null::NullTypeLayouts, GcArrayLayout, GcTypeLayouts, ModuleInternedTypeIndex, PtrSize, + TypeIndex, VMGcKind, WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, + WasmStorageType, WasmValType, +}; + +#[derive(Default)] +pub struct NullCompiler { + layouts: NullTypeLayouts, +} + +impl GcCompiler for NullCompiler { + fn layouts(&self) -> &dyn GcTypeLayouts { + &self.layouts + } + + fn alloc_array( + &mut self, + func_env: &mut FuncEnvironment<'_>, + builder: &mut FunctionBuilder<'_>, + array_type_index: TypeIndex, + init: super::ArrayInit<'_>, + ) -> WasmResult { + todo!("FITZGEN: alloc_array") + } + + fn alloc_struct( + &mut self, + func_env: &mut FuncEnvironment<'_>, + builder: &mut FunctionBuilder<'_>, + struct_type_index: TypeIndex, + field_vals: &[ir::Value], + ) -> WasmResult { + todo!("FITZGEN: alloc_struct") + } + + fn translate_read_gc_reference( + &mut self, + func_env: &mut FuncEnvironment<'_>, + builder: &mut FunctionBuilder, + ty: WasmRefType, + src: ir::Value, + flags: ir::MemFlags, + ) -> WasmResult { + todo!("FITZGEN: translate_read_gc_reference") + } + + fn translate_write_gc_reference( + &mut self, + func_env: &mut FuncEnvironment<'_>, + builder: &mut FunctionBuilder, + ty: WasmRefType, + dst: ir::Value, + new_val: ir::Value, + flags: ir::MemFlags, + ) -> WasmResult<()> { + todo!("FITZGEN: translate_write_gc_reference") + } +} diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml index c12593ec0eff..dfad9a9d9d34 100644 --- a/crates/environ/Cargo.toml +++ b/crates/environ/Cargo.toml @@ -55,6 +55,8 @@ component-model = [ ] demangle = ['std', 'dep:rustc-demangle', 'dep:cpp_demangle'] gc = [] +gc-drc = ["gc"] +gc-null = ["gc"] compile = [ 'gimli/write', 'object/write_core', diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs index 104c716d59d2..090db85bee84 100644 --- a/crates/environ/src/builtin.rs +++ b/crates/environ/src/builtin.rs @@ -70,18 +70,18 @@ macro_rules! foreach_builtin_function { // once it will no longer be used again. (Note: `val` is not of type // `reference` because it needn't appear in any stack maps, as it // must not be live after this call.) - #[cfg(feature = "gc")] + #[cfg(feature = "gc-drc")] drop_gc_ref(vmctx: vmctx, val: i32); // Do a GC, treating the optional `root` as a GC root and returning // the updated `root` (so that, in the case of moving collectors, // callers have a valid version of `root` again). - #[cfg(feature = "gc")] + #[cfg(feature = "gc-drc")] gc(vmctx: vmctx, root: reference) -> reference; // Allocate a new, uninitialized GC object and return a reference to // it. - #[cfg(feature = "gc")] + #[cfg(feature = "gc-drc")] gc_alloc_raw( vmctx: vmctx, kind: i32, diff --git a/crates/environ/src/gc.rs b/crates/environ/src/gc.rs index 167b3517b928..173031f1d7bc 100644 --- a/crates/environ/src/gc.rs +++ b/crates/environ/src/gc.rs @@ -9,9 +9,12 @@ //! on our various `gc` cargo features is the actual garbage collection //! functions and their associated impact on binary size anyways. -#[cfg(feature = "gc")] +#[cfg(feature = "gc-drc")] pub mod drc; +#[cfg(feature = "gc-null")] +pub mod null; + use crate::prelude::*; use crate::{WasmArrayType, WasmCompositeType, WasmStorageType, WasmStructType, WasmValType}; use core::alloc::Layout; @@ -49,6 +52,98 @@ pub fn byte_size_of_wasm_ty_in_gc_heap(ty: &WasmStorageType) -> u32 { } } +/// Align `offset` up to `bytes`, updating `max_align` if `align` is the +/// new maximum alignment, and returning the aligned offset. +#[cfg(any(feature = "gc-drc", feature = "gc-null"))] +fn align_up(offset: &mut u32, max_align: &mut u32, align: u32) -> u32 { + debug_assert!(max_align.is_power_of_two()); + debug_assert!(align.is_power_of_two()); + *offset = offset.checked_add(align - 1).unwrap() & !(align - 1); + *max_align = core::cmp::max(*max_align, align); + *offset +} + +/// Define a new field of size and alignment `bytes`, updating the object's +/// total `size` and `align` as necessary. The offset of the new field is +/// returned. +#[cfg(any(feature = "gc-drc", feature = "gc-null"))] +fn field(size: &mut u32, align: &mut u32, bytes: u32) -> u32 { + let offset = align_up(size, align, bytes); + *size += bytes; + offset +} + +/// Common code to define a GC array's layout, given the size and alignment of +/// the collector's GC header and its expected offset of the array length field. +#[cfg(any(feature = "gc-drc", feature = "gc-null"))] +fn common_array_layout( + ty: &WasmArrayType, + header_size: u32, + header_align: u32, + expected_array_length_offset: u32, +) -> GcArrayLayout { + assert!(header_size >= crate::VM_GC_HEADER_SIZE); + assert!(header_align >= crate::VM_GC_HEADER_ALIGN); + + let mut size = header_size; + let mut align = header_align; + + let length_field_offset = field(&mut size, &mut align, 4); + assert_eq!(length_field_offset, expected_array_length_offset); + + let elem_size = byte_size_of_wasm_ty_in_gc_heap(&ty.0.element_type); + let elems_offset = align_up(&mut size, &mut align, elem_size); + assert_eq!(elems_offset, size); + + GcArrayLayout { + base_size: size, + align, + elem_size, + } +} + +/// Common code to define a GC struct's layout, given the size and alignment of +/// the collector's GC header and its expected offset of the array length field. +#[cfg(any(feature = "gc-drc", feature = "gc-null"))] +fn common_struct_layout( + ty: &WasmStructType, + header_size: u32, + header_align: u32, +) -> GcStructLayout { + assert!(header_size >= crate::VM_GC_HEADER_SIZE); + assert!(header_align >= crate::VM_GC_HEADER_ALIGN); + + // Process each field, aligning it to its natural alignment. + // + // We don't try and do any fancy field reordering to minimize padding + // (yet?) because (a) the toolchain probably already did that and (b) + // we're just doing the simple thing first. We can come back and improve + // things here if we find that (a) isn't actually holding true in + // practice. + let mut size = header_size; + let mut align = header_align; + + let fields = ty + .fields + .iter() + .map(|f| { + let field_size = byte_size_of_wasm_ty_in_gc_heap(&f.element_type); + field(&mut size, &mut align, field_size) + }) + .collect(); + + // Ensure that the final size is a multiple of the alignment, for + // simplicity. + let align_size_to = align; + align_up(&mut size, &mut align, align_size_to); + + GcStructLayout { + size, + align, + fields, + } +} + /// A trait for getting the layout of a Wasm GC struct or array inside a /// particular collector. pub trait GcTypeLayouts { diff --git a/crates/environ/src/gc/drc.rs b/crates/environ/src/gc/drc.rs index 3a4054df697d..5e2a5c782462 100644 --- a/crates/environ/src/gc/drc.rs +++ b/crates/environ/src/gc/drc.rs @@ -11,25 +11,6 @@ pub const HEADER_ALIGN: u32 = 8; /// The offset of the length field in a `VMDrcArrayHeader`. pub const ARRAY_LENGTH_OFFSET: u32 = HEADER_SIZE; -/// Align `offset` up to `bytes`, updating `max_align` if `align` is the -/// new maximum alignment, and returning the aligned offset. -fn align_up(offset: &mut u32, max_align: &mut u32, align: u32) -> u32 { - debug_assert!(max_align.is_power_of_two()); - debug_assert!(align.is_power_of_two()); - *offset = offset.checked_add(align - 1).unwrap() & !(align - 1); - *max_align = core::cmp::max(*max_align, align); - *offset -} - -/// Define a new field of size and alignment `bytes`, updating the object's -/// total `size` and `align` as necessary. The offset of the new field is -/// returned. -fn field(size: &mut u32, align: &mut u32, bytes: u32) -> u32 { - let offset = align_up(size, align, bytes); - *size += bytes; - offset -} - /// The layout of Wasm GC objects in the deferred reference-counting collector. #[derive(Default)] pub struct DrcTypeLayouts; @@ -40,51 +21,10 @@ impl GcTypeLayouts for DrcTypeLayouts { } fn array_layout(&self, ty: &WasmArrayType) -> GcArrayLayout { - let mut size = HEADER_SIZE; - let mut align = HEADER_ALIGN; - - let length_field_offset = field(&mut size, &mut align, 4); - debug_assert_eq!(length_field_offset, ARRAY_LENGTH_OFFSET); - - let elem_size = byte_size_of_wasm_ty_in_gc_heap(&ty.0.element_type); - let elems_offset = align_up(&mut size, &mut align, elem_size); - debug_assert_eq!(elems_offset, size); - - GcArrayLayout { - base_size: size, - align, - elem_size, - } + common_array_layout(ty, HEADER_SIZE, HEADER_ALIGN, ARRAY_LENGTH_OFFSET) } fn struct_layout(&self, ty: &WasmStructType) -> GcStructLayout { - // Process each field, aligning it to its natural alignment. - // - // We don't try and do any fancy field reordering to minimize padding - // (yet?) because (a) the toolchain probably already did that and (b) - // we're just doing the simple thing first. We can come back and improve - // things here if we find that (a) isn't actually holding true in - // practice. - let mut size = HEADER_SIZE; - let mut align = HEADER_ALIGN; - - let fields = ty - .fields - .iter() - .map(|f| { - let field_size = byte_size_of_wasm_ty_in_gc_heap(&f.element_type); - field(&mut size, &mut align, field_size) - }) - .collect(); - - // Ensure that the final size is a multiple of the alignment, for - // simplicity. - align_up(&mut size, &mut 16, align); - - GcStructLayout { - size, - align, - fields, - } + common_struct_layout(ty, HEADER_SIZE, HEADER_ALIGN) } } diff --git a/crates/environ/src/gc/null.rs b/crates/environ/src/gc/null.rs new file mode 100644 index 000000000000..e3e69bcb357e --- /dev/null +++ b/crates/environ/src/gc/null.rs @@ -0,0 +1,30 @@ +//! Layout of Wasm GC objects in the null garbage collector. + +use super::*; + +/// The size of the `VMNullHeader` header for GC objects. +pub const HEADER_SIZE: u32 = 8; + +/// The align of the `VMNullHeader` header for GC objects. +pub const HEADER_ALIGN: u32 = 8; + +/// The offset of the length field in a `VMNullArrayHeader`. +pub const ARRAY_LENGTH_OFFSET: u32 = HEADER_SIZE; + +/// The layout of Wasm GC objects in the null collector. +#[derive(Default)] +pub struct NullTypeLayouts; + +impl GcTypeLayouts for NullTypeLayouts { + fn array_length_field_offset(&self) -> u32 { + ARRAY_LENGTH_OFFSET + } + + fn array_layout(&self, ty: &WasmArrayType) -> GcArrayLayout { + common_array_layout(ty, HEADER_SIZE, HEADER_ALIGN, ARRAY_LENGTH_OFFSET) + } + + fn struct_layout(&self, ty: &WasmStructType) -> GcStructLayout { + common_struct_layout(ty, HEADER_SIZE, HEADER_ALIGN) + } +} diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index ddf317124a15..dec38a8b70ba 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -1,3 +1,5 @@ +use core::fmt; + use anyhow::{anyhow, bail, Result}; use serde_derive::{Deserialize, Serialize}; use target_lexicon::{PointerWidth, Triple}; @@ -5,6 +7,10 @@ use target_lexicon::{PointerWidth, Triple}; /// Tunable parameters for WebAssembly compilation. #[derive(Clone, Hash, Serialize, Deserialize, Debug)] pub struct Tunables { + /// The garbage collector implementation to use, which implies the layout of + /// GC objects and barriers that must be emitted in Wasm code. + pub collector: Option, + /// For static heaps, the size in bytes of virtual memory reservation for /// the heap. pub static_memory_reservation: u64, @@ -97,6 +103,8 @@ impl Tunables { /// Returns the default set of tunables for running under MIRI. pub fn default_miri() -> Tunables { Tunables { + collector: None, + // No virtual memory tricks are available on miri so make these // limits quite conservative. static_memory_reservation: 1 << 20, @@ -164,3 +172,21 @@ impl Tunables { } } } + +/// The garbage collector implementation to use. +#[derive(Clone, Copy, Hash, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum Collector { + /// The deferred reference-counting collector. + DeferredReferenceCounting, + /// The null collector. + Null, +} + +impl fmt::Display for Collector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Collector::DeferredReferenceCounting => write!(f, "deferred reference-counting"), + Collector::Null => write!(f, "null"), + } + } +} diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index bd10631b44a7..352c08f2e5df 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -114,6 +114,8 @@ default = [ 'async', 'cache', 'gc', + 'gc-drc', + 'gc-null', 'wat', 'profiling', 'parallel-compilation', @@ -247,8 +249,17 @@ runtime = [ # # When the `runtime` Cargo feature is enabled, this feature gates the ability to # load and run Wasm that uses those proposals. +# +# You can additionally configure which GC implementations are enabled via the +# `gc-drc` and `gc-null` features. gc = ["wasmtime-environ/gc", "wasmtime-cranelift?/gc"] +# Enable the deferred reference counting garbage collector. +gc-drc = ["gc", "wasmtime-environ/gc-drc", "wasmtime-cranelift?/gc-drc"] + +# Enable the null garbage collector. +gc-null = ["gc", "wasmtime-environ/gc-null", "wasmtime-cranelift?/gc-null"] + # Enable runtime support for the WebAssembly threads proposal. threads = ["wasmtime-cranelift?/threads", "std"] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 5aa1e89c9af2..e8d20aff4264 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -106,6 +106,7 @@ impl core::hash::Hash for ModuleVersionStrategy { pub struct Config { #[cfg(any(feature = "cranelift", feature = "winch"))] compiler_config: CompilerConfig, + collector: Collector, profiling_strategy: ProfilingStrategy, tunables: ConfigTunables, @@ -226,6 +227,8 @@ impl Config { tunables: ConfigTunables::default(), #[cfg(any(feature = "cranelift", feature = "winch"))] compiler_config: CompilerConfig::default(), + #[cfg(feature = "gc")] + collector: Collector::default(), #[cfg(feature = "cache")] cache_config: CacheConfig::new_cache_disabled(), profiling_strategy: ProfilingStrategy::None, @@ -1042,6 +1045,19 @@ impl Config { self } + /// Configures which garbage collector will be used for Wasm modules. + /// + /// This method can be used to configure which garbage collector + /// implementation is used for Wasm modules. For more documentation, consult + /// the [`Collector`] enumeration and its documentation. + /// + /// The default value for this is `Collector::Auto`. + #[cfg(feature = "gc")] + pub fn collector(&mut self, collector: Collector) -> &mut Self { + self.collector = collector; + self + } + /// Creates a default profiler based on the profiling strategy chosen. /// /// Profiler creation calls the type's default initializer where the purpose is @@ -2026,7 +2042,48 @@ impl Config { #[cfg(feature = "runtime")] pub(crate) fn build_gc_runtime(&self) -> Result> { - Ok(Arc::new(crate::runtime::vm::default_gc_runtime()) as Arc) + assert!( + self.features().reference_types() + || self.features().function_references() + || self.features().gc() + ); + + // TODO FITZGEN: make `Engine::gc_runtime` fallible and lazily allocate the gc runtime + + match self.collector.not_auto() { + #[cfg(feature = "gc-drc")] + Some(Collector::DeferredReferenceCounting) => { + return Ok( + Arc::new(crate::runtime::vm::DrcCollector::default()) as Arc + ) + } + #[cfg(not(feature = "gc-drc"))] + Some(Collector::DeferredReferenceCounting) => bail!( + "cannot create an engine using the deferred reference-counting \ + collector because the `gc-drc` feature was not enabled at \ + compile time", + ), + + #[cfg(feature = "gc-null")] + Some(Collector::NullCollector) => { + return Ok( + Arc::new(crate::runtime::vm::NullCollector::default()) as Arc + ) + } + #[cfg(not(feature = "gc-null"))] + Some(Collector::DeferredReferenceCounting) => bail!( + "cannot create an engine using the null collector because \ + the `gc-null` feature was not enabled at compile time", + ), + + Some(Collector::Auto) => unreachable!(), + + None => bail!( + "cannot create an engine with GC support when none of the \ + collectors are available; enable one of the following \ + features: `gc-drc`, `gc-null`", + ), + } } #[cfg(feature = "runtime")] @@ -2378,6 +2435,106 @@ impl Strategy { } } +/// Possible garbage collector implementations for Wasm. +/// +/// This is used as an argument to the [`Config::collector`] method. +/// +/// The properties of Wasmtime's available collectors are summarized in the +/// following table: +/// +/// | Collector | Collects Garbage[^1] | Latency[^2] | Throughput[^3] | Allocation Speed[^4] | Heap Utilization[^5] | +/// |-----------------------------|----------------------|-------------|----------------|----------------------|----------------------| +/// | `DeferredReferenceCounting` | Yes, but not cycles | 🙂 | 🙁 | 😐 | 😐 | +/// | `Null` | No | 🙂 | 🙂 | 🙂 | 🙂 | +/// +/// [^1]: Whether or not the collector is capable of collecting garbage and cyclic garbage. +/// +/// [^2]: How long the Wasm program is paused during garbage +/// collections. Shorter is better. In general, better latency implies +/// worse throughput and vice versa. +/// +/// [^3]: How fast the Wasm program runs when using this collector. Roughly +/// equivalent to the number of Wasm instructions executed per +/// second. Faster is better. In general, better throughput implies worse +/// latency and vice versa. +/// +/// [^4]: How fast can individual objects be allocated? +/// +/// [^5]: How many objects can the collector fit into N bytes of memory? That +/// is, how much space for bookkeeping and metadata does this collector +/// require? Less space taken up by metadata means more space for +/// additional objects. Reference counts are larger than mark bits and +/// free lists are larger than bump pointers, for example. +#[non_exhaustive] +#[derive(PartialEq, Eq, Clone, Debug, Copy)] +pub enum Collector { + /// An indicator that the garbage collector should be automatically + /// selected. + /// + /// This is generally what you want for most projects and indicates that the + /// `wasmtime` crate itself should make the decision about what the best + /// collector for a wasm module is. + /// + /// Currently this always defaults to the deferred reference-counting + /// collector, but the default value may change over time. + Auto, + + /// The deferred reference-counting collector. + /// + /// A reference-counting collector, generally trading improved latency for + /// worsened throughput. However, to avoid the largest overheads of + /// reference counting, it avoids manipulating reference counts for Wasm + /// objects on the stack. Instead, it will hold a reference count for an + /// over-approximation of all objects that are currently on the stack, trace + /// the stack during collection to find the precise set of on-stack roots, + /// and decrement the reference count of any object that was in the + /// over-approximation but not the precise set. This improves throughtput, + /// compared to "pure" reference counting, by performing many fewer + /// refcount-increment and -decrement operations. The cost is the increased + /// latency associated with tracing the stack. + /// + /// This collector cannot currently collect cycles; they will leak until the + /// GC heap's store is dropped. + DeferredReferenceCounting, + + /// The null collector. + /// + /// This collector does not actually collect any garbage. It simply + /// allocates objects until it runs out of memory, at which point further + /// objects allocation attempts will trap. + /// + /// This collector is useful for incredibly short-running Wasm instances + /// where additionally you would rather halt an over-allocating Wasm program + /// than spend time collecting its garbage to allow it to keep running. It + /// is also useful for measuring the overheads associated with other + /// collectors, as this collector imposes as close to zero throughput and + /// latency overhead as possible. + Null, +} + +impl Default for Collector { + fn default() -> Collector { + Collector::Auto + } +} + +impl Collector { + fn not_auto(&self) -> Option { + match self { + Collector::Auto => { + if cfg!(feature = "gc-drc") { + Some(Collector::DeferredReferenceCounting) + } else if cfg!(feature = "gc-null") { + Some(Collector::Null) + } else { + None + } + } + other => Some(*other), + } + } +} + /// Possible optimization levels for the Cranelift codegen backend. #[non_exhaustive] #[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index 885acbcde86f..2b45480a34c5 100644 --- a/crates/wasmtime/src/engine/serialization.rs +++ b/crates/wasmtime/src/engine/serialization.rs @@ -363,6 +363,7 @@ impl Metadata<'_> { fn check_tunables(&mut self, other: &Tunables) -> Result<()> { let Tunables { + collector, static_memory_reservation, static_memory_offset_guard_size, dynamic_memory_offset_guard_size, @@ -389,6 +390,7 @@ impl Metadata<'_> { debug_adapter_modules: _, } = self.tunables; + Self::check_collector(collector, other.collector)?; Self::check_int( static_memory_reservation, other.static_memory_reservation, @@ -591,6 +593,30 @@ impl Metadata<'_> { Ok(()) } + + fn check_collector( + module: Option, + host: Option, + ) -> Result<()> { + match (module, host) { + (None, None) => Ok(()), + (Some(module), Some(host)) if module == host => Ok(()), + + (None, Some(_)) => { + bail!("module was compiled without GC but GC is enabled in the host") + } + (Some(_), None) => { + bail!("module was compiled with GC however GC is disabled in the host") + } + + (Some(module), Some(host)) => { + bail!( + "module was compiled for the {module} collector but \ + the host is configured to use the {host} collector", + ) + } + } + } } #[cfg(test)] diff --git a/crates/wasmtime/src/runtime/vm/gc.rs b/crates/wasmtime/src/runtime/vm/gc.rs index b2f020c85cd9..de866212a610 100644 --- a/crates/wasmtime/src/runtime/vm/gc.rs +++ b/crates/wasmtime/src/runtime/vm/gc.rs @@ -26,7 +26,7 @@ use core::alloc::Layout; use core::mem::MaybeUninit; use core::ptr; use core::{any::Any, num::NonZeroUsize}; -use wasmtime_environ::{GcArrayLayout, GcStructLayout, VMGcKind, VMSharedTypeIndex}; +use wasmtime_environ::{GcArrayLayout, GcStructLayout, GcTypeLayouts, VMGcKind, VMSharedTypeIndex}; /// GC-related data that is one-to-one with a `wasmtime::Store`. /// @@ -257,12 +257,25 @@ impl GcStore { } } +struct DisabledCollector; + +unsafe impl GcRuntime for DisabledCollector { + fn new_gc_heap(&self) -> Result> { + Ok(Box::new(DisabledGcHeap) as Box) + } + + fn layouts(&self) -> &dyn GcTypeLayouts { + unreachable!() + } +} + /// Get a no-op GC heap for when GC is disabled (either statically at compile /// time or dynamically due to it being turned off in the `wasmtime::Config`). pub fn disabled_gc_heap() -> Box { return Box::new(DisabledGcHeap); } +#[derive(Default)] pub(crate) struct DisabledGcHeap; unsafe impl GcHeap for DisabledGcHeap { diff --git a/crates/wasmtime/src/runtime/vm/gc/disabled.rs b/crates/wasmtime/src/runtime/vm/gc/disabled.rs index 245631605f0f..6edec9c9cfb4 100644 --- a/crates/wasmtime/src/runtime/vm/gc/disabled.rs +++ b/crates/wasmtime/src/runtime/vm/gc/disabled.rs @@ -15,25 +15,6 @@ pub fn default_gc_runtime() -> impl GcRuntime { DisabledCollector } -struct DisabledCollector; - -unsafe impl GcRuntime for DisabledCollector { - fn new_gc_heap(&self) -> Result> { - unreachable!() - } - - fn layouts(&self) -> &dyn GcTypeLayouts { - unreachable!() - } -} - -pub enum VMExternRef {} - -pub enum VMEqRef {} - -pub enum VMStructRef {} - -pub enum VMArrayRef {} pub struct VMGcObjectDataMut<'a> { inner: VMStructRef, diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled.rs b/crates/wasmtime/src/runtime/vm/gc/enabled.rs index 0a7466537608..01894771fcf3 100644 --- a/crates/wasmtime/src/runtime/vm/gc/enabled.rs +++ b/crates/wasmtime/src/runtime/vm/gc/enabled.rs @@ -2,17 +2,25 @@ mod arrayref; mod data; -mod drc; mod externref; mod free_list; mod structref; pub use arrayref::*; pub use data::*; -pub use drc::*; pub use externref::*; pub use structref::*; +#[cfg(feature = "gc-drc")] +mod drc; +#[cfg(feature = "gc-drc")] +pub use drc::*; + +#[cfg(feature = "gc-null")] +mod null; +#[cfg(feature = "gc-null")] +pub use null::*; + use crate::runtime::vm::GcRuntime; /// Get the default GC runtime. diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled/null.rs b/crates/wasmtime/src/runtime/vm/gc/enabled/null.rs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/wasmtime/src/runtime/vm/libcalls.rs b/crates/wasmtime/src/runtime/vm/libcalls.rs index 3652fa754725..151c2cc8147e 100644 --- a/crates/wasmtime/src/runtime/vm/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/libcalls.rs @@ -411,7 +411,7 @@ unsafe fn table_get_lazy_init_func_ref( } /// Drop a GC reference. -#[cfg(feature = "gc")] +#[cfg(feature = "gc-drc")] unsafe fn drop_gc_ref(instance: &mut Instance, gc_ref: u32) { log::trace!("libcalls::drop_gc_ref({gc_ref:#x})"); let gc_ref = VMGcRef::from_raw_u32(gc_ref).expect("non-null VMGcRef"); @@ -422,7 +422,7 @@ unsafe fn drop_gc_ref(instance: &mut Instance, gc_ref: u32) { /// Do a GC, keeping `gc_ref` rooted and returning the updated `gc_ref` /// reference. -#[cfg(feature = "gc")] +#[cfg(feature = "gc-drc")] unsafe fn gc(instance: &mut Instance, gc_ref: u32) -> Result { let gc_ref = VMGcRef::from_raw_u32(gc_ref); let gc_ref = gc_ref.map(|r| (*instance.store()).unwrap_gc_store_mut().clone_gc_ref(&r)); @@ -455,7 +455,7 @@ unsafe fn gc(instance: &mut Instance, gc_ref: u32) -> Result { /// Allocate a raw, unininitialized GC object for Wasm code. /// /// The Wasm code is responsible for initializing the object. -#[cfg(feature = "gc")] +#[cfg(feature = "gc-drc")] unsafe fn gc_alloc_raw( instance: &mut Instance, kind: u32,