diff --git a/crates/wiggle/wasmtime/Cargo.toml b/crates/wiggle/wasmtime/Cargo.toml index 06f17700ed..f51ea119bb 100644 --- a/crates/wiggle/wasmtime/Cargo.toml +++ b/crates/wiggle/wasmtime/Cargo.toml @@ -26,6 +26,11 @@ name = "atoms_async" path = "tests/atoms_async.rs" required-features = ["async", "wasmtime/wat"] +[[test]] +name = "atoms_sync" +path = "tests/atoms_sync.rs" +required-features = ["wasmtime/wat"] + [badges] maintenance = { status = "actively-developed" } diff --git a/crates/wiggle/wasmtime/macro/src/config.rs b/crates/wiggle/wasmtime/macro/src/config.rs index 76a23dd31f..e37d3492ea 100644 --- a/crates/wiggle/wasmtime/macro/src/config.rs +++ b/crates/wiggle/wasmtime/macro/src/config.rs @@ -16,7 +16,6 @@ pub struct Config { pub witx: WitxConf, pub ctx: CtxConf, pub modules: ModulesConf, - #[cfg(feature = "async")] pub async_: AsyncConf, } @@ -26,7 +25,6 @@ pub enum ConfigField { Witx(WitxConf), Ctx(CtxConf), Modules(ModulesConf), - #[cfg(feature = "async")] Async(AsyncConf), } @@ -67,17 +65,7 @@ impl Parse for ConfigField { } else if lookahead.peek(Token![async]) { input.parse::()?; input.parse::()?; - #[cfg(feature = "async")] - { - Ok(ConfigField::Async(input.parse()?)) - } - #[cfg(not(feature = "async"))] - { - Err(syn::Error::new( - input.span(), - "async not supported, enable cargo feature \"async\"", - )) - } + Ok(ConfigField::Async(input.parse()?)) } else { Err(lookahead.error()) } @@ -90,7 +78,6 @@ impl Config { let mut witx = None; let mut ctx = None; let mut modules = None; - #[cfg(feature = "async")] let mut async_ = None; for f in fields { match f { @@ -118,7 +105,6 @@ impl Config { } modules = Some(c); } - #[cfg(feature = "async")] ConfigField::Async(c) => { if async_.is_some() { return Err(Error::new(err_loc, "duplicate `async` field")); @@ -132,7 +118,6 @@ impl Config { witx: witx.ok_or_else(|| Error::new(err_loc, "`witx` field required"))?, ctx: ctx.ok_or_else(|| Error::new(err_loc, "`ctx` field required"))?, modules: modules.ok_or_else(|| Error::new(err_loc, "`modules` field required"))?, - #[cfg(feature = "async")] async_: async_.unwrap_or_default(), }) } diff --git a/crates/wiggle/wasmtime/macro/src/lib.rs b/crates/wiggle/wasmtime/macro/src/lib.rs index 24732c7f2e..b7bf5543c9 100644 --- a/crates/wiggle/wasmtime/macro/src/lib.rs +++ b/crates/wiggle/wasmtime/macro/src/lib.rs @@ -48,11 +48,6 @@ pub fn wasmtime_integration(args: TokenStream) -> TokenStream { let doc = config.load_document(); let names = Names::new(quote!(wasmtime_wiggle)); - #[cfg(feature = "async")] - let async_config = config.async_.clone(); - #[cfg(not(feature = "async"))] - let async_config = AsyncConf::default(); - let modules = config.modules.iter().map(|(name, module_conf)| { let module = doc .module(&witx::Id::new(name)) @@ -63,7 +58,7 @@ pub fn wasmtime_integration(args: TokenStream) -> TokenStream { &names, &config.target, &config.ctx.name, - &async_config, + &config.async_, ) }); quote!( #(#modules)* ).into() @@ -106,14 +101,22 @@ fn generate_module( let mut ctor_externs = Vec::new(); let mut host_funcs = Vec::new(); + #[cfg(not(feature = "async"))] + let mut requires_dummy_executor = false; + for f in module.funcs() { + let is_async = async_conf.is_async(module.name.as_str(), f.name.as_str()); + #[cfg(not(feature = "async"))] + if is_async { + requires_dummy_executor = true; + } generate_func( &module_id, &f, names, &target_module, ctx_type, - async_conf.is_async(module.name.as_str(), f.name.as_str()), + is_async, &mut fns, &mut ctor_externs, &mut host_funcs, @@ -157,6 +160,15 @@ contained in the `cx` parameter.", } }); + #[cfg(not(feature = "async"))] + let dummy_executor = if requires_dummy_executor { + dummy_executor() + } else { + quote!() + }; + #[cfg(feature = "async")] + let dummy_executor = quote!(); + quote! { #type_docs pub struct #type_name { @@ -219,6 +231,8 @@ contained in the `cx` parameter.", } #(#fns)* + + #dummy_executor } } } @@ -283,8 +297,10 @@ fn generate_func( }); if is_async { - let wrapper = format_ident!("wrap{}_async", params.len()); - ctors.push(quote! { + #[cfg(feature = "async")] + { + let wrapper = format_ident!("wrap{}_async", params.len()); + ctors.push(quote! { let #name_ident = wasmtime::Func::#wrapper( store, ctx.clone(), @@ -294,6 +310,23 @@ fn generate_func( } ); }); + } + + #[cfg(not(feature = "async"))] + { + // Emit a synchronous function. Self::#fn_ident returns a Future, so we need to + // use a dummy executor to let any synchronous code inside there execute correctly. If + // the future ends up Pending, this func will Trap. + ctors.push(quote! { + let my_ctx = ctx.clone(); + let #name_ident = wasmtime::Func::wrap( + store, + move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> { + Self::run_in_dummy_executor(Self::#fn_ident(&caller, &mut my_ctx.borrow_mut() #(, #arg_names)*)) + } + ); + }); + } } else { ctors.push(quote! { let my_ctx = ctx.clone(); @@ -307,22 +340,45 @@ fn generate_func( } let host_wrapper = if is_async { - let wrapper = format_ident!("wrap{}_host_func_async", params.len()); - quote! { - config.#wrapper( - module, - field, - move |caller #(,#arg_decls)*| - -> Box>> { - Box::new(async move { - let ctx = caller.store() + #[cfg(feature = "async")] + { + let wrapper = format_ident!("wrap{}_host_func_async", params.len()); + quote! { + config.#wrapper( + module, + field, + move |caller #(,#arg_decls)*| + -> Box>> { + Box::new(async move { + let ctx = caller.store() + .get::>>() + .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; + let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*).await; + result + }) + } + ); + } + } + + #[cfg(not(feature = "async"))] + { + // Emit a synchronous host function. Self::#fn_ident returns a Future, so we need to + // use a dummy executor to let any synchronous code inside there execute correctly. If + // the future ends up Pending, this func will Trap. + quote! { + config.wrap_host_func( + module, + field, + move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> { + let ctx = caller + .store() .get::>>() .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; - let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*).await; - result - }) - } - ); + Self::run_in_dummy_executor(Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*)) + }, + ); + } } } else { quote! { @@ -334,11 +390,52 @@ fn generate_func( .store() .get::>>() .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; - let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*); - result + Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*) }, ); } }; host_funcs.push((func.name.clone(), host_wrapper)); } +#[cfg(not(feature = "async"))] +fn dummy_executor() -> TokenStream2 { + quote! { + fn run_in_dummy_executor(future: F) -> F::Output { + use std::pin::Pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + + let mut f = Pin::from(Box::new(future)); + let waker = dummy_waker(); + let mut cx = Context::from_waker(&waker); + match f.as_mut().poll(&mut cx) { + Poll::Ready(val) => return val, + Poll::Pending => { + panic!("Cannot wait on pending future: must enable wiggle \"async\" future and execute on an async Store") + } + } + + fn dummy_waker() -> Waker { + return unsafe { Waker::from_raw(clone(5 as *const _)) }; + + unsafe fn clone(ptr: *const ()) -> RawWaker { + assert_eq!(ptr as usize, 5); + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + RawWaker::new(ptr, &VTABLE) + } + + unsafe fn wake(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + + unsafe fn wake_by_ref(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + + unsafe fn drop(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + } + + } + } +} diff --git a/crates/wiggle/wasmtime/tests/atoms_sync.rs b/crates/wiggle/wasmtime/tests/atoms_sync.rs new file mode 100644 index 0000000000..057ebbaa6e --- /dev/null +++ b/crates/wiggle/wasmtime/tests/atoms_sync.rs @@ -0,0 +1,188 @@ +#![allow(unused)] +// These tests are designed to check the behavior with the crate's async feature (& wasmtimes async +// feature) disabled. Run with: +// `cargo test --no-default-features --features wasmtime/wat --test atoms_sync` +#[cfg(feature = "async")] +#[test] +fn these_tests_require_async_feature_disabled() {} + +use std::cell::RefCell; +use std::rc::Rc; + +wasmtime_wiggle::from_witx!({ + witx: ["$CARGO_MANIFEST_DIR/tests/atoms.witx"], + async: { + atoms::{double_int_return_float} + } +}); + +wasmtime_wiggle::wasmtime_integration!({ + target: crate, + witx: ["$CARGO_MANIFEST_DIR/tests/atoms.witx"], + ctx: Ctx, + modules: { atoms => { name: Atoms } }, + async: { + atoms::double_int_return_float + } +}); + +pub struct Ctx; +impl wiggle::GuestErrorType for types::Errno { + fn success() -> Self { + types::Errno::Ok + } +} + +#[wasmtime_wiggle::async_trait] +impl atoms::Atoms for Ctx { + fn int_float_args(&self, an_int: u32, an_float: f32) -> Result<(), types::Errno> { + println!("INT FLOAT ARGS: {} {}", an_int, an_float); + Ok(()) + } + async fn double_int_return_float( + &self, + an_int: u32, + ) -> Result { + Ok((an_int as f32) * 2.0) + } +} + +fn run_int_float_args(linker: &wasmtime::Linker) { + let shim_mod = shim_module(linker.store()); + let shim_inst = linker.instantiate(&shim_mod).unwrap(); + + let results = shim_inst + .get_func("int_float_args_shim") + .unwrap() + .call(&[0i32.into(), 123.45f32.into()]) + .unwrap(); + + assert_eq!(results.len(), 1, "one return value"); + assert_eq!( + results[0].unwrap_i32(), + types::Errno::Ok as i32, + "int_float_args errno" + ); +} + +fn run_double_int_return_float(linker: &wasmtime::Linker) { + let shim_mod = shim_module(linker.store()); + let shim_inst = linker.instantiate(&shim_mod).unwrap(); + + let input: i32 = 123; + let result_location: i32 = 0; + + let results = shim_inst + .get_func("double_int_return_float_shim") + .unwrap() + .call(&[input.into(), result_location.into()]) + .unwrap(); + + assert_eq!(results.len(), 1, "one return value"); + assert_eq!( + results[0].unwrap_i32(), + types::Errno::Ok as i32, + "double_int_return_float errno" + ); + + // The actual result is in memory: + let mem = shim_inst.get_memory("memory").unwrap(); + let mut result_bytes: [u8; 4] = [0, 0, 0, 0]; + mem.read(result_location as usize, &mut result_bytes) + .unwrap(); + let result = f32::from_le_bytes(result_bytes); + assert_eq!((input * 2) as f32, result); +} + +#[cfg(not(feature = "async"))] +#[test] +fn test_sync_host_func() { + let store = store(); + + let ctx = Rc::new(RefCell::new(Ctx)); + let atoms = Atoms::new(&store, ctx.clone()); + + let mut linker = wasmtime::Linker::new(&store); + atoms.add_to_linker(&mut linker).unwrap(); + + run_int_float_args(&linker); +} + +#[cfg(not(feature = "async"))] +#[test] +fn test_async_host_func() { + let store = store(); + + let ctx = Rc::new(RefCell::new(Ctx)); + let atoms = Atoms::new(&store, ctx.clone()); + + let mut linker = wasmtime::Linker::new(&store); + atoms.add_to_linker(&mut linker).unwrap(); + + run_double_int_return_float(&linker); +} + +#[cfg(not(feature = "async"))] +#[test] +fn test_sync_config_host_func() { + let mut config = wasmtime::Config::new(); + Atoms::add_to_config(&mut config); + + let engine = wasmtime::Engine::new(&config).unwrap(); + let store = wasmtime::Store::new(&engine); + + assert!(Atoms::set_context(&store, Ctx).is_ok()); + + let linker = wasmtime::Linker::new(&store); + run_int_float_args(&linker); +} + +#[cfg(not(feature = "async"))] +#[test] +fn test_async_config_host_func() { + let mut config = wasmtime::Config::new(); + Atoms::add_to_config(&mut config); + + let engine = wasmtime::Engine::new(&config).unwrap(); + let store = wasmtime::Store::new(&engine); + + assert!(Atoms::set_context(&store, Ctx).is_ok()); + + let linker = wasmtime::Linker::new(&store); + run_double_int_return_float(&linker); +} + +fn store() -> wasmtime::Store { + wasmtime::Store::new(&wasmtime::Engine::new(&wasmtime::Config::new()).unwrap()) +} + +// Wiggle expects the caller to have an exported memory. Wasmtime can only +// provide this if the caller is a WebAssembly module, so we need to write +// a shim module: +fn shim_module(store: &wasmtime::Store) -> wasmtime::Module { + wasmtime::Module::new( + store.engine(), + r#" + (module + (memory 1) + (export "memory" (memory 0)) + (import "atoms" "int_float_args" (func $int_float_args (param i32 f32) (result i32))) + (import "atoms" "double_int_return_float" (func $double_int_return_float (param i32 i32) (result i32))) + + (func $int_float_args_shim (param i32 f32) (result i32) + local.get 0 + local.get 1 + call $int_float_args + ) + (func $double_int_return_float_shim (param i32 i32) (result i32) + local.get 0 + local.get 1 + call $double_int_return_float + ) + (export "int_float_args_shim" (func $int_float_args_shim)) + (export "double_int_return_float_shim" (func $double_int_return_float_shim)) + ) + "#, + ) + .unwrap() +}