Add a wasmtime::component::bindgen! macro (#5317)

* Import Wasmtime support from the `wit-bindgen` repo

This commit imports the `wit-bindgen-gen-host-wasmtime-rust` crate from
the `wit-bindgen` repository into the upstream Wasmtime repository. I've
chosen to not import the full history here since the crate is relatively
small and doesn't have a ton of complexity. While the history of the
crate is quite long the current iteration of the crate's history is
relatively short so there's not a ton of import there anyway. The
thinking is that this can now continue to evolve in-tree.

* Refactor `wasmtime-component-macro` a bit

Make room for a `wit_bindgen` macro to slot in.

* Add initial support for a `bindgen` macro

* Add tests for `wasmtime::component::bindgen!`

* Improve error forgetting `async` feature

* Add end-to-end tests for bindgen

* Add an audit of `unicase`

* Add a license to the test-helpers crate

* Add vet entry for `pulldown-cmark`

* Update publish script with new crate

* Try to fix publish script

* Update audits

* Update lock file
This commit is contained in:
Alex Crichton
2022-12-06 13:06:00 -06:00
committed by GitHub
parent 293bb5b334
commit 2329ecc341
43 changed files with 4336 additions and 1212 deletions

View File

@@ -12,12 +12,24 @@ edition.workspace = true
[lib]
proc-macro = true
test = false
doctest = false
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["extra-traits"] }
wasmtime-component-util = { workspace = true }
wasmtime-wit-bindgen = { workspace = true }
wit-parser = { workspace = true }
[badges]
maintenance = { status = "actively-developed" }
[dev-dependencies]
wasmtime = { path = '../wasmtime', features = ['component-model'] }
component-macro-test-helpers = { path = 'test-helpers' }
tracing = { workspace = true }
[features]
async = []

View File

