Add an example #[wasmtime] Rust macro (#283)
This commit adds a `wasmtime-rust` crate to the `misc` folder next to the previously added Python extension. The intention is that this showcases loading a WebAssembly file natively in Rust and how with an attribute macro it can feel lightweight in terms of boilerplate. The macro itself is pretty non-featureful today beyond the bare bones to get anything working, but there's all sorts of possibilities like JIT-compiled entry stubs we could eventually do with all the type information!
This commit is contained in:
committed by
Till Schneidereit
parent
54dd085e27
commit
d1b1500d19
@@ -39,7 +39,10 @@ rayon = "1.1"
|
|||||||
wasm-webidl-bindings = "0.4"
|
wasm-webidl-bindings = "0.4"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["misc/wasmtime-py"]
|
members = [
|
||||||
|
"misc/wasmtime-rust",
|
||||||
|
"misc/wasmtime-py",
|
||||||
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
lightbeam = ["wasmtime-environ/lightbeam", "wasmtime-jit/lightbeam"]
|
lightbeam = ["wasmtime-environ/lightbeam", "wasmtime-jit/lightbeam"]
|
||||||
|
|||||||
17
misc/wasmtime-rust/Cargo.toml
Normal file
17
misc/wasmtime-rust/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "wasmtime-rust"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Alex Crichton <alex@alexcrichton.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
test = false
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cranelift-codegen = "0.38.0"
|
||||||
|
cranelift-native = "0.38.0"
|
||||||
|
failure = "0.1.5"
|
||||||
|
wasmtime-interface-types = { path = "../../wasmtime-interface-types" }
|
||||||
|
wasmtime-jit = { path = "../../wasmtime-jit" }
|
||||||
|
wasmtime-rust-macro = { path = "./macro" }
|
||||||
37
misc/wasmtime-rust/README.md
Normal file
37
misc/wasmtime-rust/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# `wasmtime-rust` - Using WebAssembly from Rust
|
||||||
|
|
||||||
|
This crate is intended to be an example of how to load WebAssembly files from a
|
||||||
|
native Rust application. You can always use `wasmtime` and its family of crates
|
||||||
|
directly, but the purpose of this crate is to provide an ergonomic macro:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[wasmtime_rust::wasmtime]
|
||||||
|
trait WasmMarkdown {
|
||||||
|
fn render(&mut self, input: &str) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), failure::Error> {
|
||||||
|
let mut markdown = WasmMarkdown::load_file("markdown.wasm")?;
|
||||||
|
println!("{}", markdown.render("# Hello, Rust!"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `wasmtime` macro defined in the `wasmtime-rust` crate is placed on a `trait`
|
||||||
|
which includes the set of functionality which a wasm module should export. In
|
||||||
|
this case we're expecting one `render` function which takes and returns a
|
||||||
|
string.
|
||||||
|
|
||||||
|
The macro expands to a `struct` with all of the methods on the trait (they must
|
||||||
|
all be `&mut self`) and one function called `load_file` to actually instantiate
|
||||||
|
the module.
|
||||||
|
|
||||||
|
Note that this macro is still in early stages of development, so error messages
|
||||||
|
aren't great yet and all functionality isn't supported yet.
|
||||||
|
|
||||||
|
## Missing features
|
||||||
|
|
||||||
|
Currently if the wasm module imports any symbols outside of the WASI namespace
|
||||||
|
the module will not load. It's intended that support for this will be added soon
|
||||||
|
though!
|
||||||
13
misc/wasmtime-rust/examples/markdown.rs
Normal file
13
misc/wasmtime-rust/examples/markdown.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use wasmtime_rust::wasmtime;
|
||||||
|
|
||||||
|
#[wasmtime]
|
||||||
|
trait WasmMarkdown {
|
||||||
|
fn render(&mut self, input: &str) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), failure::Error> {
|
||||||
|
let mut markdown = WasmMarkdown::load_file("markdown.wasm")?;
|
||||||
|
println!("{}", markdown.render("# Hello, Rust!"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
15
misc/wasmtime-rust/macro/Cargo.toml
Normal file
15
misc/wasmtime-rust/macro/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "wasmtime-rust-macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Alex Crichton <alex@alexcrichton.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
test = false
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
quote = "1.0"
|
||||||
|
syn = { version = "1.0", features = ['full'] }
|
||||||
5
misc/wasmtime-rust/macro/README.md
Normal file
5
misc/wasmtime-rust/macro/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# `wasmtime-rust-macro`
|
||||||
|
|
||||||
|
This is the actual definition of the `#[wasmtime]` macro, but it's intended that
|
||||||
|
this crate isn't used directly but rather the `wasmtime-rust` crate is used
|
||||||
|
instead.
|
||||||
159
misc/wasmtime-rust/macro/src/lib.rs
Normal file
159
misc/wasmtime-rust/macro/src/lib.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn wasmtime(
|
||||||
|
_attr: proc_macro::TokenStream,
|
||||||
|
item: proc_macro::TokenStream,
|
||||||
|
) -> proc_macro::TokenStream {
|
||||||
|
let item = syn::parse_macro_input!(item as syn::ItemTrait);
|
||||||
|
expand(item).unwrap_or_else(|e| e.to_compile_error()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand(item: syn::ItemTrait) -> syn::Result<TokenStream> {
|
||||||
|
let definition = generate_struct(&item)?;
|
||||||
|
let load = generate_load(&item)?;
|
||||||
|
let methods = generate_methods(&item)?;
|
||||||
|
let name = &item.ident;
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#definition
|
||||||
|
impl #name {
|
||||||
|
#load
|
||||||
|
#methods
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_struct(item: &syn::ItemTrait) -> syn::Result<TokenStream> {
|
||||||
|
let vis = &item.vis;
|
||||||
|
let name = &item.ident;
|
||||||
|
let root = root();
|
||||||
|
Ok(quote! {
|
||||||
|
#vis struct #name {
|
||||||
|
cx: #root::wasmtime_jit::Context,
|
||||||
|
handle: #root::wasmtime_jit::InstanceHandle,
|
||||||
|
data: #root::wasmtime_interface_types::ModuleData,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_load(item: &syn::ItemTrait) -> syn::Result<TokenStream> {
|
||||||
|
let vis = &item.vis;
|
||||||
|
let name = &item.ident;
|
||||||
|
let root = root();
|
||||||
|
Ok(quote! {
|
||||||
|
#vis fn load_file(path: impl AsRef<std::path::Path>) -> Result<#name, #root::failure::Error> {
|
||||||
|
let bytes = std::fs::read(path)?;
|
||||||
|
|
||||||
|
let isa = {
|
||||||
|
let isa_builder = #root::cranelift_native::builder()
|
||||||
|
.map_err(|s| #root::failure::format_err!("{}", s))?;
|
||||||
|
let flag_builder = #root::cranelift_codegen::settings::builder();
|
||||||
|
isa_builder.finish(#root::cranelift_codegen::settings::Flags::new(flag_builder))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cx = #root::wasmtime_jit::Context::with_isa(isa);
|
||||||
|
let data = #root::wasmtime_interface_types::ModuleData::new(&bytes)?;
|
||||||
|
let handle = cx.instantiate_module(None, &bytes)?;
|
||||||
|
|
||||||
|
Ok(#name { cx, handle, data })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_methods(item: &syn::ItemTrait) -> syn::Result<TokenStream> {
|
||||||
|
macro_rules! bail {
|
||||||
|
($e:expr, $($fmt:tt)*) => (
|
||||||
|
return Err(syn::Error::new($e.span(), format!($($fmt)*)));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let mut result = TokenStream::new();
|
||||||
|
let root = root();
|
||||||
|
|
||||||
|
for item in item.items.iter() {
|
||||||
|
let method = match item {
|
||||||
|
syn::TraitItem::Method(f) => f,
|
||||||
|
other => bail!(other, "only methods are allowed"),
|
||||||
|
};
|
||||||
|
if let Some(e) = &method.default {
|
||||||
|
bail!(e, "cannot specify an implementation of methods");
|
||||||
|
}
|
||||||
|
if let Some(t) = &method.sig.constness {
|
||||||
|
bail!(t, "cannot be `const`");
|
||||||
|
}
|
||||||
|
if let Some(t) = &method.sig.asyncness {
|
||||||
|
bail!(t, "cannot be `async`");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut args = Vec::new();
|
||||||
|
for arg in method.sig.inputs.iter() {
|
||||||
|
let arg = match arg {
|
||||||
|
syn::FnArg::Receiver(_) => continue,
|
||||||
|
syn::FnArg::Typed(arg) => arg,
|
||||||
|
};
|
||||||
|
let ident = match &*arg.pat {
|
||||||
|
syn::Pat::Ident(i) => i,
|
||||||
|
other => bail!(other, "must use bare idents for arguments"),
|
||||||
|
};
|
||||||
|
if let Some(t) = &ident.by_ref {
|
||||||
|
bail!(t, "arguments cannot bind by reference");
|
||||||
|
}
|
||||||
|
if let Some(t) = &ident.mutability {
|
||||||
|
bail!(t, "arguments cannot be mutable");
|
||||||
|
}
|
||||||
|
if let Some((_, t)) = &ident.subpat {
|
||||||
|
bail!(t, "arguments cannot have sub-bindings");
|
||||||
|
}
|
||||||
|
let ident = &ident.ident;
|
||||||
|
args.push(quote! {
|
||||||
|
#root::wasmtime_interface_types::Value::from(#ident)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let convert_ret = match &method.sig.output {
|
||||||
|
syn::ReturnType::Default => {
|
||||||
|
quote! {
|
||||||
|
<() as #root::FromVecValue>::from(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syn::ReturnType::Type(_, ty) => match &**ty {
|
||||||
|
syn::Type::Tuple(..) => {
|
||||||
|
quote! { <#ty as #root::FromVecValue>::from(results) }
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
quote! { <(#ty,) as #root::FromVecValue>::from(results).map(|t| t.0) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let sig = &method.sig;
|
||||||
|
let attrs = &method.attrs;
|
||||||
|
let name = &method.sig.ident;
|
||||||
|
|
||||||
|
result.extend(quote! {
|
||||||
|
#(#attrs)*
|
||||||
|
#sig {
|
||||||
|
let args = [
|
||||||
|
#(#args),*
|
||||||
|
];
|
||||||
|
let results = self.data.invoke(
|
||||||
|
&mut self.cx,
|
||||||
|
&mut self.handle,
|
||||||
|
stringify!(#name),
|
||||||
|
&args,
|
||||||
|
).expect("wasm execution failed");
|
||||||
|
#convert_ret.expect("failed to convert return type")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root() -> TokenStream {
|
||||||
|
quote! { wasmtime_rust::__rt }
|
||||||
|
}
|
||||||
47
misc/wasmtime-rust/src/lib.rs
Normal file
47
misc/wasmtime-rust/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
pub use wasmtime_rust_macro::wasmtime;
|
||||||
|
|
||||||
|
// modules used by the macro
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod __rt {
|
||||||
|
pub use cranelift_codegen;
|
||||||
|
pub use cranelift_native;
|
||||||
|
pub use failure;
|
||||||
|
pub use wasmtime_interface_types;
|
||||||
|
pub use wasmtime_jit;
|
||||||
|
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
use wasmtime_interface_types::Value;
|
||||||
|
|
||||||
|
pub trait FromVecValue: Sized {
|
||||||
|
fn from(list: Vec<Value>) -> Result<Self, failure::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! tuple {
|
||||||
|
($(($($a:ident),*),)*) => ($(
|
||||||
|
impl<$($a: TryFrom<Value>),*> FromVecValue for ($($a,)*)
|
||||||
|
where $(failure::Error: From<$a::Error>,)*
|
||||||
|
{
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn from(list: Vec<Value>) -> Result<Self, failure::Error> {
|
||||||
|
let mut iter = list.into_iter();
|
||||||
|
$(
|
||||||
|
let $a = iter.next()
|
||||||
|
.ok_or_else(|| failure::format_err!("not enough values"))?
|
||||||
|
.try_into()?;
|
||||||
|
)*
|
||||||
|
if iter.next().is_some() {
|
||||||
|
failure::format_err!("too many return values");
|
||||||
|
}
|
||||||
|
Ok(($($a,)*))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*)
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple! {
|
||||||
|
(),
|
||||||
|
(A),
|
||||||
|
(A, B),
|
||||||
|
(A, B, C),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// The set of all possible WebAssembly Interface Types
|
/// The set of all possible WebAssembly Interface Types
|
||||||
@@ -20,6 +21,17 @@ macro_rules! from {
|
|||||||
Value::$b(val)
|
Value::$b(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Value> for $a {
|
||||||
|
type Error = failure::Error;
|
||||||
|
|
||||||
|
fn try_from(val: Value) -> Result<$a, Self::Error> {
|
||||||
|
match val {
|
||||||
|
Value::$b(v) => Ok(v),
|
||||||
|
v => failure::bail!("cannot convert {:?} to {}", v, stringify!($a)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)*)
|
)*)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user