Skip to content
11 changes: 4 additions & 7 deletions crates/cranelift/src/func_environ/gc/drc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ use wasmtime_environ::{
WasmValType, drc::DrcTypeLayouts,
};

// The minimum over-approximated stack roots list size for which we will trigger
// a GC.
const MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD: i64 = 1024;

#[derive(Default)]
pub struct DrcCompiler {
layouts: DrcTypeLayouts,
Expand Down Expand Up @@ -190,9 +186,10 @@ impl DrcCompiler {
);

let doubled_last_len = builder.ins().iadd(last_len, last_len);
let min_threshold = builder
.ins()
.iconst(ir::types::I32, MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD);
let min_threshold = builder.ins().iconst(
ir::types::I32,
i64::from(wasmtime_environ::drc::MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD),
);
let threshold = builder.ins().umax(doubled_last_len, min_threshold);

let should_gc =
Expand Down
2 changes: 1 addition & 1 deletion crates/cranelift/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ mod trap;
use self::compiler::Compiler;

const TRAP_INTERNAL_ASSERT: TrapCode = TrapCode::unwrap_user(1);
const TRAP_GC_HEAP_CORRUPT: TrapCode = TrapCode::unwrap_user(2);
pub const TRAP_GC_HEAP_CORRUPT: TrapCode = TrapCode::unwrap_user(2);
const TRAP_OFFSET: u8 = 3;
pub const TRAP_CANNOT_LEAVE_COMPONENT: TrapCode =
TrapCode::unwrap_user(Trap::CannotLeaveComponent as u8 + TRAP_OFFSET);
Expand Down
13 changes: 13 additions & 0 deletions crates/environ/src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ use core::alloc::Layout;
/// enabled.
pub const POISON: u8 = 0b00001111;

/// The bit within a `VMDrcHeader`'s reserved bits that is the mark
/// bit. Collectively, this bit in all the heap's objects' headers implements
/// the precise-stack-roots set.
pub const DRC_HEADER_MARK_BIT: u32 = 1 << 0;

/// The bit within a `VMDrcHeader`'s reserved bits that is the
/// in-the-over-approximated-stack-roots list bit.
pub const DRC_HEADER_IN_OVER_APPROX_LIST_BIT: u32 = 1 << 1;

/// The minimum length the over-approximated-stack-roots list must reach
/// before a read barrier considers forcing a GC.
pub const DRC_MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD: u32 = 1024;

/// Assert a condition, but only when `gc_zeal` is enabled.
#[macro_export]
macro_rules! gc_assert {
Expand Down
11 changes: 3 additions & 8 deletions crates/environ/src/gc/drc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ pub const EXCEPTION_TAG_INSTANCE_OFFSET: u32 = HEADER_SIZE;
/// The offset of the tag-defined-index field in an exception header.
pub const EXCEPTION_TAG_DEFINED_OFFSET: u32 = HEADER_SIZE + 4;

/// The bit within a `VMDrcHeader`'s reserved bits that is the mark
/// bit. Collectively, this bit in all the heap's objects' headers implements
/// the precise-stack-roots set.
pub const HEADER_MARK_BIT: u32 = 1 << 0;

/// The bit within a `VMDrcHeader`'s reserved bits that is the
/// in-the-over-approximated-stack-roots list bit.
pub const HEADER_IN_OVER_APPROX_LIST_BIT: u32 = 1 << 1;
pub use super::DRC_HEADER_IN_OVER_APPROX_LIST_BIT as HEADER_IN_OVER_APPROX_LIST_BIT;
pub use super::DRC_HEADER_MARK_BIT as HEADER_MARK_BIT;
pub use super::DRC_MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD as MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD;

/// The layout of Wasm GC objects in the deferred reference-counting collector.
#[derive(Default)]
Expand Down
10 changes: 0 additions & 10 deletions crates/test-util/src/wast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,16 +562,6 @@ impl WastTest {
return true;
}

// Winch supports GC types only under the barrier-free collectors;
// the deferred reference-counting collector is refused until Winch
// emits GC barriers.
if config.compiler == Compiler::Winch
&& config.collector == Collector::DeferredReferenceCounting
&& self.config.gc_types()
{
return true;
}

// Disable spec tests per target for proposals that Winch does not implement yet.
if config.compiler == Compiler::Winch {
// Common list for tests that fail in all targets supported by Winch.
Expand Down
7 changes: 0 additions & 7 deletions crates/wasmtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2458,13 +2458,6 @@ impl Config {
| WasmFeatures::LEGACY_EXCEPTIONS
| WasmFeatures::STACK_SWITCHING;

// Winch supports GC types only under the barrier-free
// collectors; the deferred reference-counting collector
// requires GC barriers that Winch does not emit yet.
#[cfg(feature = "gc")]
if self.collector.not_auto() == Some(Collector::DeferredReferenceCounting) {
unsupported |= WasmFeatures::GC_TYPES;
}
match self.compiler_target().architecture {
target_lexicon::Architecture::Aarch64(_) => {
unsupported |= WasmFeatures::THREADS;
Expand Down
9 changes: 5 additions & 4 deletions docs/stability-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,11 @@ emitting Pulley bytecode.
| [`exception-handling`] | ✅ | ❌ |
| [`stack-switching`] | ❌ | ❌ |

[^a]: Winch supports some features of the [`reference-types`] proposal such as
the change to support multiple tables and LEB-encoding table indices in
instructions, but it does not support GC types such as `externref` or the
new table opcodes in the [`reference-types`] proposal.
[^a]: Winch supports GC reference values such as `externref` through parameters,
results, locals, globals, and calls, including the required stack maps and
collector barriers. It also supports multiple tables and LEB-encoded table
indices, but does not yet support every table and element-segment case in the
[`reference-types`] proposal.
[^b]: Pulley does not support the [`threads`] proposal because there is no known
safe way to implement this with Rust's memory model.
[^c]: Winch's support for aarch64 is complete for Core Wasm.
Expand Down
211 changes: 211 additions & 0 deletions tests/all/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3839,6 +3839,217 @@ fn winch_externref_survives_gc_in_frame() -> Result<()> {
Ok(())
}

/// The write barrier's decrement chain releases an object once a global stops
/// holding the last reference to it.
#[test]
#[cfg_attr(miri, ignore)]
fn winch_drc_write_barrier_drops_old_global_value() -> Result<()> {
let mut config = Config::new();
config.strategy(Strategy::Winch);
config.collector(Collector::DeferredReferenceCounting);
let Ok(engine) = Engine::new(&config) else {
return Ok(());
};
let module = Module::new(
&engine,
r#"
(module
(global $g (mut externref) (ref.null extern))
(func (export "set") (param externref)
(global.set $g (local.get 0))))
"#,
)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])?;
let set = instance.get_func(&mut store, "set").unwrap();

let dropped = Arc::new(AtomicBool::new(false));
{
let mut scope = RootScope::new(&mut store);
let r = ExternRef::new(&mut scope, SetFlagOnDrop(dropped.clone()))?;
set.call(&mut scope, &[Val::ExternRef(Some(r))], &mut [])?;
}

// The global holds the only reference; nothing may be dropped yet.
store.gc(None)?;
assert!(!dropped.load(SeqCst));

// Overwriting the global decrements the count to zero and releases the
// old value.
set.call(&mut store, &[Val::ExternRef(None)], &mut [])?;
store.gc(None)?;
assert!(dropped.load(SeqCst));

Ok(())
}

/// The read barrier holds a count for references entering the stack, so
/// overwriting their last long-lived home cannot free them out from under
/// the frame that loaded them.
#[test]
#[cfg_attr(miri, ignore)]
fn winch_drc_read_barrier_keeps_loaded_ref_alive() -> Result<()> {
let mut config = Config::new();
config.strategy(Strategy::Winch);
config.collector(Collector::DeferredReferenceCounting);
let Ok(engine) = Engine::new(&config) else {
return Ok(());
};
let module = Module::new(
&engine,
r#"
(module
(import "" "gc" (func $gc))
(global $g (mut externref) (ref.null extern))
(func (export "set") (param externref)
(global.set $g (local.get 0)))
(func (export "swap") (result externref)
(local $tmp externref)
(local.set $tmp (global.get $g))
(global.set $g (ref.null extern))
(call $gc)
(local.get $tmp)))
"#,
)?;
let mut store = Store::new(&engine, ());
let gc = Func::wrap(&mut store, |mut cx: Caller<'_, ()>| {
let _ = cx.gc(None);
});
let instance = Instance::new(&mut store, &module, &[gc.into()])?;
let set = instance.get_func(&mut store, "set").unwrap();
let swap = instance.get_typed_func::<(), Option<Rooted<ExternRef>>>(&mut store, "swap")?;

{
let mut scope = RootScope::new(&mut store);
let r = ExternRef::new(&mut scope, 0xDECAFu32)?;
set.call(&mut scope, &[Val::ExternRef(Some(r))], &mut [])?;
}

// Settle the deferred unroot so the global truly holds the last count.
store.gc(None)?;

// `swap` loads the reference onto the stack, overwrites the global, and
// collects while the stack copy is live.
let out = swap.call(&mut store, ())?.expect("must not be null");
let got = out
.data(&store)?
.and_then(|d| d.downcast_ref::<u32>().copied());
assert_eq!(got, Some(0xDECAF));

Ok(())
}

/// An `externref` may wrap an unboxed i31 created through the host API. Such
/// values do not have reference counts and must bypass both DRC barriers.
#[test]
#[cfg_attr(miri, ignore)]
fn winch_drc_i31_wrapped_as_externref_skips_global_barriers() -> Result<()> {
let mut config = Config::new();
config.strategy(Strategy::Winch);
config.collector(Collector::DeferredReferenceCounting);
let Ok(engine) = Engine::new(&config) else {
return Ok(());
};
let module = Module::new(
&engine,
r#"
(module
(global $g (mut externref) (ref.null extern))
(func (export "set") (param externref)
local.get 0
global.set $g)
(func (export "get") (result externref)
global.get $g))
"#,
)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])?;
let set = instance.get_func(&mut store, "set").unwrap();
let get = instance.get_func(&mut store, "get").unwrap();

let anyref = AnyRef::from_i31(&mut store, I31::wrapping_u32(0x1234));
let externref = ExternRef::convert_any(&mut store, anyref)?;
set.call(&mut store, &[Val::ExternRef(Some(externref))], &mut [])?;

let mut results = [Val::null_extern_ref()];
get.call(&mut store, &[], &mut results)?;
let externref = results[0]
.unwrap_externref()
.expect("global.get returned null");
let anyref = AnyRef::convert_extern(&mut store, *externref)?;
assert_eq!(anyref.unwrap_i31(&store)?.get_u32(), 0x1234);

// Replacing the i31-backed externref exercises the old-value side of the
// write barrier as well.
set.call(&mut store, &[Val::null_extern_ref()], &mut [])?;
Ok(())
}

/// Growing the over-approximated-stack-roots list to its threshold forces a
/// collection from the read barrier and preserves the reference whose load
/// triggered that collection.
#[test]
#[cfg_attr(miri, ignore)]
fn winch_drc_read_barrier_forces_gc_at_threshold() -> Result<()> {
let mut config = Config::new();
config.strategy(Strategy::Winch);
config.collector(Collector::DeferredReferenceCounting);
let Ok(engine) = Engine::new(&config) else {
return Ok(());
};

let num_refs = wasmtime_environ::DRC_MIN_OVER_APPROX_STACK_ROOTS_GC_THRESHOLD as usize;
let mut wat = "(module\n".to_string();
for i in 0..num_refs {
wat.push_str(&format!(
r#"(global $g{i} (export "g{i}") (mut externref) (ref.null extern))
"#,
));
}
wat.push_str("(func (export \"drain\")\n");
for i in 0..num_refs {
wat.push_str(&format!(
"(drop (global.get $g{i}))\n(global.set $g{i} (ref.null extern))\n"
));
}
wat.push_str("))");

let module = Module::new(&engine, &wat)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])?;
let dropped = Arc::new(AtomicUsize::new(0));