@@ -0,0 +1,137 @@
use proc_macro2::{Span, TokenStream};
use std::path::{Path, PathBuf};
use syn::parse::{Error, Parse, ParseStream, Result};
use syn::punctuated::Punctuated;
use syn::token;
use syn::Token;
use wasmtime_wit_bindgen::Opts;
use wit_parser::World;
#[derive(Default)]
pub struct Config {
opts: Opts, // ...
world: World,
files: Vec<String>,
}
pub fn expand(input: &Config) -> Result<TokenStream> {
if !cfg!(feature = "async") && input.opts.async_ {
return Err(Error::new(
Span::call_site(),
"cannot enable async bindings unless `async` crate feature is active",
));
}
let src = input.opts.generate(&input.world);
let mut contents = src.parse::<TokenStream>().unwrap();
// Include a dummy `include_str!` for any files we read so rustc knows that
// we depend on the contents of those files.
let cwd = std::env::var("CARGO_MANIFEST_DIR").unwrap();
for file in input.files.iter() {
contents.extend(
format!(
"const _: &str = include_str!(r#\"{}\"#);\n",
Path::new(&cwd).join(file).display()
)
.parse::<TokenStream>()
.unwrap(),
);
}
Ok(contents)
}
impl Parse for Config {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let call_site = Span::call_site();
let mut world = None;
let mut ret = Config::default();
if input.peek(token::Brace) {
let content;
syn::braced!(content in input);
let fields = Punctuated::<Opt, Token![,]>::parse_terminated(&content)?;
for field in fields.into_pairs() {
match field.into_value() {
Opt::Path(path) => {
if world.is_some() {
return Err(Error::new(path.span(), "cannot specify second world"));
}
world = Some(ret.parse(path)?);
}
Opt::Inline(span, w) => {
if world.is_some() {
return Err(Error::new(span, "cannot specify second world"));
}
world = Some(w);
}
Opt::Tracing(val) => ret.opts.tracing = val,
Opt::Async(val) => ret.opts.async_ = val,
}
}
} else {
let s = input.parse::<syn::LitStr>()?;
world = Some(ret.parse(s)?);
}
ret.world = world.ok_or_else(|| {
Error::new(
call_site,
"must specify a `*.wit` file to generate bindings for",
)
})?;
Ok(ret)
}
}
impl Config {
fn parse(&mut self, path: syn::LitStr) -> Result<World> {
let span = path.span();
let path = path.value();
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let path = manifest_dir.join(path);
self.files.push(path.to_str().unwrap().to_string());
World::parse_file(path).map_err(|e| Error::new(span, e))
}
}
mod kw {
syn::custom_keyword!(path);
syn::custom_keyword!(inline);
syn::custom_keyword!(tracing);
}
enum Opt {
Path(syn::LitStr),
Inline(Span, World),
Tracing(bool),
Async(bool),
}
impl Parse for Opt {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let l = input.lookahead1();
if l.peek(kw::path) {
input.parse::<kw::path>()?;
input.parse::<Token![:]>()?;
Ok(Opt::Path(input.parse()?))
} else if l.peek(kw::inline) {
let span = input.parse::<kw::inline>()?.span;
input.parse::<Token![:]>()?;
let s = input.parse::<syn::LitStr>()?;
let world =
World::parse("<macro-input>", &s.value()).map_err(|e| Error::new(s.span(), e))?;
Ok(Opt::Inline(span, world))
} else if l.peek(kw::tracing) {
input.parse::<kw::tracing>()?;
input.parse::<Token![:]>()?;
Ok(Opt::Tracing(input.parse::<syn::LitBool>()?.value))
} else if l.peek(Token![async]) {
input.parse::<Token![async]>()?;
input.parse::<Token![:]>()?;
Ok(Opt::Async(input.parse::<syn::LitBool>()?.value))
} else {
Err(l.error())
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
[package]
name = "component-macro-test-helpers"
version = "0.0.0"
edition.workspace = true
publish = false
license = "Apache-2.0 WITH LLVM-exception"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"

View File

@@ -0,0 +1,23 @@
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
#[proc_macro]
pub fn foreach(input: TokenStream) -> TokenStream {
let input = proc_macro2::TokenStream::from(input);
let mut cwd = std::env::current_dir().unwrap();
cwd.push("crates/component-macro/tests/codegen");
let mut result = Vec::new();
for f in cwd.read_dir().unwrap() {
let f = f.unwrap().path();
if f.extension().and_then(|s| s.to_str()) == Some("wit") {
let name = f.file_stem().unwrap().to_str().unwrap();
let name = Ident::new(&name.replace("-", "_"), Span::call_site());
let path = f.to_str().unwrap();
result.push(quote! {
#input!(#name #path);
});
}
}
(quote!( #(#result)*)).into()
}

View File

@@ -0,0 +1,24 @@
macro_rules! gentest {
($id:ident $path:tt) => {
mod $id {
mod normal {
wasmtime::component::bindgen!($path);
}
mod async_ {
wasmtime::component::bindgen!({
path: $path,
async: true,
});
}
mod tracing {
wasmtime::component::bindgen!({
path: $path,
tracing: true,
});
}
}
// ...
};
}
component_macro_test_helpers::foreach!(gentest);

View File

@@ -0,0 +1,12 @@
interface chars {
/// A function that accepts a character
take-char: func(x: char)
/// A function that returns a character
return-char: func() -> char
}
world the-world {
import imports: chars
export exports: chars
default export chars
}

View File

@@ -0,0 +1,39 @@
// hello 🐱 world
interface conventions {
kebab-case: func()
record ludicrous-speed {
how-fast-are-you-going: u32,
i-am-going-extremely-slow: u64,
}
foo: func(x: ludicrous-speed)
%function-with-dashes: func()
%function-with-no-weird-characters: func()
apple: func()
apple-pear: func()
apple-pear-grape: func()
a0: func()
// Comment out identifiers that collide when mapped to snake_case, for now; see
// https://github.com/WebAssembly/component-model/issues/118
//APPLE: func()
//APPLE-pear-GRAPE: func()
//apple-PEAR-grape: func()
is-XML: func()
%explicit: func()
%explicit-kebab: func()
// Identifiers with the same name as keywords are quoted.
%bool: func()
}
world the-world {
import imports: conventions
export exports: conventions
default export conventions
}

View File

@@ -0,0 +1 @@
world empty {}

View File

@@ -0,0 +1,54 @@
interface flegs {
flags flag1 {
b0,
}
flags flag2 {
b0, b1,
}
flags flag4 {
b0, b1, b2, b3,
}
flags flag8 {
b0, b1, b2, b3, b4, b5, b6, b7,
}
flags flag16 {
b0, b1, b2, b3, b4, b5, b6, b7,
b8, b9, b10, b11, b12, b13, b14, b15,
}
flags flag32 {
b0, b1, b2, b3, b4, b5, b6, b7,
b8, b9, b10, b11, b12, b13, b14, b15,
b16, b17, b18, b19, b20, b21, b22, b23,
b24, b25, b26, b27, b28, b29, b30, b31,
}
flags flag64 {
b0, b1, b2, b3, b4, b5, b6, b7,
b8, b9, b10, b11, b12, b13, b14, b15,
b16, b17, b18, b19, b20, b21, b22, b23,
b24, b25, b26, b27, b28, b29, b30, b31,
b32, b33, b34, b35, b36, b37, b38, b39,
b40, b41, b42, b43, b44, b45, b46, b47,
b48, b49, b50, b51, b52, b53, b54, b55,
b56, b57, b58, b59, b60, b61, b62, b63,
}
roundtrip-flag1: func(x: flag1) -> flag1
roundtrip-flag2: func(x: flag2) -> flag2
roundtrip-flag4: func(x: flag4) -> flag4
roundtrip-flag8: func(x: flag8) -> flag8
roundtrip-flag16: func(x: flag16) -> flag16
roundtrip-flag32: func(x: flag32) -> flag32
roundtrip-flag64: func(x: flag64) -> flag64
}
world the-flags {
import import-flags: flegs
export export-flags: flegs
default export flegs
}

View File

@@ -0,0 +1,12 @@
interface floats {
float32-param: func(x: float32)
float64-param: func(x: float64)
float32-result: func() -> float32
float64-result: func() -> float64
}
world the-world {
import imports: floats
export exports: floats
default export floats
}

View File

@@ -0,0 +1,39 @@
interface integers {
a1: func(x: u8)
a2: func(x: s8)
a3: func(x: u16)
a4: func(x: s16)
a5: func(x: u32)
a6: func(x: s32)
a7: func(x: u64)
a8: func(x: s64)
a9: func(
p1: u8,
p2: s8,
p3: u16,
p4: s16,
p5: u32,
p6: s32,
p7: u64,
p8: s64,
)
r1: func() -> u8
r2: func() -> s8
r3: func() -> u16
r4: func() -> s16
r5: func() -> u32
r6: func() -> s32
r7: func() -> u64
r8: func() -> s64
pair-ret: func() -> tuple<s64, u8>
}
world the-world {
import imports: integers
export exports: integers
default export integers
}

View File

@@ -0,0 +1,84 @@
interface lists {
list-u8-param: func(x: list<u8>)
list-u16-param: func(x: list<u16>)
list-u32-param: func(x: list<u32>)
list-u64-param: func(x: list<u64>)
list-s8-param: func(x: list<s8>)
list-s16-param: func(x: list<s16>)
list-s32-param: func(x: list<s32>)
list-s64-param: func(x: list<s64>)
list-float32-param: func(x: list<float32>)
list-float64-param: func(x: list<float64>)
list-u8-ret: func() -> list<u8>
list-u16-ret: func() -> list<u16>
list-u32-ret: func() -> list<u32>
list-u64-ret: func() -> list<u64>
list-s8-ret: func() -> list<s8>
list-s16-ret: func() -> list<s16>
list-s32-ret: func() -> list<s32>
list-s64-ret: func() -> list<s64>
list-float32-ret: func() -> list<float32>
list-float64-ret: func() -> list<float64>
tuple-list: func(x: list<tuple<u8, s8>>) -> list<tuple<s64, u32>>
string-list-arg: func(a: list<string>)
string-list-ret: func() -> list<string>
tuple-string-list: func(x: list<tuple<u8, string>>) -> list<tuple<string, u8>>
string-list: func(x: list<string>) -> list<string>
record some-record {
x: string,
y: other-record,
z: list<other-record>,
c1: u32,
c2: u64,
c3: s32,
c4: s64,
}
record other-record {
a1: u32,
a2: u64,
a3: s32,
a4: s64,
b: string,
c: list<u8>,
}
record-list: func(x: list<some-record>) -> list<other-record>
record-list-reverse: func(x: list<other-record>) -> list<some-record>
variant some-variant {
a(string),
b,
c(u32),
d(list<other-variant>),
}
variant other-variant {
a,
b(u32),
c(string),
}
variant-list: func(x: list<some-variant>) -> list<other-variant>
type load-store-all-sizes = list<tuple<
string,
u8,
s8,
u16,
s16,
u32,
s32,
u64,
s64,
float32,
float64,
char,
>>
load-store-everything: func(a: load-store-all-sizes) -> load-store-all-sizes
}
world the-lists {
import import-lists: lists
export export-lists: lists
default export lists
}

View File

@@ -0,0 +1,51 @@
interface manyarg {
many-args: func(
a1: u64,
a2: u64,
a3: u64,
a4: u64,
a5: u64,
a6: u64,
a7: u64,
a8: u64,
a9: u64,
a10: u64,
a11: u64,
a12: u64,
a13: u64,
a14: u64,
a15: u64,
a16: u64,
)
record big-struct {
a1: string,
a2: string,
a3: string,
a4: string,
a5: string,
a6: string,
a7: string,
a8: string,
a9: string,
a10: string,
a11: string,
a12: string,
a13: string,
a14: string,
a15: string,
a16: string,
a17: string,
a18: string,
a19: string,
a20: string,
}
big-argument: func(x: big-struct)
}
world the-world {
import imports: manyarg
export exports: manyarg
default export manyarg
}

View File

@@ -0,0 +1,13 @@
interface multi-return {
mra: func()
mrb: func() -> ()
mrc: func() -> u32
mrd: func() -> (a: u32)
mre: func() -> (a: u32, b: float32)
}
world the-world {
import imports: multi-return
export exports: multi-return
default export multi-return
}

View File

@@ -0,0 +1,60 @@
interface records {
tuple-arg: func(x: tuple<char, u32>)
tuple-result: func() -> tuple<char, u32>
record empty {}
empty-arg: func(x: empty)
empty-result: func() -> empty
/// A record containing two scalar fields
/// that both have the same type
record scalars {
/// The first field, named a
a: u32,
/// The second field, named b
b: u32,
}
scalar-arg: func(x: scalars)
scalar-result: func() -> scalars
/// A record that is really just flags
/// All of the fields are bool
record really-flags {
a: bool,
b: bool,
c: bool,
d: bool,
e: bool,
f: bool,
g: bool,
h: bool,
i: bool,
}
flags-arg: func(x: really-flags)
flags-result: func() -> really-flags
record aggregates {
a: scalars,
b: u32,
c: empty,
d: string,
e: really-flags,
}
aggregate-arg: func(x: aggregates)
aggregate-result: func() -> aggregates
type tuple-typedef = tuple<s32>
type int-typedef = s32
type tuple-typedef2 = tuple<int-typedef>
typedef-inout: func(e: tuple-typedef2) -> s32
}
world the-world {
import imports: records
export exports: records
default export records
}

View File

@@ -0,0 +1,16 @@
interface simple {
f1: func()
f2: func(a: u32)
f3: func(a: u32, b: u32)
f4: func() -> u32
f5: func() -> tuple<u32, u32>
f6: func(a: u32, b: u32, c: u32) -> tuple<u32, u32, u32>
}
world the-world {
import imports: simple
export exports: simple
default export simple
}

View File

@@ -0,0 +1,13 @@
interface simple-lists {
simple-list1: func(l: list<u32>)
simple-list2: func() -> list<u32>
// TODO: reenable this when smw implements this
// simple-list3: func(a: list<u32>, b: list<u32>) -> tuple<list<u32>, list<u32>>
simple-list4: func(l: list<list<u32>>) -> list<list<u32>>
}
world my-world {
import imports: simple-lists
export exports: simple-lists
default export simple-lists
}

View File

@@ -0,0 +1,14 @@
interface anon {
enum error {
success,
failure,
}
option-test: func() -> result<option<string>, error>
}
world the-world {
import imports: anon
export exports: anon
default export anon
}

View File

@@ -0,0 +1,5 @@
world the-world {
default export interface {
y: func()
}
}

View File

@@ -0,0 +1,5 @@
world the-world {
export the-name: interface {
y: func()
}
}

View File

@@ -0,0 +1,5 @@
world the-world {
import imports: interface {
y: func()
}
}

View File

@@ -0,0 +1,11 @@
interface strings {
a: func(x: string)
b: func() -> string
c: func(a: string, b: string) -> string
}
world the-world {
import imports: strings
export exports: strings
default export strings
}

View File

@@ -0,0 +1,65 @@
interface unions {
/// A union of all of the integral types
union all-integers {
/// Bool is equivalent to a 1 bit integer
/// and is treated that way in some languages
bool,
u8, u16, u32, u64,
s8, s16, s32, s64
}
union all-floats {
float32, float64
}
union all-text {
char, string
}
// Returns the same case as the input but with 1 added
add-one-integer: func(num: all-integers) -> all-integers
// Returns the same case as the input but with 1 added
add-one-float: func(num: all-floats) -> all-floats
// Returns the same case as the input but with the first character replaced
replace-first-char: func(text: all-text, letter: char) -> all-text
// Returns the index of the case provided
identify-integer: func(num: all-integers) -> u8
// Returns the index of the case provided
identify-float: func(num: all-floats) -> u8
// Returns the index of the case provided
identify-text: func(text: all-text) -> u8
union duplicated-s32 {
/// The first s32
s32,
/// The second s32
s32,
/// The third s32
s32
}
// Returns the same case as the input but with 1 added
add-one-duplicated: func(num: duplicated-s32) -> duplicated-s32
// Returns the index of the case provided
identify-duplicated: func(num: duplicated-s32) -> u8
/// A type containing numeric types that are distinct in most languages
union distinguishable-num {
/// A Floating Point Number
float64,
/// A Signed Integer
s64
}
// Returns the same case as the input but with 1 added
add-one-distinguishable-num: func(num: distinguishable-num) -> distinguishable-num
// Returns the index of the case provided
identify-distinguishable-num: func(num: distinguishable-num) -> u8
}
world the-unions {
import import-unions: unions
export export-unions: unions
default export unions
}

View File

@@ -0,0 +1,146 @@
interface variants {
enum e1 {
a,
}
e1-arg: func(x: e1)
e1-result: func() -> e1
union u1 {
u32,
float32,
}
u1-arg: func(x: u1)
u1-result: func() -> u1
record empty {}
variant v1 {
a,
b(u1),
c(e1),
d(string),
e(empty),
f,
g(u32),
}
v1-arg: func(x: v1)
v1-result: func() -> v1
bool-arg: func(x: bool)
bool-result: func() -> bool
option-arg: func(
a: option<bool>,
b: option<tuple<>>,
c: option<u32>,
d: option<e1>,
e: option<float32>,
f: option<u1>,
g: option<option<bool>>,
)
option-result: func() -> tuple<
option<bool>,
option<tuple<>>,
option<u32>,
option<e1>,
option<float32>,
option<u1>,
option<option<bool>>,
>
variant casts1 {
a(s32),
b(float32),
}
variant casts2 {
a(float64),
b(float32),
}
variant casts3 {
a(float64),
b(u64),
}
variant casts4 {
a(u32),
b(s64),
}
variant casts5 {
a(float32),
b(s64),
}
variant casts6 {
a(tuple<float32, u32>),
b(tuple<u32, u32>),
}
casts: func(
a: casts1,
b: casts2,
c: casts3,
d: casts4,
e: casts5,
f: casts6,
) -> tuple<
casts1,
casts2,
casts3,
casts4,
casts5,
casts6,
>
result-arg: func(
a: result,
b: result<_, e1>,
c: result<e1>,
d: result<tuple<>, tuple<>>,
e: result<u32, v1>,
f: result<string, list<u8>>,
)
result-result: func() -> tuple<
result,
result<_, e1>,
result<e1>,
result<tuple<>, tuple<>>,
result<u32, v1>,
result<string, list<u8>>,
>
enum my-errno {
bad1,
bad2,
}
return-result-sugar: func() -> result<s32, my-errno>
return-result-sugar2: func() -> result<_, my-errno>
return-result-sugar3: func() -> result<my-errno, my-errno>
return-result-sugar4: func() -> result<tuple<s32, u32>, my-errno>
return-option-sugar: func() -> option<s32>
return-option-sugar2: func() -> option<my-errno>
result-simple: func() -> result<u32, s32>
record is-clone {
v1: v1,
}
is-clone-arg: func(a: is-clone)
is-clone-return: func() -> is-clone
return-named-option: func() -> (a: option<u8>)
return-named-result: func() -> (a: result<u8, my-errno>)
}
world my-world {
import imports: variants
export exports: variants
default export variants
}