wasmtime: Implement table.get and table.set

These instructions have fast, inline JIT paths for the common cases, and only
call out to host VM functions for the slow paths. This required some changes to
`cranelift-wasm`'s `FuncEnvironment`: instead of taking a `FuncCursor` to insert
an instruction sequence within the current basic block,
`FuncEnvironment::translate_table_{get,set}` now take a `&mut FunctionBuilder`
so that they can create whole new basic blocks. This is necessary for
implementing GC read/write barriers that involve branching (e.g. checking for
null, or whether a store buffer is at capacity).

Furthermore, it required that the `load`, `load_complex`, and `store`
instructions handle loading and storing through an `r{32,64}` rather than just
`i{32,64}` addresses. This involved making `r{32,64}` types acceptable
instantiations of the `iAddr` type variable, plus a few new instruction
encodings.

Part of #929
This commit is contained in:
Nick Fitzgerald
2020-06-23 10:43:08 -07:00
parent 959e424c81
commit 8c5f59c0cf
20 changed files with 894 additions and 76 deletions

View File

@@ -3,6 +3,26 @@ use std::cell::Cell;
use std::rc::Rc;
use wasmtime::*;
struct SetFlagOnDrop(Rc<Cell<bool>>);
impl Drop for SetFlagOnDrop {
fn drop(&mut self) {
self.0.set(true);
}
}
struct GcOnDrop {
store: Store,
gc_count: Rc<Cell<usize>>,
}
impl Drop for GcOnDrop {
fn drop(&mut self) {
self.store.gc();
self.gc_count.set(self.gc_count.get() + 1);
}
}
#[test]
fn smoke_test_gc() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
@@ -57,15 +77,7 @@ fn smoke_test_gc() -> anyhow::Result<()> {
drop(r);
assert!(inner_dropped.get());
return Ok(());
struct SetFlagOnDrop(Rc<Cell<bool>>);
impl Drop for SetFlagOnDrop {
fn drop(&mut self) {
self.0.set(true);
}
}
Ok(())
}
#[test]
@@ -210,3 +222,301 @@ fn many_live_refs() -> anyhow::Result<()> {
}
}
}
#[test]
fn drop_externref_via_table_set() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
r#"
(module
(table $t 1 externref)
(func (export "table-set") (param externref)
(table.set $t (i32.const 0) (local.get 0))
)
)
"#,
)?;
let instance = Instance::new(&store, &module, &[])?;
let table_set = instance.get_func("table-set").unwrap();
let foo_is_dropped = Rc::new(Cell::new(false));
let bar_is_dropped = Rc::new(Cell::new(false));
let foo = ExternRef::new(SetFlagOnDrop(foo_is_dropped.clone()));
let bar = ExternRef::new(SetFlagOnDrop(bar_is_dropped.clone()));
{
let args = vec![Val::ExternRef(Some(foo))];
table_set.call(&args)?;
}
store.gc();
assert!(!foo_is_dropped.get());
assert!(!bar_is_dropped.get());
{
let args = vec![Val::ExternRef(Some(bar))];
table_set.call(&args)?;
}
store.gc();
assert!(foo_is_dropped.get());
assert!(!bar_is_dropped.get());
table_set.call(&[Val::ExternRef(None)])?;
assert!(foo_is_dropped.get());
assert!(bar_is_dropped.get());
Ok(())
}
#[test]
fn gc_in_externref_dtor() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
r#"
(module
(table $t 1 externref)
(func (export "table-set") (param externref)
(table.set $t (i32.const 0) (local.get 0))
)
)
"#,
)?;
let instance = Instance::new(&store, &module, &[])?;
let table_set = instance.get_func("table-set").unwrap();
let gc_count = Rc::new(Cell::new(0));
// Put a `GcOnDrop` into the table.
{
let args = vec![Val::ExternRef(Some(ExternRef::new(GcOnDrop {
store: store.clone(),
gc_count: gc_count.clone(),
})))];
table_set.call(&args)?;
}
// Remove the `GcOnDrop` from the `VMExternRefActivationsTable`.
store.gc();
// Overwrite the `GcOnDrop` table element, causing it to be dropped, and
// triggering a GC.
table_set.call(&[Val::ExternRef(None)])?;
assert_eq!(gc_count.get(), 1);
Ok(())
}
#[test]
fn touch_own_table_element_in_externref_dtor() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
r#"
(module
(table $t (export "table") 1 externref)
(func (export "table-set") (param externref)
(table.set $t (i32.const 0) (local.get 0))
)
)
"#,
)?;
let instance = Instance::new(&store, &module, &[])?;
let table = instance.get_table("table").unwrap();
let table_set = instance.get_func("table-set").unwrap();
let touched = Rc::new(Cell::new(false));
{
let args = vec![Val::ExternRef(Some(ExternRef::new(TouchTableOnDrop {
table,
touched: touched.clone(),
})))];
table_set.call(&args)?;
}
// Remove the `TouchTableOnDrop` from the `VMExternRefActivationsTable`.
store.gc();
table_set.call(&[Val::ExternRef(Some(ExternRef::new("hello".to_string())))])?;
assert!(touched.get());
return Ok(());
struct TouchTableOnDrop {
table: Table,
touched: Rc<Cell<bool>>,
}
impl Drop for TouchTableOnDrop {
fn drop(&mut self) {
// From the `Drop` implementation, we see the new table element, not
// `self`.
let elem = self.table.get(0).unwrap().unwrap_externref().unwrap();
assert!(elem.data().is::<String>());
assert_eq!(elem.data().downcast_ref::<String>().unwrap(), "hello");
self.touched.set(true);
}
}
}
#[test]
fn gc_during_gc_when_passing_refs_into_wasm() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
r#"
(module
(table $t 1 externref)
(func (export "f") (param externref)
(table.set $t (i32.const 0) (local.get 0))
)
)
"#,
)?;
let instance = Instance::new(&store, &module, &[])?;
let f = instance.get_func("f").unwrap();
let gc_count = Rc::new(Cell::new(0));
for _ in 0..1024 {
let args = vec![Val::ExternRef(Some(ExternRef::new(GcOnDrop {
store: store.clone(),
gc_count: gc_count.clone(),
})))];
f.call(&args)?;
}
f.call(&[Val::ExternRef(None)])?;
store.gc();
assert_eq!(gc_count.get(), 1024);
Ok(())
}
#[test]
fn gc_during_gc_from_many_table_gets() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
r#"
(module
(import "" "" (func $observe_ref (param externref)))
(table $t 1 externref)
(func (export "init") (param externref)
(table.set $t (i32.const 0) (local.get 0))
)
(func (export "run") (param i32)
(loop $continue
(if (i32.eqz (local.get 0)) (return))
(call $observe_ref (table.get $t (i32.const 0)))
(local.set 0 (i32.sub (local.get 0) (i32.const 1)))
(br $continue)
)
)
)
"#,
)?;
let observe_ref = Func::new(
&store,
FuncType::new(
vec![ValType::ExternRef].into_boxed_slice(),
vec![].into_boxed_slice(),
),
|_caller, _params, _results| Ok(()),
);
let instance = Instance::new(&store, &module, &[observe_ref.into()])?;
let init = instance.get_func("init").unwrap();
let run = instance.get_func("run").unwrap();
let gc_count = Rc::new(Cell::new(0));
// Initialize the table element with a `GcOnDrop`. This also puts it in the
// `VMExternRefActivationsTable`.
{
let args = vec![Val::ExternRef(Some(ExternRef::new(GcOnDrop {
store: store.clone(),
gc_count: gc_count.clone(),
})))];
init.call(&args)?;
}
// Overwrite the `GcOnDrop` with another reference. The `GcOnDrop` is still
// in the `VMExternRefActivationsTable`.
{
let args = vec![Val::ExternRef(Some(ExternRef::new(String::from("hello"))))];
init.call(&args)?;
}
// Now call `run`, which does a bunch of `table.get`s, filling up the
// `VMExternRefActivationsTable`'s bump region, and eventually triggering a
// GC that will deallocate our `GcOnDrop` which will also trigger a nested
// GC.
run.call(&[Val::I32(1024)])?;
// We should have done our nested GC.
assert_eq!(gc_count.get(), 1);
Ok(())
}
#[test]
fn pass_externref_into_wasm_during_destructor_in_gc() -> anyhow::Result<()> {
let (store, module) = ref_types_module(
r#"
(module
(table $t 1 externref)
(func (export "f") (param externref)
nop
)
)
"#,
)?;
let instance = Instance::new(&store, &module, &[])?;
let f = instance.get_func("f").unwrap();
let r = ExternRef::new("hello");
let did_call = Rc::new(Cell::new(false));
// Put a `CallOnDrop` into the `VMExternRefActivationsTable`.
{
let args = vec![Val::ExternRef(Some(ExternRef::new(CallOnDrop(
f.clone(),
r.clone(),
did_call.clone(),
))))];
f.call(&args)?;
}
// One ref count for `r`, one for the `CallOnDrop`.
assert_eq!(r.strong_count(), 2);
// Do a GC, which will see that the only reference holding the `CallOnDrop`
// is the `VMExternRefActivationsTable`, and will drop it. Dropping it will
// cause it to call into `f` again.
store.gc();
assert!(did_call.get());
// The `CallOnDrop` is no longer holding onto `r`, but the
// `VMExternRefActivationsTable` is.
assert_eq!(r.strong_count(), 2);
// GC again to empty the `VMExternRefActivationsTable`. Now `r` is the only
// thing holding its `externref` alive.
store.gc();
assert_eq!(r.strong_count(), 1);
return Ok(());
struct CallOnDrop(Func, ExternRef, Rc<Cell<bool>>);
impl Drop for CallOnDrop {
fn drop(&mut self) {
self.0
.call(&[Val::ExternRef(Some(self.1.clone()))])
.unwrap();
self.2.set(true);
}
}
}

View File

@@ -0,0 +1,35 @@
(module
(table $t 1 externref)
(func (export "init") (param externref)
(table.set $t (i32.const 0) (local.get 0))
)
(func (export "get-many-externrefs") (param $i i32)
(loop $continue
;; Exit when our loop counter `$i` reaches zero.
(if (i32.eqz (local.get $i))
(return)
)
;; Get an `externref` out of the table. This could cause the
;; `VMExternRefActivationsTable`'s bump region to reach full capacity,
;; which triggers a GC.
;;
;; Set the table element back into the table, just so that the element is
;; still considered live at the time of the `table.get`, it ends up in the
;; stack map, and we poke more of our GC bits.
(table.set $t (i32.const 0) (table.get $t (i32.const 0)))
;; Decrement our loop counter `$i`.
(local.set $i (i32.sub (local.get $i) (i32.const 1)))
;; Continue to the next loop iteration.
(br $continue)
)
unreachable
)
)
(invoke "init" (ref.extern 1))
(invoke "get-many-externrefs" (i32.const 8192))