From 19e9bed9c3a2f4deae95d86dd6693677d6ec5585 Mon Sep 17 00:00:00 2001 From: arnaudgolfouse Date: Sat, 5 Aug 2023 15:08:02 +0200 Subject: [PATCH 1/3] Automatically check results of the various functions via `cargo test` --- examples/hello_c/hello.c | 2 +- examples/hello_rust/src/lib.rs | 2 +- examples/hello_zig/hello.zig | 2 +- examples/test-runner/Cargo.toml | 4 +- examples/test-runner/src/main.rs | 284 ++++++++++++++++++++++++------- 5 files changed, 229 insertions(+), 65 deletions(-) diff --git a/examples/hello_c/hello.c b/examples/hello_c/hello.c index 5364273..9343cb0 100644 --- a/examples/hello_c/hello.c +++ b/examples/hello_c/hello.c @@ -30,7 +30,7 @@ EMSCRIPTEN_KEEPALIVE void wasm_minimal_protocol_free_byte_buffer(uint8_t *ptr, EMSCRIPTEN_KEEPALIVE int32_t hello(void) { - const char static_message[] = "Hello world !"; + const char static_message[] = "Hello from wasm!!!"; const size_t length = sizeof(static_message); char *message = malloc(length); memcpy((void *)message, (void *)static_message, length); diff --git a/examples/hello_rust/src/lib.rs b/examples/hello_rust/src/lib.rs index febb741..4337780 100644 --- a/examples/hello_rust/src/lib.rs +++ b/examples/hello_rust/src/lib.rs @@ -9,7 +9,7 @@ pub fn hello() -> Vec { #[wasm_func] pub fn double_it(arg: &[u8]) -> Vec { - [arg, b".", arg].concat() + [arg, arg].concat() } #[wasm_func] diff --git a/examples/hello_zig/hello.zig b/examples/hello_zig/hello.zig index b6bfdf9..fb5fb59 100644 --- a/examples/hello_zig/hello.zig +++ b/examples/hello_zig/hello.zig @@ -17,7 +17,7 @@ export fn wasm_minimal_protocol_free_byte_buffer(ptr: [*]u8, len: usize) void { // === export fn hello() i32 { - const message = "Hello world !"; + const message = "Hello from wasm!!!"; var result = allocator.alloc(u8, message.len) catch return 1; @memcpy(result, message); wasm_minimal_protocol_send_result_to_host(result.ptr, result.len); diff --git a/examples/test-runner/Cargo.toml b/examples/test-runner/Cargo.toml index e9ca1fe..57e665e 100644 --- a/examples/test-runner/Cargo.toml +++ b/examples/test-runner/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" anyhow = "1.0.71" cfg-if = "1.0.0" host-wasmi = { path = "../host-wasmi" } -wasi-stub = { path = "../../wasi-stub", optional = true } +wasi-stub = { path = "../../wasi-stub" } [features] default = [] -wasi = ["dep:wasi-stub"] +wasi = [] diff --git a/examples/test-runner/src/main.rs b/examples/test-runner/src/main.rs index a14fe91..79d0ece 100644 --- a/examples/test-runner/src/main.rs +++ b/examples/test-runner/src/main.rs @@ -3,22 +3,18 @@ use anyhow::Result; use host_wasmi::PluginInstance; -use std::process::Command; - -#[cfg(not(feature = "wasi"))] -mod consts { - pub const RUST_TARGET: &str = "wasm32-unknown-unknown"; - pub const RUST_PATH: &str = - "examples/hello_rust/target/wasm32-unknown-unknown/debug/hello.wasm"; - pub const ZIG_TARGET: &str = "wasm32-freestanding"; -} +use std::{path::Path, process::Command}; -#[cfg(feature = "wasi")] -mod consts { - pub const RUST_TARGET: &str = "wasm32-wasi"; - pub const RUST_PATH: &str = "examples/hello_rust/target/wasm32-wasi/debug/hello.wasm"; - pub const ZIG_TARGET: &str = "wasm32-wasi"; -} +const WASI: bool = { + #[cfg(feature = "wasi")] + { + true + } + #[cfg(not(feature = "wasi"))] + { + false + } +}; fn main() -> Result<()> { let mut custom_run = false; @@ -26,59 +22,42 @@ fn main() -> Result<()> { if args.is_empty() { anyhow::bail!("1 argument required: 'rust', 'zig' or 'c'") } - #[cfg(feature = "wasi")] - println!("[INFO] The WASI functions will be stubbed (by `wasi-stub`) for this run"); - let plugin_binary = match args[0].as_str() { + if WASI { + println!("[INFO] The WASI functions will be stubbed (by `wasi-stub`) for this run"); + } + let mut plugin_binary = match args[0].as_str() { "rust" => { println!("=== compiling the Rust plugin"); - Command::new("cargo") - .arg("build") - .arg("--target") - .arg(consts::RUST_TARGET) - .current_dir("examples/hello_rust") - .spawn()? - .wait()?; + let result = compile_rust(WASI, ".")?; println!("==="); - println!("[INFO] getting wasm from: {}", consts::RUST_PATH); - std::fs::read(consts::RUST_PATH)? + println!( + "[INFO] getting wasm from: {}", + if WASI { + "examples/hello_rust/target/wasm32-wasi/debug/hello.wasm" + } else { + "examples/hello_rust/target/wasm32-unknown-unknown/debug/hello.wasm" + } + ); + result } "zig" => { println!("=== compiling the Zig plugin"); - Command::new("zig") - .arg("build-lib") - .arg("hello.zig") - .arg("-target") - .arg(consts::ZIG_TARGET) - .arg("-dynamic") - .arg("-rdynamic") - .current_dir("examples/hello_zig") - .spawn() - .expect("do you have zig installed and in the path?") - .wait()?; + let result = compile_zig(WASI, ".")?; println!("==="); println!("[INFO] getting wasm from: examples/hello_zig/hello.wasm"); - std::fs::read("examples/hello_zig/hello.wasm")? + result } "c" => { println!("=== compiling the C plugin"); - #[cfg(not(feature = "wasi"))] - eprintln!("WARNING: the C example should be compiled with `--features wasi`"); + if !WASI { + eprintln!("WARNING: the C example should be compiled with `--features wasi`"); + } println!("{}", std::env::current_dir().unwrap().display()); - Command::new("emcc") - .arg("--no-entry") - .arg("-s") - .arg("ERROR_ON_UNDEFINED_SYMBOLS=0") - .arg("-o") - .arg("hello.wasm") - .arg("hello.c") - .current_dir("examples/hello_c/") - .spawn() - .expect("do you have emcc installed and in the path?") - .wait()?; + let result = compile_c(".")?; println!("==="); println!("[INFO] getting wasm from: examples/hello_c/hello.wasm"); - std::fs::read("examples/hello_c/hello.wasm")? + result } "-i" | "--input" => { custom_run = true; @@ -94,13 +73,14 @@ fn main() -> Result<()> { _ => anyhow::bail!("unknown argument '{}'", args[0].as_str()), }; - #[cfg(feature = "wasi")] - let plugin_binary = { - println!("[INFO] Using wasi-stub"); - let res = wasi_stub::stub_wasi_functions(&plugin_binary)?; - println!("[INFO] WASI functions have been stubbed"); - res - }; + if WASI { + plugin_binary = { + println!("[INFO] Using wasi-stub"); + let res = wasi_stub::stub_wasi_functions(&plugin_binary)?; + println!("[INFO] WASI functions have been stubbed"); + res + }; + } let mut plugin_instance = PluginInstance::new_from_bytes(plugin_binary).unwrap(); if custom_run { @@ -148,3 +128,187 @@ fn main() -> Result<()> { Ok(()) } + +fn compile_rust(wasi: bool, dir_root: &str) -> Result> { + let target = if wasi { + "wasm32-wasi" + } else { + "wasm32-unknown-unknown" + }; + Command::new("cargo") + .arg("build") + .arg("--target") + .arg(target) + .current_dir(Path::new(dir_root).join("examples/hello_rust")) + .spawn() + .map_err(|err| anyhow::format_err!("while spawning the build command: {err}"))? + .wait()?; + let path = Path::new(dir_root).join(format!( + "examples/hello_rust/target/{target}/debug/hello.wasm" + )); + std::fs::read(&path) + .map_err(|err| anyhow::format_err!("while reading {}: {err}", path.display())) +} + +fn compile_zig(wasi: bool, dir_root: &str) -> Result> { + Command::new("zig") + .arg("build-lib") + .arg("hello.zig") + .arg("-target") + .arg(if wasi { + "wasm32-wasi" + } else { + "wasm32-freestanding" + }) + .arg("-dynamic") + .arg("-rdynamic") + .current_dir(Path::new(dir_root).join("examples/hello_zig")) + .spawn() + .expect("do you have zig installed and in the path?") + .wait()?; + let path = Path::new(dir_root).join("examples/hello_zig/hello.wasm"); + std::fs::read(&path) + .map_err(|err| anyhow::format_err!("while reading {}: {err}", path.display())) +} + +fn compile_c(dir_root: &str) -> Result> { + Command::new("emcc") + .arg("--no-entry") + .arg("-s") + .arg("ERROR_ON_UNDEFINED_SYMBOLS=0") + .arg("-o") + .arg("hello.wasm") + .arg("hello.c") + .current_dir(Path::new(dir_root).join("examples/hello_c/")) + .spawn() + .expect("do you have emcc installed and in the path?") + .wait()?; + let path = Path::new(dir_root).join("examples/hello_c/hello.wasm"); + std::fs::read(&path) + .map_err(|err| anyhow::format_err!("while reading {}: {err}", path.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + /// Needed to avoid tests walking all over each others. + static LOCK: Mutex<()> = Mutex::new(()); + const ROOT_DIR: &str = "../.."; + + fn run_function( + plugin_instance: &mut PluginInstance, + function: &str, + args: &[&[u8]], + ) -> Result { + match plugin_instance.call(function, args) { + Ok(res) => match std::str::from_utf8(res.get()) { + Ok(s) => Ok(s.to_owned()), + Err(_) => panic!("Error: function call '{function}' did not return UTF-8"), + }, + Err(err) => Err(err), + } + } + + fn test_default_functions(plugin_instance: &mut PluginInstance) -> bool { + let mut all_passed = true; + for (function, args, expected) in [ + ("hello", &[] as &[&[u8]], Ok("Hello from wasm!!!")), + ("double_it", &[b"double me!!"], Ok("double me!!double me!!")), + ("concatenate", &[b"val1", b"value2"], Ok("val1*value2")), + ( + "shuffle", + &[b"value1", b"value2", b"value3"], + Ok("value3-value1-value2"), + ), + ("returns_ok", &[], Ok("This is an `Ok`")), + ( + "returns_err", + &[], + Err("plugin errored with: 'This is an `Err`'"), + ), + ("will_panic", &[], Err("plugin panicked")), + ] { + let res = run_function(plugin_instance, function, args); + let res = res.as_ref().map(|s| s.as_str()).map_err(|s| s.as_str()); + if expected != res { + all_passed = false; + eprintln!("Incorrect result when calling {}:", function); + eprintln!(" - expected: {:?}", expected); + eprintln!(" - got: {:?}", res); + } else { + println!("calling {function} returned: {:?}", res) + } + } + all_passed + } + + #[test] + fn rust_no_wasi() -> Result<()> { + let lock = LOCK.lock(); + let binary = compile_rust(false, ROOT_DIR)?; + drop(lock); + let mut plugin_instance = PluginInstance::new_from_bytes(binary).unwrap(); + if !test_default_functions(&mut plugin_instance) { + anyhow::bail!("Some incorrect result detected"); + } else { + Ok(()) + } + } + + #[test] + fn rust_wasi() -> Result<()> { + let lock = LOCK.lock(); + let binary = compile_rust(true, ROOT_DIR)?; + drop(lock); + let binary = wasi_stub::stub_wasi_functions(&binary)?; + let mut plugin_instance = PluginInstance::new_from_bytes(binary).unwrap(); + if !test_default_functions(&mut plugin_instance) { + anyhow::bail!("Some incorrect result detected"); + } else { + Ok(()) + } + } + + #[test] + fn zig_no_wasi() -> Result<()> { + let lock = LOCK.lock(); + let binary = compile_zig(false, ROOT_DIR)?; + drop(lock); + let mut plugin_instance = PluginInstance::new_from_bytes(binary).unwrap(); + if !test_default_functions(&mut plugin_instance) { + anyhow::bail!("Some incorrect result detected"); + } else { + Ok(()) + } + } + + #[test] + fn zig_wasi() -> Result<()> { + let lock = LOCK.lock(); + let binary = compile_zig(true, ROOT_DIR)?; + drop(lock); + let binary = wasi_stub::stub_wasi_functions(&binary)?; + let mut plugin_instance = PluginInstance::new_from_bytes(binary).unwrap(); + if !test_default_functions(&mut plugin_instance) { + anyhow::bail!("Some incorrect result detected"); + } else { + Ok(()) + } + } + + #[test] + fn c_wasi() -> Result<()> { + let lock = LOCK.lock(); + let binary = compile_c(ROOT_DIR)?; + drop(lock); + let binary = wasi_stub::stub_wasi_functions(&binary)?; + let mut plugin_instance = PluginInstance::new_from_bytes(binary).unwrap(); + if !test_default_functions(&mut plugin_instance) { + anyhow::bail!("Some incorrect result detected"); + } else { + Ok(()) + } + } +} From 51be2adb771a9fb4459e1c06bdcebf0fe98e3c9c Mon Sep 17 00:00:00 2001 From: arnaudgolfouse Date: Sun, 6 Aug 2023 12:44:38 +0200 Subject: [PATCH 2/3] Use an iterator instead of a slice for args --- examples/host-wasmi/src/lib.rs | 22 +++++++++++----------- examples/test-runner/src/main.rs | 12 ++++-------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/examples/host-wasmi/src/lib.rs b/examples/host-wasmi/src/lib.rs index c41c969..4dffc2c 100644 --- a/examples/host-wasmi/src/lib.rs +++ b/examples/host-wasmi/src/lib.rs @@ -123,12 +123,17 @@ impl PluginInstance { }) } - fn write(&mut self, args: &[&[u8]]) { - self.store.data_mut().arg_buffer = args.concat(); - } - - pub fn call(&mut self, function: &str, args: &[&[u8]]) -> Result { - self.write(args); + pub fn call<'a>( + &mut self, + function: &str, + args: impl IntoIterator, + ) -> Result { + let mut result_args = Vec::new(); + let arg_buffer = &mut self.store.data_mut().arg_buffer; + for arg in args { + result_args.push(Value::I32(arg.len() as _)); + arg_buffer.extend_from_slice(arg); + } let (_, function) = self .functions @@ -136,11 +141,6 @@ impl PluginInstance { .find(|(s, _)| s == function) .ok_or(format!("Plugin doesn't have the method: {function}"))?; - let result_args = args - .iter() - .map(|a| Value::I32(a.len() as _)) - .collect::>(); - let mut code = [Value::I32(2)]; let is_err = function .call(&mut self.store, &result_args, &mut code) diff --git a/examples/test-runner/src/main.rs b/examples/test-runner/src/main.rs index 79d0ece..45d480d 100644 --- a/examples/test-runner/src/main.rs +++ b/examples/test-runner/src/main.rs @@ -85,12 +85,8 @@ fn main() -> Result<()> { let mut plugin_instance = PluginInstance::new_from_bytes(plugin_binary).unwrap(); if custom_run { let function = args[2].as_str(); - let args = args - .iter() - .skip(3) - .map(|x| x.as_bytes()) - .collect::>(); - let result = match plugin_instance.call(function, &args) { + let args = args.iter().skip(3).map(|x| x.as_bytes()); + let result = match plugin_instance.call(function, args) { Ok(res) => res, Err(err) => { eprintln!("Error: {err}"); @@ -113,7 +109,7 @@ fn main() -> Result<()> { ("returns_err", &[]), ("will_panic", &[]), ] { - let result = match plugin_instance.call(function, args) { + let result = match plugin_instance.call(function, args.iter().copied()) { Ok(res) => res, Err(err) => { eprintln!("Error: {err}"); @@ -202,7 +198,7 @@ mod tests { function: &str, args: &[&[u8]], ) -> Result { - match plugin_instance.call(function, args) { + match plugin_instance.call(function, args.iter().copied()) { Ok(res) => match std::str::from_utf8(res.get()) { Ok(s) => Ok(s.to_owned()), Err(_) => panic!("Error: function call '{function}' did not return UTF-8"), From b56ffbae72873ff747dc25850b4e5c30ac16a4f8 Mon Sep 17 00:00:00 2001 From: arnaudgolfouse Date: Sun, 6 Aug 2023 12:56:50 +0200 Subject: [PATCH 3/3] Handle plugin errors better --- examples/host-wasmi/src/lib.rs | 51 +++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/examples/host-wasmi/src/lib.rs b/examples/host-wasmi/src/lib.rs index 4dffc2c..2e42982 100644 --- a/examples/host-wasmi/src/lib.rs +++ b/examples/host-wasmi/src/lib.rs @@ -30,13 +30,15 @@ impl<'a> ReturnedData<'a> { impl Drop for ReturnedData<'_> { fn drop(&mut self) { - self.free_function - .call( - &mut *self.context_mut, - &[Value::I32(self.ptr as _), Value::I32(self.len as _)], - &mut [], - ) - .unwrap(); + if self.ptr != 0 { + self.free_function + .call( + &mut *self.context_mut, + &[Value::I32(self.ptr as _), Value::I32(self.len as _)], + &mut [], + ) + .unwrap(); + } } } @@ -128,8 +130,12 @@ impl PluginInstance { function: &str, args: impl IntoIterator, ) -> Result { + self.store.data_mut().result_ptr = 0; + self.store.data_mut().result_len = 0; + let mut result_args = Vec::new(); let arg_buffer = &mut self.store.data_mut().arg_buffer; + arg_buffer.clear(); for arg in args { result_args.push(Value::I32(arg.len() as _)); arg_buffer.extend_from_slice(arg); @@ -139,20 +145,20 @@ impl PluginInstance { .functions .iter() .find(|(s, _)| s == function) - .ok_or(format!("Plugin doesn't have the method: {function}"))?; - - let mut code = [Value::I32(2)]; - let is_err = function - .call(&mut self.store, &result_args, &mut code) - .is_err(); - let code = if is_err { - Value::I32(2) - } else { - code.first().cloned().unwrap_or(Value::I32(3)) // if the function returns nothing - }; + .ok_or(format!("plugin doesn't have the method: {function}"))?; - let (ptr, len) = (self.store.data().result_ptr, self.store.data().result_len); + let mut code = Value::I32(2); + let ty = function.ty(&self.store); + if ty.params().len() != result_args.len() { + return Err("incorrect number of arguments".to_string()); + } + let call_result = function.call( + &mut self.store, + &result_args, + std::array::from_mut(&mut code), + ); + let (ptr, len) = (self.store.data().result_ptr, self.store.data().result_len); let result = ReturnedData { memory: self.memory, ptr, @@ -161,13 +167,18 @@ impl PluginInstance { context_mut: &mut self.store, }; + match call_result { + Ok(()) => {} + Err(wasmi::Error::Trap(_)) => return Err("plugin panicked".to_string()), + Err(_) => return Err("plugin did not respect the protocol".to_string()), + }; + match code { Value::I32(0) => Ok(result), Value::I32(1) => Err(match std::str::from_utf8(result.get()) { Ok(err) => format!("plugin errored with: '{}'", err,), Err(_) => String::from("plugin errored and did not return valid UTF-8"), }), - Value::I32(2) => Err("plugin panicked".to_string()), _ => Err("plugin did not respect the protocol".to_string()), } }