I'm splitting this issue out of https://github.com/bytecodealliance/wasmtime/issues/4185 to write up some thoughts on how this can be done. Specifically today the current Wasmtime support for the component model has mappings for many component model types to Rust native types but not all of them. For example integers, strings, lists, tuples, etc, are all mapped directly to Rust types. Basically if the component model types equivalent in Rust is in the Rust standard library that's already implemented. What that leaves to implement, however, is Rust-defined mappings for component model types that are "structural" like records.
This issue is intended to document the current thinking of how we're going to expose this. The general idea is that we'll create a proc-macro
crate, probably named something like wasmtime-component-macro
, which is an internal dependency of the wasmtime
crate. The various macros would then get reexported at the wasmtime::component::*
namespace.
Currently the bindings for host types are navigated through three traits: ComponentValue
, Lift
, and Lower
. We'll want a custom derive for all three of these traits. Deriving Lift
and Lower
require a ComponentValue
derive as well, but users should be able to pick one of Lift
and Lower
without the other one.
record
Records in the component model correspond to struct
s in Rust. The rough shape of this will be:
use wasmtime::component::{ComponentValue, Lift, Lower};
#[derive(ComponentValue, Lift, Lower)]
#[component(record)]
struct Foo {
#[component(name = "foo-bar-baz")]
a: i32,
b: u32,
}
To typecheck correctly the record
type must list fields in the same order as the fields listed in the Rust code for now. Field reordering may be implemented at a later date but for now we'll do strict matching. Fields must have both matching names and matching types.
The #[component(record)]
here may seem redundant but it's somewhat required below for variants/enums.
The #[component(name = "...")]
is intended to rename the field from the component model's perspective. The type-checking will test against the name
specified.
Using this derive on a tuple or empty struct will result in a compile-time error.
variant
Variants roughly correspond to Rust enum
s:
use wasmtime::component::{ComponentValue, Lift, Lower};
#[derive(ComponentValue, Lift, Lower)]
#[component(variant)]
enum Foo {
#[component(name = "foo-bar-baz")]
A(u32),
B,
}
Typechecking, like records, will check cases in-order and all cases must match in both name and payload. A missing payload in Rust is automatically interpreted as the unit
payload in the component model.
Variants with named fields (B { bar: u32 }
) will be disallowed. Variants with multiple payloads (B(u32, u32)
) will also be disallowed.
Note that #[component(variant)]
here distinguishes it from...
enum
use wasmtime::component::{ComponentValue, Lift, Lower};
#[derive(ComponentValue, Lift, Lower)]
#[component(enum)]
enum Foo {
#[component(name = "foo-bar-baz")]
A,
B,
}
Typechecking is similar to variants where the number/names of cases must all match.
Variants with any payload are disallowed in this derive mode.
union
This will, perhaps surprisingly, still map to an enum
in Rust since this is still a tagged union, not a literal C union
:
use wasmtime::component::{ComponentValue, Lift, Lower};
#[derive(ComponentValue, Lift, Lower)]
#[component(union)]
enum Foo {
A(u32),
B(f32),
}
The number of cases and the types of each case must match a union definition to correctly typecheck. Union cases don't have names so renaming here isn't needed.
A payload on each enum case in Rust is required, and like with variant
it's required to be a tuple-variant with only one element. All other forms of payloads are disallowed. Note that the names in Rust are just informative in Rust, it doesn't affect the ABI or type-checking
flags
These will be a bit "funkier" than the above since there's not something obvious to attach a #[derive]
to:
wasmtime::component::flags! {
#[derive(Lift, Lower)]
flags Foo {
#[component(name = "...")]
const A;
const B;
const C;
}
}
The general idea here is to roughly take inspiration from the bitflags
crate in terms of what the generated code does. Ideally this should have a convenient Debug
implementation along with various constants to OR-together and such in Rust. The exact syntax here is up for debate, this is just a strawman.
Implementation Details
One caveat is that the ComponentValue
/Lift
/Lower
traits mention internal types in the wasmtime
crate which aren't intended to be part of the public API. To solve this the macro will reference items in a path such as:
wasmtime::component::__internal::the_name
The __internal
module will be #[doc(hidden)]
and will only exist to reexport dependencies needed by the proc-macro. This crate may end up having a bland pub use wasmtime_environ
or individual items, whatever works best.
The actual generated trait impls will probably look very similar to the implementations that exist for tuples, and Result<T, E>
already present in typed.rs
Alternatives
One alternative to the above is to have #[derive(ComponentRecord)]
instead of #[derive(ComponentValue)] #[component(record)]
or something like that. While historically some discussions have leaned in this direction with the introduction of Lift
and Lower
traits I personally feel that the balance is now slightly in the other direction where it would be nice if we can keep derive
targeted at the specific traits and then configuration for the derive happens afterwards.
wasm-proposal:component-model