{
let mut scope = RootScope::new(&mut store);
for i in 0..num_refs {
let gc_ref = ExternRef::new(&mut scope, CountDrops(dropped.clone()))?;
let global = instance
.get_global(&mut scope, &format!("g{i}"))
.expect("global must be exported");
global.set(&mut scope, Val::ExternRef(Some(gc_ref)))?;
}
}

// Settle the host roots. Each global is now the only long-lived home for
// its reference.
store.gc(None)?;
assert_eq!(dropped.load(SeqCst), 0);

let drain = instance.get_typed_func::<(), ()>(&mut store, "drain")?;
drain.call(&mut store, ())?;

// The final global.get reaches the threshold and forces a collection.
// The preceding references are no longer on the stack or in globals, but
// the triggering reference is still live in the global.get result slot.
assert_eq!(dropped.load(SeqCst), num_refs - 1);

// The triggering reference becomes collectible after `drain` returns.
store.gc(None)?;
assert_eq!(dropped.load(SeqCst), num_refs);

Ok(())
}

/// Reference values crossing the ABI boundary in every position: stack-passed
/// externref params and multi-value externref results (more than fit in registers)
#[test]
Expand Down
38 changes: 0 additions & 38 deletions tests/all/winch_engine_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,41 +64,3 @@ fn ensure_compatibility_between_winch_and_debug_native(config: &mut Config) -> R

Ok(())
}

#[wasmtime_test(strategies(only(Winch)))]
#[cfg_attr(miri, ignore)]
fn ensure_compatibility_between_winch_and_drc_collector(config: &mut Config) -> Result<()> {
config.collector(Collector::DeferredReferenceCounting);
config.gc_support(true);
let result = Engine::new(&config);
match result {
Ok(_) => {
wasmtime::bail!(
"Expected incompatibility between the deferred reference-counting \
collector and Winch"
)
}
Err(e) => {
assert_eq!(
e.to_string(),
"the wasm_gc_types feature is not supported on this compiler configuration"
);
}
}

Ok(())
}

#[wasmtime_test(strategies(only(Winch)))]
#[cfg_attr(miri, ignore)]
fn winch_with_drc_collector_disables_gc_types_by_default(config: &mut Config) -> Result<()> {
config.collector(Collector::DeferredReferenceCounting);
let engine = Engine::new(&config)?;
let result = Module::new(
&engine,
r#"(module (global (mut externref) (ref.null extern)))"#,
);
assert!(result.is_err());

Ok(())
}
Loading
Loading