Skip to content

Commit

Permalink
Implement the Wasm GC instructions for converting between anyref an…
Browse files Browse the repository at this point in the history
…d `externref` (#9435)

* Implement the Wasm GC instructions for converting between `anyref` and `externref`

This commit implements two instructions:

1. `any.convert_extern`
2. `extern.convert_any`

These instructions are used to convert between `anyref` and `externref`
values. The `any.convert_extern` instruction takes an `anyref` value and
converts it to an `externref` value. The `extern.convert_any` instruction takes
an `externref` value and converts it to an `anyref` value.

Rather than implementing wrapper objects -- for example an `struct
AnyOfExtern(ExternRef)` type that is a subtype of `AnyRef` -- we instead reuse
the same representation converted references as their unconverted reference. For
example, `(any.convert_extern my_externref)` is identical to the original
`my_externref` value. This means that we don't actually emit any clif
instructions to implement these Wasm instructions; they are no-ops!

Wasm code remains none-the-wiser because it cannot directly test for the
difference between, for example, a `my_anyref` and the result of
`(extern.convert_any my_anyref)` because they are in two different type
hierarchies, so any direct `ref.test` would be invalid. The Wasm code would have
to convert one into the other's type hierarchy, at which point it doesn't know
whether wrapping/unwrapping took place.

We did need some changes at the host API and host API implementation levels,
however:

* We needed to relax the requirement that a `wasmtime::AnyRef` only wraps a
  `VMGcRef` that points to an object that is a subtype of `anyref` and similar for
  `wasmtime::ExternRef`.

* We needed to make the `wasmtime::ExternRef::data[_mut]` methods return an
  option of their associated host data, since any `externref` resulting from
  `(extern.convert_any ...)` does not have any associated host data. (This change
  would have been required either way, even if we used wrapper objects.)

* fix tests

* fix wasmtime-environ tests
  • Loading branch information
fitzgen authored Oct 10, 2024
1 parent d50fea6 commit 12c20b2
Show file tree
Hide file tree
Showing 27 changed files with 342 additions and 98 deletions.
2 changes: 1 addition & 1 deletion crates/c-api/src/ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ pub unsafe extern "C" fn wasmtime_externref_data(
externref
.and_then(|e| e.as_wasmtime())
.and_then(|e| {
let data = e.data(cx).ok()?;
let data = e.data(cx).ok()??;
Some(data.downcast_ref::<crate::ForeignData>().unwrap().data)
})
.unwrap_or(ptr::null_mut())
Expand Down
12 changes: 9 additions & 3 deletions crates/cranelift/src/translate/code_translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2755,13 +2755,19 @@ pub fn translate_operator<FE: FuncEnvironment + ?Sized>(
)?;
state.push1(result);
}
Operator::AnyConvertExtern => {
// Pop an `externref`, push an `anyref`. But they have the same
// representation, so we don't actually need to do anything.
}
Operator::ExternConvertAny => {
// Pop an `anyref`, push an `externref`. But they have the same
// representation, so we don't actually need to do anything.
}

Operator::RefCastNonNull { .. }
| Operator::RefCastNullable { .. }
| Operator::BrOnCast { .. }
| Operator::BrOnCastFail { .. }
| Operator::AnyConvertExtern
| Operator::ExternConvertAny => {
| Operator::BrOnCastFail { .. } => {
return Err(wasm_unsupported!("GC operator not yet implemented: {op:?}"));
}

Expand Down
34 changes: 10 additions & 24 deletions crates/environ/src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,25 +225,23 @@ impl GcStructLayout {
/// VMGcKind::EqRef`.
///
/// Furthermore, this type only uses the highest 6 bits of its `u32`
/// representation, allowing the lower 26 bytes to be bitpacked with other stuff
/// representation, allowing the lower 27 bytes to be bitpacked with other stuff
/// as users see fit.
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[rustfmt::skip]
#[allow(missing_docs)]
pub enum VMGcKind {
ExternRef = 0b010000 << 26,
ExternOfAnyRef = 0b011000 << 26,
AnyRef = 0b100000 << 26,
AnyOfExternRef = 0b100100 << 26,
EqRef = 0b101000 << 26,
ArrayRef = 0b101001 << 26,
StructRef = 0b101010 << 26,
ExternRef = 0b01000 << 27,
AnyRef = 0b10000 << 27,
EqRef = 0b10100 << 27,
ArrayRef = 0b10101 << 27,
StructRef = 0b10110 << 27,
}

impl VMGcKind {
/// Mask this value with a `u32` to get just the bits that `VMGcKind` uses.
pub const MASK: u32 = 0b111111 << 26;
pub const MASK: u32 = 0b11111 << 27;

/// Mask this value with a `u32` that potentially contains a `VMGcKind` to
/// get the bits that `VMGcKind` doesn't use.
Expand All @@ -262,9 +260,7 @@ impl VMGcKind {
let masked = val & Self::MASK;
match masked {
x if x == Self::ExternRef.as_u32() => Self::ExternRef,
x if x == Self::ExternOfAnyRef.as_u32() => Self::ExternOfAnyRef,
x if x == Self::AnyRef.as_u32() => Self::AnyRef,
x if x == Self::AnyOfExternRef.as_u32() => Self::AnyOfExternRef,
x if x == Self::EqRef.as_u32() => Self::EqRef,
x if x == Self::ArrayRef.as_u32() => Self::ArrayRef,
x if x == Self::StructRef.as_u32() => Self::StructRef,
Expand Down Expand Up @@ -294,21 +290,11 @@ mod tests {

#[test]
fn kind_matches() {
let all = [
ExternRef,
ExternOfAnyRef,
AnyRef,
AnyOfExternRef,
EqRef,
ArrayRef,
StructRef,
];
let all = [ExternRef, AnyRef, EqRef, ArrayRef, StructRef];

for (sup, subs) in [
(ExternRef, vec![ExternOfAnyRef]),
(ExternOfAnyRef, vec![]),
(AnyRef, vec![AnyOfExternRef, EqRef, ArrayRef, StructRef]),
(AnyOfExternRef, vec![]),
(ExternRef, vec![]),
(AnyRef, vec![EqRef, ArrayRef, StructRef]),
(EqRef, vec![ArrayRef, StructRef]),
(ArrayRef, vec![]),
(StructRef, vec![]),
Expand Down
18 changes: 15 additions & 3 deletions crates/fuzzing/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,15 +707,27 @@ pub fn table_ops(
// run into a use-after-free bug with one of these refs we
// are more likely to trigger a segfault.
if let Some(a) = a {
let a = a.data(&caller)?.downcast_ref::<CountDrops>().unwrap();
let a = a
.data(&caller)?
.unwrap()
.downcast_ref::<CountDrops>()
.unwrap();
assert!(a.0.load(SeqCst) <= expected_drops.load(SeqCst));
}
if let Some(b) = b {
let b = b.data(&caller)?.downcast_ref::<CountDrops>().unwrap();
let b = b
.data(&caller)?
.unwrap()
.downcast_ref::<CountDrops>()
.unwrap();
assert!(b.0.load(SeqCst) <= expected_drops.load(SeqCst));
}
if let Some(c) = c {
let c = c.data(&caller)?.downcast_ref::<CountDrops>().unwrap();
let c = c
.data(&caller)?
.unwrap()
.downcast_ref::<CountDrops>()
.unwrap();
assert!(c.0.load(SeqCst) <= expected_drops.load(SeqCst));
}
Ok(())
Expand Down
71 changes: 71 additions & 0 deletions crates/wasmtime/src/runtime/gc/enabled/anyref.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Implementation of `anyref` in Wasmtime.

use super::{ExternRef, RootedGcRefImpl};
use crate::prelude::*;
use crate::runtime::vm::VMGcRef;
use crate::{
Expand Down Expand Up @@ -183,6 +184,64 @@ impl AnyRef {
Rooted::new(store, gc_ref)
}

/// Convert an `externref` into an `anyref`.
///
/// This is equivalent to the `any.convert_extern` instruction in Wasm.
///
/// You can recover the underlying `externref` again via the
/// [`ExternRef::convert_any`] method or the `extern.convert_any` Wasm
/// instruction.
///
/// Returns an error if the `externref` GC reference has been unrooted (eg
/// if you attempt to use a `Rooted<ExternRef>` after exiting the scope it
/// was rooted within). See the documentation for
/// [`Rooted<T>`][crate::Rooted] for more details.
///
/// # Example
///
/// ```
/// use wasmtime::*;
/// # fn foo() -> Result<()> {
/// let engine = Engine::default();
/// let mut store = Store::new(&engine, ());
///
/// // Create an `externref`.
/// let externref = ExternRef::new(&mut store, "hello")?;
///
/// // Convert that `externref` into an `anyref`.
/// let anyref = AnyRef::convert_extern(&mut store, externref)?;
///
/// // The converted value is an `anyref` but is not an `eqref`.
/// assert_eq!(anyref.matches_ty(&store, &HeapType::Any)?, true);
/// assert_eq!(anyref.matches_ty(&store, &HeapType::Eq)?, false);
///
/// // We can convert it back to the original `externref` and get its
/// // associated host data again.
/// let externref = ExternRef::convert_any(&mut store, anyref)?;
/// let data = externref
/// .data(&store)?
/// .expect("externref should have host data")
/// .downcast_ref::<&str>()
/// .expect("host data should be a str");
/// assert_eq!(*data, "hello");
/// # Ok(()) }
/// # foo().unwrap();
pub fn convert_extern(
mut store: impl AsContextMut,
externref: Rooted<ExternRef>,
) -> Result<Rooted<Self>> {
let mut store = AutoAssertNoGc::new(store.as_context_mut().0);
Self::_convert_extern(&mut store, externref)
}

pub(crate) fn _convert_extern(
store: &mut AutoAssertNoGc<'_>,
externref: Rooted<ExternRef>,
) -> Result<Rooted<Self>> {
let gc_ref = externref.try_clone_gc_ref(store)?;
Ok(Self::from_cloned_gc_ref(store, gc_ref))
}

/// Creates a new strongly-owned [`AnyRef`] from the raw value provided.
///
/// This is intended to be used in conjunction with [`Func::new_unchecked`],
Expand Down Expand Up @@ -238,6 +297,11 @@ impl AnyRef {
.header(&gc_ref)
.kind()
.matches(VMGcKind::AnyRef)
|| store
.unwrap_gc_store()
.header(&gc_ref)
.kind()
.matches(VMGcKind::ExternRef)
);
Rooted::new(store, gc_ref)
}
Expand Down Expand Up @@ -292,6 +356,13 @@ impl AnyRef {

let header = store.gc_store()?.header(gc_ref);

if header.kind().matches(VMGcKind::ExternRef) {
return Ok(HeapType::Any);
}

debug_assert!(header.kind().matches(VMGcKind::AnyRef));
debug_assert!(header.kind().matches(VMGcKind::EqRef));

if header.kind().matches(VMGcKind::StructRef) {
return Ok(HeapType::ConcreteStruct(
StructType::from_shared_type_index(store.engine(), header.ty().unwrap()),
Expand Down
Loading

0 comments on commit 12c20b2

Please sign in to comment.