Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
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: i64 = 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