Skip to content

Commit

Permalink
Define garbage collection rooting APIs (#8011)
Browse files Browse the repository at this point in the history
* Define garbage collection rooting APIs

Rooting prevents GC objects from being collected while they are actively being
used.

We have a few sometimes-conflicting goals with our GC rooting APIs:

1. Safety: It should never be possible to get a use-after-free bug because the
   user misused the rooting APIs, the collector "mistakenly" determined an
   object was unreachable and collected it, and then the user tried to access
   the object. This is our highest priority.

2. Moving GC: Our rooting APIs should moving collectors (such as generational
   and compacting collectors) where an object might get relocated after a
   collection and we need to update the GC root's pointer to the moved
   object. This means we either need cooperation and internal mutability from
   individual GC roots as well as the ability to enumerate all GC roots on the
   native Rust stack, or we need a level of indirection.

3. Performance: Our rooting APIs should generally be as low-overhead as
   possible. They definitely shouldn't require synchronization and locking to
   create, access, and drop GC roots.

4. Ergonomics: Our rooting APIs should be, if not a pleasure, then at least not
   a burden for users. Additionally, the API's types should be `Sync` and `Send`
   so that they work well with async Rust.

For example, goals (3) and (4) are in conflict when we think about how to
support (2). Ideally, for ergonomics, a root would automatically unroot itself
when dropped. But in the general case that requires holding a reference to the
store's root set, and that root set needs to be held simultaneously by all GC
roots, and they each need to mutate the set to unroot themselves. That implies
`Rc<RefCell<...>>` or `Arc<Mutex<...>>`! The former makes the store and GC root
types not `Send` and not `Sync`. The latter imposes synchronization and locking
overhead. So we instead make GC roots indirect and require passing in a store
context explicitly to unroot in the general case. This trades worse ergonomics
for better performance and support for moving GC and async Rust.

Okay, with that out of the way, this module provides two flavors of rooting
API. One for the common, scoped lifetime case, and another for the rare case
where we really need a GC root with an arbitrary, non-LIFO/non-scoped lifetime:

1. `RootScope` and `Rooted<T>`: These are used for temporarily rooting GC
   objects for the duration of a scope. Upon exiting the scope, they are
   automatically unrooted. The internal implementation takes advantage of the
   LIFO property inherent in scopes, making creating and dropping `Rooted<T>`s
   and `RootScope`s super fast and roughly equivalent to bump allocation.

   This type is vaguely similar to V8's [`HandleScope`].

   [`HandleScope`]: https://v8.github.io/api/head/classv8_1_1HandleScope.html

   Note that `Rooted<T>` can't be statically tied to its context scope via a
   lifetime parameter, unfortunately, as that would allow the creation and use
   of only one `Rooted<T>` at a time, since the `Rooted<T>` would take a borrow
   of the whole context.

   This supports the common use case for rooting and provides good ergonomics.

2. `ManuallyRooted<T>`: This is the fully general rooting API used for holding
   onto non-LIFO GC roots with arbitrary lifetimes. However, users must manually
   unroot them. Failure to manually unroot a `ManuallyRooted<T>` before it is
   dropped will result in the GC object (and everything it transitively
   references) leaking for the duration of the `Store`'s lifetime.

   This type is roughly similar to SpiderMonkey's [`PersistentRooted<T>`],
   although they avoid the manual-unrooting with internal mutation and shared
   references. (Our constraints mean we can't do those things, as mentioned
   explained above.)

   [`PersistentRooted<T>`]: http://devdoc.net/web/developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/JS::PersistentRooted.html

At the end of the day, both `Rooted<T>` and `ManuallyRooted<T>` are just tagged
indices into the store's `RootSet`. This indirection allows working with Rust's
borrowing discipline (we use `&mut Store` to represent mutable access to the GC
heap) while still allowing rooted references to be moved around without tying up
the whole store in borrows. Additionally, and crucially, this indirection allows
us to update the *actual* GC pointers in the `RootSet` and support moving GCs
(again, as mentioned above).

* Reorganize GC-related submodules in `wasmtime-runtime`

* Reorganize GC-related submodules in `wasmtime`

* Use `Into<StoreContext[Mut]<'a, T>` for `Externref::data[_mut]` methods

* Run rooting tests under MIRI

* Make `into_abi` take an `AutoAssertNoGc`

* Don't use atomics to update externref ref counts anymore

* Try to make lifetimes/safety more-obviously correct

Remove some transmute methods, assert that `VMExternRef`s are the only valid
`VMGcRef`, etc.

* Update extenref constructor examples

* Make `GcRefImpl::transmute_ref` a non-default trait method

* Make inline fast paths for GC LIFO scopes

* Make `RootSet::unroot_gc_ref` an `unsafe` function

* Move Hash and Eq for Rooted, move to impl methods

* Remove type parameter from `AutoAssertNoGc`

Just wrap a `&mut StoreOpaque` directly.

* Make a bunch of internal `ExternRef` methods that deal with raw `VMGcRef`s take `AutoAssertNoGc` instead of `StoreOpaque`

* Fix compile after rebase

* rustfmt

* revert unrelated egraph changes

* Fix non-gc build

* Mark `AutoAssertNoGc` methods inline

* review feedback

* Temporarily remove externref support from the C API

Until we can add proper GC rooting.

* Remove doxygen reference to temp deleted function

* Remove need to `allow(private_interfaces)`

* Fix call benchmark compilation
  • Loading branch information
fitzgen authored Mar 6, 2024
1 parent ebbfc90 commit bd2ea90
Show file tree
Hide file tree
Showing 50 changed files with 3,617 additions and 1,250 deletions.
4 changes: 2 additions & 2 deletions benches/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ fn bench_host_to_wasm<Params, Results>(
let mut space = vec![ValRaw::i32(0); params.len().max(results.len())];
b.iter(|| unsafe {
for (i, param) in params.iter().enumerate() {
space[i] = param.to_raw(&mut *store);
space[i] = param.to_raw(&mut *store).unwrap();
}
untyped
.call_unchecked(&mut *store, space.as_mut_ptr(), space.len())
Expand Down Expand Up @@ -348,7 +348,7 @@ fn wasm_to_host(c: &mut Criterion) {
Val::I64(0) => {}
_ => unreachable!(),
}
space[0] = Val::F32(0).to_raw(&mut caller);
space[0] = Val::F32(0).to_raw(&mut caller).unwrap();
Ok(())
})
.unwrap();
Expand Down
4 changes: 1 addition & 3 deletions crates/c-api/include/wasmtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,7 @@
* provided access to it. For example in a host function created with
* #wasmtime_func_new you can use #wasmtime_context_t in the host function
* callback. This is because an argument, a #wasmtime_caller_t, provides access
* to #wasmtime_context_t. On the other hand a destructor passed to
* #wasmtime_externref_new, however, cannot use a #wasmtime_context_t because
* it was not provided access to one. Doing so may lead to memory unsafety.
* to #wasmtime_context_t.
*
* ### Stores
*
Expand Down
105 changes: 7 additions & 98 deletions crates/c-api/include/wasmtime/val.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,81 +14,6 @@
extern "C" {
#endif

/**
* \typedef wasmtime_externref_t
* \brief Convenience alias for #wasmtime_externref
*
* \struct wasmtime_externref
* \brief A host-defined un-forgeable reference to pass into WebAssembly.
*
* This structure represents an `externref` that can be passed to WebAssembly.
* It cannot be forged by WebAssembly itself and is guaranteed to have been
* created by the host.
*/
typedef struct wasmtime_externref wasmtime_externref_t;

/**
* \brief Create a new `externref` value.
*
* Creates a new `externref` value wrapping the provided data, returning the
* pointer to the externref.
*
* \param data the host-specific data to wrap
* \param finalizer an optional finalizer for `data`
*
* When the reference is reclaimed, the wrapped data is cleaned up with the
* provided `finalizer`.
*
* The returned value must be deleted with #wasmtime_externref_delete
*/
WASM_API_EXTERN wasmtime_externref_t *
wasmtime_externref_new(void *data, void (*finalizer)(void *));

/**
* \brief Get an `externref`'s wrapped data
*
* Returns the original `data` passed to #wasmtime_externref_new. It is required
* that `data` is not `NULL`.
*/
WASM_API_EXTERN void *wasmtime_externref_data(wasmtime_externref_t *data);

/**
* \brief Creates a shallow copy of the `externref` argument, returning a
* separately owned pointer (increases the reference count).
*/
WASM_API_EXTERN wasmtime_externref_t *
wasmtime_externref_clone(wasmtime_externref_t *ref);

/**
* \brief Decrements the reference count of the `ref`, deleting it if it's the
* last reference.
*/
WASM_API_EXTERN void wasmtime_externref_delete(wasmtime_externref_t *ref);

/**
* \brief Converts a raw `externref` value coming from #wasmtime_val_raw_t into
* a #wasmtime_externref_t.
*
* Note that the returned #wasmtime_externref_t is an owned value that must be
* deleted via #wasmtime_externref_delete by the caller if it is non-null.
*/
WASM_API_EXTERN wasmtime_externref_t *
wasmtime_externref_from_raw(wasmtime_context_t *context, void *raw);

/**
* \brief Converts a #wasmtime_externref_t to a raw value suitable for storing
* into a #wasmtime_val_raw_t.
*
* Note that the returned underlying value is not tracked by Wasmtime's garbage
* collector until it enters WebAssembly. This means that a GC may release the
* context's reference to the raw value, making the raw value invalid within the
* context of the store. Do not perform a GC between calling this function and
* passing it to WebAssembly.
*/
WASM_API_EXTERN void *
wasmtime_externref_to_raw(wasmtime_context_t *context,
const wasmtime_externref_t *ref);

/// \brief Discriminant stored in #wasmtime_val::kind
typedef uint8_t wasmtime_valkind_t;
/// \brief Value of #wasmtime_valkind_t meaning that #wasmtime_val_t is an i32
Expand All @@ -104,9 +29,6 @@ typedef uint8_t wasmtime_valkind_t;
/// \brief Value of #wasmtime_valkind_t meaning that #wasmtime_val_t is a
/// funcref
#define WASMTIME_FUNCREF 5
/// \brief Value of #wasmtime_valkind_t meaning that #wasmtime_val_t is an
/// externref
#define WASMTIME_EXTERNREF 6

/// \brief A 128-bit value representing the WebAssembly `v128` type. Bytes are
/// stored in little-endian order.
Expand Down Expand Up @@ -136,11 +58,6 @@ typedef union wasmtime_valunion {
/// If this value represents a `ref.null func` value then the `store_id` field
/// is set to zero.
wasmtime_func_t funcref;
/// Field used if #wasmtime_val_t::kind is #WASMTIME_EXTERNREF
///
/// If this value represents a `ref.null extern` value then this pointer will
/// be `NULL`.
wasmtime_externref_t *externref;
/// Field used if #wasmtime_val_t::kind is #WASMTIME_V128
wasmtime_v128 v128;
} wasmtime_valunion_t;
Expand Down Expand Up @@ -186,14 +103,6 @@ typedef union wasmtime_val_raw {
///
/// Note that this field is always stored in a little-endian format.
void *funcref;
/// Field for when this val is a WebAssembly `externref` value.
///
/// If this is set to 0 then it's a null externref, otherwise this must be
/// passed to `wasmtime_externref_from_raw` to determine the
/// `wasmtime_externref_t`.
///
/// Note that this field is always stored in a little-endian format.
void *externref;
} wasmtime_val_raw_t;

/**
Expand All @@ -203,11 +112,9 @@ typedef union wasmtime_val_raw {
* \union wasmtime_val
* \brief Container for different kinds of wasm values.
*
* Note that this structure may contain an owned value, namely
* #wasmtime_externref_t, depending on the context in which this is used. APIs
* which consume a #wasmtime_val_t do not take ownership, but APIs that return
* #wasmtime_val_t require that #wasmtime_val_delete is called to deallocate
* the value.
* APIs which consume a #wasmtime_val_t do not take ownership, but APIs that
* return #wasmtime_val_t require that #wasmtime_val_delete is called to
* deallocate the value.
*/
typedef struct wasmtime_val {
/// Discriminant of which field of #of is valid.
Expand All @@ -222,12 +129,14 @@ typedef struct wasmtime_val {
* Note that this only deletes the contents, not the memory that `val` points to
* itself (which is owned by the caller).
*/
WASM_API_EXTERN void wasmtime_val_delete(wasmtime_val_t *val);
WASM_API_EXTERN void wasmtime_val_delete(wasmtime_context_t *context,
wasmtime_val_t *val);

/**
* \brief Copies `src` into `dst`.
*/
WASM_API_EXTERN void wasmtime_val_copy(wasmtime_val_t *dst,
WASM_API_EXTERN void wasmtime_val_copy(wasmtime_context_t *context,
wasmtime_val_t *dst,
const wasmtime_val_t *src);

#ifdef __cplusplus
Expand Down
23 changes: 17 additions & 6 deletions crates/c-api/src/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,12 @@ async fn invoke_c_async_callback<'a>(
let mut hostcall_val_storage = mem::take(&mut caller.data_mut().hostcall_val_storage);
debug_assert!(hostcall_val_storage.is_empty());
hostcall_val_storage.reserve(params.len() + results.len());
hostcall_val_storage.extend(params.iter().cloned().map(|p| wasmtime_val_t::from_val(p)));
hostcall_val_storage.extend(
params
.iter()
.cloned()
.map(|p| wasmtime_val_t::from_val(&mut caller, p)),
);
hostcall_val_storage.extend((0..results.len()).map(|_| wasmtime_val_t {
kind: WASMTIME_I32,
of: wasmtime_val_union { i32: 0 },
Expand Down Expand Up @@ -151,7 +156,7 @@ async fn invoke_c_async_callback<'a>(
// Translate the `wasmtime_val_t` results into the `results` space
for (i, result) in out_results.iter().enumerate() {
unsafe {
results[i] = result.to_val();
results[i] = result.to_val(&mut caller.caller);
}
}
// Move our `vals` storage back into the store now that we no longer
Expand Down Expand Up @@ -229,7 +234,7 @@ async fn do_func_call_async(
match result {
Ok(()) => {
for (slot, val) in results.iter_mut().zip(wt_results.iter()) {
crate::initialize(slot, wasmtime_val_t::from_val(val.clone()));
crate::initialize(slot, wasmtime_val_t::from_val(&mut store, val.clone()));
}
params.truncate(0);
store.data_mut().wasm_val_storage = params;
Expand All @@ -240,7 +245,7 @@ async fn do_func_call_async(

#[no_mangle]
pub unsafe extern "C" fn wasmtime_func_call_async<'a>(
store: CStoreContextMut<'a>,
mut store: CStoreContextMut<'a>,
func: &'a Func,
args: *const wasmtime_val_t,
nargs: usize,
Expand All @@ -251,10 +256,16 @@ pub unsafe extern "C" fn wasmtime_func_call_async<'a>(
) -> Box<wasmtime_call_future_t<'a>> {
let args = crate::slice_from_raw_parts(args, nargs)
.iter()
.map(|i| i.to_val());
.map(|i| i.to_val(&mut store))
.collect::<Vec<_>>();
let results = crate::slice_from_raw_parts_mut(results, nresults);
let fut = Box::pin(do_func_call_async(
store, func, args, results, trap_ret, err_ret,
store,
func,
args.into_iter(),
results,
trap_ret,
err_ret,
));
Box::new(wasmtime_call_future_t { underlying: fut })
}
Expand Down
13 changes: 9 additions & 4 deletions crates/c-api/src/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,12 @@ pub(crate) unsafe fn c_callback_to_rust_fn(
let mut vals = mem::take(&mut caller.data_mut().hostcall_val_storage);
debug_assert!(vals.is_empty());
vals.reserve(params.len() + results.len());
vals.extend(params.iter().cloned().map(|p| wasmtime_val_t::from_val(p)));
vals.extend(
params
.iter()
.cloned()
.map(|p| wasmtime_val_t::from_val(&mut caller, p)),
);
vals.extend((0..results.len()).map(|_| wasmtime_val_t {
kind: crate::WASMTIME_I32,
of: wasmtime_val_union { i32: 0 },
Expand All @@ -272,7 +277,7 @@ pub(crate) unsafe fn c_callback_to_rust_fn(

// Translate the `wasmtime_val_t` results into the `results` space
for (i, result) in out_results.iter().enumerate() {
results[i] = result.to_val();
results[i] = result.to_val(&mut caller.caller);
}

// Move our `vals` storage back into the store now that we no longer
Expand Down Expand Up @@ -330,7 +335,7 @@ pub unsafe extern "C" fn wasmtime_func_call(
&mut params,
crate::slice_from_raw_parts(args, nargs)
.iter()
.map(|i| i.to_val()),
.map(|i| i.to_val(&mut store)),
nresults,
);

Expand All @@ -345,7 +350,7 @@ pub unsafe extern "C" fn wasmtime_func_call(
Ok(Ok(())) => {
let results = crate::slice_from_raw_parts_mut(results, nresults);
for (slot, val) in results.iter_mut().zip(wt_results.iter()) {
crate::initialize(slot, wasmtime_val_t::from_val(val.clone()));
crate::initialize(slot, wasmtime_val_t::from_val(&mut store, val.clone()));
}
params.truncate(0);
store.data_mut().wasm_val_storage = params;
Expand Down
15 changes: 9 additions & 6 deletions crates/c-api/src/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ pub unsafe extern "C" fn wasm_global_set(g: &mut wasm_global_t, val: &wasm_val_t

#[no_mangle]
pub unsafe extern "C" fn wasmtime_global_new(
store: CStoreContextMut<'_>,
mut store: CStoreContextMut<'_>,
gt: &wasm_globaltype_t,
val: &wasmtime_val_t,
ret: &mut Global,
) -> Option<Box<wasmtime_error_t>> {
let global = Global::new(store, gt.ty().ty.clone(), val.to_val());
let val = val.to_val(&mut store);
let global = Global::new(store, gt.ty().ty.clone(), val);
handle_result(global, |global| {
*ret = global;
})
Expand All @@ -100,18 +101,20 @@ pub extern "C" fn wasmtime_global_type(

#[no_mangle]
pub extern "C" fn wasmtime_global_get(
store: CStoreContextMut<'_>,
mut store: CStoreContextMut<'_>,
global: &Global,
val: &mut MaybeUninit<wasmtime_val_t>,
) {
crate::initialize(val, wasmtime_val_t::from_val(global.get(store)))
let gval = global.get(&mut store);
crate::initialize(val, wasmtime_val_t::from_val(store, gval))
}

#[no_mangle]
pub unsafe extern "C" fn wasmtime_global_set(
store: CStoreContextMut<'_>,
mut store: CStoreContextMut<'_>,
global: &Global,
val: &wasmtime_val_t,
) -> Option<Box<wasmtime_error_t>> {
handle_result(global.set(store, val.to_val()), |()| {})
let val = val.to_val(&mut store);
handle_result(global.set(store, val), |()| {})
}
11 changes: 3 additions & 8 deletions crates/c-api/src/ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,9 @@ pub extern "C" fn wasm_ref_copy(r: Option<&wasm_ref_t>) -> Option<Box<wasm_ref_t
}

#[no_mangle]
pub extern "C" fn wasm_ref_same(a: Option<&wasm_ref_t>, b: Option<&wasm_ref_t>) -> bool {
match (a.map(|a| &a.r), b.map(|b| &b.r)) {
(Some(Ref::Extern(Some(a))), Some(Ref::Extern(Some(b)))) => a.ptr_eq(b),
(None, None) => true,
// Note: we don't support equality for `Func`, so we always return
// `false` for `funcref`s.
_ => false,
}
pub extern "C" fn wasm_ref_same(_a: Option<&wasm_ref_t>, _b: Option<&wasm_ref_t>) -> bool {
// We need a store to determine whether these are the same reference or not.
abort("wasm_ref_same")
}

#[no_mangle]
Expand Down
Loading

0 comments on commit bd2ea90

Please sign in to comment.