diff --git a/.tarpaulin.toml b/.tarpaulin.toml index 9f7dae3decf..78b59bfb065 100644 --- a/.tarpaulin.toml +++ b/.tarpaulin.toml @@ -1,17 +1,16 @@ [cranelift_coverage] features = "cranelift,singlepass,llvm,test-no-traps,test-cranelift" +examples = ["early-exit", "engine-jit", "engine-native", "engine-headless", "cross-compilation", "compiler-cranelift", "exported-function", "wasi"] release = true [llvm_coverage] features = "cranelift,singlepass,llvm,test-no-traps,test-llvm" +examples = ["compiler-llvm"] release = true [singlepass_coverage] features = "cranelift,singlepass,llvm,test-no-traps,test-singlepass" -release = true - -[feature_a_and_b_coverage] -features = "feature_a feature_b" +examples = ["compiler-singlepass"] release = true [report] diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7288f069a..267fd762c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## **[Unreleased]** +- [#1657](https://github.com/wasmerio/wasmer/pull/1657) Implement `wasm_trap_t` and `wasm_frame_t` for Wasm C API; add examples in Rust and C of exiting early with a host function. - [#1645](https://github.com/wasmerio/wasmer/pull/1645) Move the install script to https://github.com/wasmerio/wasmer-install ## 1.0.0-alpha3 - 2020-09-14 diff --git a/Cargo.toml b/Cargo.toml index fb84f986c14..f034431fab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,11 @@ test-no-traps = ["wasmer-wast/test-no-traps"] name = "static_and_dynamic_functions" harness = false +[[example]] +name = "early-exit" +path = "examples/early_exit.rs" +required-features = ["cranelift"] + [[example]] name = "engine-jit" path = "examples/engine_jit.rs" diff --git a/examples/early_exit.rs b/examples/early_exit.rs new file mode 100644 index 00000000000..814ab01ff6c --- /dev/null +++ b/examples/early_exit.rs @@ -0,0 +1,82 @@ +//! This example shows how the host can terminate execution of Wasm early from +//! inside a host function called by the Wasm. + +use anyhow::bail; +use std::fmt; +use wasmer::{imports, wat2wasm, Function, Instance, Module, NativeFunc, RuntimeError, Store}; +use wasmer_compiler_cranelift::Cranelift; +use wasmer_engine_jit::JIT; + +// First we need to create an error type that we'll use to signal the end of execution. +#[derive(Debug, Clone, Copy)] +struct ExitCode(u32); + +// This type must implement `std::error::Error` so we must also implement `std::fmt::Display` for it. +impl fmt::Display for ExitCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +// And then we implement `std::error::Error`. +impl std::error::Error for ExitCode {} + +// The host function that we'll use to terminate execution. +fn early_exit() { + // This is where it happens. + RuntimeError::raise(Box::new(ExitCode(1))); +} + +fn main() -> anyhow::Result<()> { + // Let's declare the Wasm module with the text representation. + let wasm_bytes = wat2wasm( + br#" +(module + (type $run_t (func (param i32 i32) (result i32))) + (type $early_exit_t (func (param) (result))) + (import "env" "early_exit" (func $early_exit (type $early_exit_t))) + (func $run (type $run_t) (param $x i32) (param $y i32) (result i32) + (call $early_exit) + (i32.add + local.get $x + local.get $y)) + (export "run" (func $run))) +"#, + )?; + + let store = Store::new(&JIT::new(&Cranelift::default()).engine()); + let module = Module::new(&store, wasm_bytes)?; + + let import_object = imports! { + "env" => { + "early_exit" => Function::new_native(&store, early_exit), + } + }; + let instance = Instance::new(&module, &import_object)?; + + // Get the `run` function which we'll use as our entrypoint. + let run_func: NativeFunc<(i32, i32), i32> = + instance.exports.get_native_function("run").unwrap(); + + // When we call a function it can either succeed or fail. + match run_func.call(1, 7) { + Ok(result) => { + bail!( + "Expected early termination with `ExitCode`, found: {}", + result + ); + } + // We're expecting it to fail. + // We attempt to downcast the error into the error type that we were expecting. + Err(e) => match e.downcast::() { + // We found the exit code used to terminate execution. + Ok(exit_code) => { + println!("Exited early with exit code: {}", exit_code); + Ok(()) + } + Err(e) => { + bail!("Unknown error `{}` found. expected `ErrorCode`", e); + } + }, + } +} diff --git a/examples/engine_cross_compilation.rs b/examples/engine_cross_compilation.rs index 1c979513872..272da2cfcee 100644 --- a/examples/engine_cross_compilation.rs +++ b/examples/engine_cross_compilation.rs @@ -27,7 +27,7 @@ use wasmer_engine_native::Native; fn main() -> Result<(), Box> { // Let's declare the Wasm module with the text representation. let wasm_bytes = wat2wasm( - r#" + br#" (module (type $sum_t (func (param i32 i32) (result i32))) (func $sum_f (type $sum_t) (param $x i32) (param $y i32) (result i32) @@ -35,8 +35,7 @@ fn main() -> Result<(), Box> { local.get $y i32.add) (export "sum" (func $sum_f))) -"# - .as_bytes(), +"#, )?; // Define a compiler configuration. diff --git a/examples/wasi.rs b/examples/wasi.rs index 72617e83596..c9c779b7450 100644 --- a/examples/wasi.rs +++ b/examples/wasi.rs @@ -21,9 +21,9 @@ use wasmer_engine_jit::JIT; use wasmer_wasi::WasiState; fn main() -> Result<(), Box> { - let wasm_path = format!( - "{}/tests/wasi-wast/wasi/unstable/hello.wasm", - std::env::var("CARGO_MANIFEST_DIR").unwrap() + let wasm_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/wasi-wast/wasi/unstable/hello.wasm" ); // Let's declare the Wasm module with the text representation. let wasm_bytes = std::fs::read(wasm_path)?; diff --git a/lib/api/src/lib.rs b/lib/api/src/lib.rs index f5631870dfb..6725141aec7 100644 --- a/lib/api/src/lib.rs +++ b/lib/api/src/lib.rs @@ -70,8 +70,8 @@ pub use wasmer_compiler::{ }; pub use wasmer_compiler::{CpuFeature, Features, Target}; pub use wasmer_engine::{ - ChainableNamedResolver, DeserializeError, Engine, InstantiationError, LinkError, NamedResolver, - NamedResolverChain, Resolver, RuntimeError, SerializeError, + ChainableNamedResolver, DeserializeError, Engine, FrameInfo, InstantiationError, LinkError, + NamedResolver, NamedResolverChain, Resolver, RuntimeError, SerializeError, }; pub use wasmer_types::{ Atomically, Bytes, GlobalInit, LocalFunctionIndex, MemoryView, Pages, ValueType, diff --git a/lib/c-api/src/wasm_c_api/mod.rs b/lib/c-api/src/wasm_c_api/mod.rs index 79737d3ac50..edef62ad815 100644 --- a/lib/c-api/src/wasm_c_api/mod.rs +++ b/lib/c-api/src/wasm_c_api/mod.rs @@ -17,9 +17,9 @@ use crate::c_try; use crate::ordered_resolver::OrderedResolver; use wasmer::{ - Engine, ExportType, Extern, ExternType, Function, FunctionType, Global, GlobalType, ImportType, - Instance, Memory, MemoryType, Module, Mutability, Pages, RuntimeError, Store, Table, TableType, - Val, ValType, + Engine, ExportType, Extern, ExternType, FrameInfo, Function, FunctionType, Global, GlobalType, + ImportType, Instance, Memory, MemoryType, Module, Mutability, Pages, RuntimeError, Store, + Table, TableType, Val, ValType, }; #[cfg(feature = "jit")] use wasmer_engine_jit::JIT; @@ -712,8 +712,11 @@ pub unsafe extern "C" fn wasm_func_new( num_rets ]; - let _traps = callback(processed_args.as_ptr(), results.as_mut_ptr()); - // TODO: do something with `traps` + let trap = callback(processed_args.as_ptr(), results.as_mut_ptr()); + if !trap.is_null() { + let trap: Box = Box::from_raw(trap); + RuntimeError::raise(Box::new(trap.inner)); + } let processed_results = results .into_iter() @@ -783,7 +786,7 @@ pub unsafe extern "C" fn wasm_func_call( func: &wasm_func_t, args: *const wasm_val_t, results: *mut wasm_val_t, -) -> Option> { +) -> Option> { let num_params = func.inner.ty().params().len(); let params: Vec = (0..num_params) .map(|i| (&(*args.add(i))).try_into()) @@ -798,7 +801,7 @@ pub unsafe extern "C" fn wasm_func_call( } None } - Err(e) => Some(NonNull::new_unchecked(Box::into_raw(Box::new(e)) as _)), + Err(e) => Some(Box::new(e.into())), } } @@ -1050,6 +1053,38 @@ macro_rules! wasm_declare_vec { } } + + impl<'a> From]>> for [] { + fn from(other: Vec<[]>) -> Self { + let mut boxed_slice = other.into_boxed_slice(); + let size = boxed_slice.len(); + let data = boxed_slice.as_mut_ptr(); + mem::forget(boxed_slice); + Self { + size, + data, + } + } + } + + impl<'a, T: Into<[]> + Clone> From<&'a [T]> for [] { + fn from(other: &'a [T]) -> Self { + let size = other.len(); + let mut copied_data = other + .iter() + .cloned() + .map(Into::into) + .collect::]>>() + .into_boxed_slice(); + let data = copied_data.as_mut_ptr(); + mem::forget(copied_data); + Self { + size, + data, + } + } + } + // TODO: investigate possible memory leak on `init` (owned pointer) #[no_mangle] pub unsafe extern "C" fn [](out: *mut [], length: usize, init: *mut []) { @@ -1150,44 +1185,53 @@ pub struct wasm_ref_t; // opaque type which is a `RuntimeError` #[repr(C)] -pub struct wasm_trap_t {} +pub struct wasm_trap_t { + inner: RuntimeError, +} -#[no_mangle] -pub unsafe extern "C" fn wasm_trap_delete(trap: Option>) { - if let Some(t_inner) = trap { - let _ = Box::from_raw(t_inner.cast::().as_ptr()); +impl From for wasm_trap_t { + fn from(other: RuntimeError) -> Self { + Self { inner: other } } } #[no_mangle] -pub unsafe extern "C" fn wasm_trap_message( - trap: *const wasm_trap_t, - out_ptr: *mut wasm_byte_vec_t, -) { - let re = &*(trap as *const RuntimeError); - // this code assumes no nul bytes appear in the message - let mut message = format!("{}\0", re); - message.shrink_to_fit(); - - // TODO use `String::into_raw_parts` when it gets stabilized - (*out_ptr).size = message.as_bytes().len(); - (*out_ptr).data = message.as_mut_ptr(); - mem::forget(message); -} - -// in trap/RuntimeError we need to store -// 1. message -// 2. origin (frame); frame contains: -// 1. func index -// 2. func offset -// 3. module offset -// 4. which instance this was apart of - -/*#[no_mangle] -pub unsafe extern "C" fn wasm_trap_trace(trap: *const wasm_trap_t, out_ptr: *mut wasm_frame_vec_t) { - let re = &*(trap as *const RuntimeError); - todo!() -}*/ +pub unsafe extern "C" fn wasm_trap_new( + _store: &mut wasm_store_t, + message: &wasm_message_t, +) -> Option> { + let message_bytes: &[u8] = message.into_slice()?; + let message_str = c_try!(std::str::from_utf8(message_bytes)); + let runtime_error = RuntimeError::new(message_str); + let trap = runtime_error.into(); + + Some(Box::new(trap)) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_delete(_trap: Option>) {} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_message(trap: &wasm_trap_t, out_ptr: &mut wasm_byte_vec_t) { + let message = trap.inner.message(); + let byte_vec: wasm_byte_vec_t = message.into_bytes().into(); + out_ptr.size = byte_vec.size; + out_ptr.data = byte_vec.data; +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_origin(trap: &wasm_trap_t) -> Option> { + trap.inner.trace().first().map(Into::into).map(Box::new) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_trace(trap: &wasm_trap_t, out_ptr: &mut wasm_frame_vec_t) { + let frames = trap.inner.trace(); + let frame_vec: wasm_frame_vec_t = frames.into(); + + out_ptr.size = frame_vec.size; + out_ptr.data = frame_vec.data; +} #[repr(C)] pub struct wasm_extern_t { @@ -1543,9 +1587,52 @@ pub unsafe extern "C" fn wasm_functype_results(ft: &wasm_functype_t) -> *const w out as *const _ } -#[derive(Debug)] +#[derive(Debug, Clone)] #[repr(C)] -pub struct wasm_frame_t {} +pub struct wasm_frame_t { + info: FrameInfo, +} + +impl<'a> From<&'a FrameInfo> for wasm_frame_t { + fn from(other: &'a FrameInfo) -> Self { + other.clone().into() + } +} + +impl From for wasm_frame_t { + fn from(other: FrameInfo) -> Self { + Self { info: other } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_copy(frame: &wasm_frame_t) -> Box { + Box::new(frame.clone()) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_delete(_frame: Option>) {} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_instance(frame: &wasm_frame_t) -> *const wasm_instance_t { + //todo!("wasm_frame_instance") + std::ptr::null() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_func_index(frame: &wasm_frame_t) -> u32 { + frame.info.func_index() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_func_offset(frame: &wasm_frame_t) -> usize { + frame.info.func_offset() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_module_offset(frame: &wasm_frame_t) -> usize { + frame.info.module_offset() +} wasm_declare_vec!(frame); @@ -1769,6 +1856,9 @@ pub unsafe extern "C" fn wasm_tabletype_as_externtype( #[allow(non_camel_case_types)] type wasm_name_t = wasm_byte_vec_t; +#[allow(non_camel_case_types)] +type wasm_message_t = wasm_byte_vec_t; + #[repr(C)] #[allow(non_camel_case_types)] pub struct wasm_exporttype_t { diff --git a/lib/c-api/tests/CMakeLists.txt b/lib/c-api/tests/CMakeLists.txt index 728e11181a1..24a135baddb 100644 --- a/lib/c-api/tests/CMakeLists.txt +++ b/lib/c-api/tests/CMakeLists.txt @@ -31,10 +31,11 @@ add_executable(wasm-c-api-serialize wasm-c-api/example/serialize.c) #add_executable(wasm-c-api-start wasm-c-api/example/start.c) #add_executable(wasm-c-api-table wasm-c-api/example/table.c) #add_executable(wasm-c-api-threads wasm-c-api/example/threads.c) -#add_executable(wasm-c-api-trap wasm-c-api/example/trap.c) +add_executable(wasm-c-api-trap wasm-c-api/example/trap.c) # Custom Wasm C API tests add_executable(wasm-c-api-wasi wasm-c-api-wasi.c) +add_executable(wasm-c-api-early-exit wasm-c-api-early-exit.c) if (DEFINED WASI_TESTS) add_executable(test-wasi-import-object test-wasi-import-object.c) @@ -237,15 +238,19 @@ add_test(NAME wasm-c-api-serialize # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/wasm-c-api/example/ #) -#target_link_libraries(wasm-c-api-trap general ${WASMER_LIB}) -#target_compile_options(wasm-c-api-trap PRIVATE ${COMPILER_OPTIONS}) -#add_test(NAME wasm-c-api-trap -# COMMAND wasm-c-api-trap -# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/wasm-c-api/example/ -#) +target_link_libraries(wasm-c-api-trap general ${WASMER_LIB}) +target_compile_options(wasm-c-api-trap PRIVATE ${COMPILER_OPTIONS}) +add_test(NAME wasm-c-api-trap + COMMAND wasm-c-api-trap + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/wasm-c-api/example/ +) set_property(TARGET wasm-c-api-wasi PROPERTY C_STANDARD 11) target_link_libraries(wasm-c-api-wasi general ${WASMER_LIB}) target_compile_options(wasm-c-api-wasi PRIVATE ${COMPILER_OPTIONS}) add_test(wasm-c-api-wasi wasm-c-api-wasi) +set_property(TARGET wasm-c-api-early-exit PROPERTY C_STANDARD 11) +target_link_libraries(wasm-c-api-early-exit general ${WASMER_LIB}) +target_compile_options(wasm-c-api-early-exit PRIVATE ${COMPILER_OPTIONS}) +add_test(wasm-c-api-early-exit wasm-c-api-early-exit) diff --git a/lib/c-api/tests/assets/call_trap.wasm b/lib/c-api/tests/assets/call_trap.wasm new file mode 100644 index 00000000000..44cbb279753 Binary files /dev/null and b/lib/c-api/tests/assets/call_trap.wasm differ diff --git a/lib/c-api/tests/assets/call_trap.wat b/lib/c-api/tests/assets/call_trap.wat new file mode 100644 index 00000000000..febd5f88d80 --- /dev/null +++ b/lib/c-api/tests/assets/call_trap.wat @@ -0,0 +1,11 @@ +(module + (type $run_t (func (param i32 i32) (result i32))) + (type $early_exit_t (func (param) (result))) + (import "env" "early_exit" (func $early_exit (type $early_exit_t))) + (func $run (type $run_t) (param $x i32) (param $y i32) (result i32) + (call $early_exit) + (i32.add + (local.get $x) + (local.get $y))) + (export "run" (func $run))) + diff --git a/lib/c-api/tests/wasm-c-api-early-exit.c b/lib/c-api/tests/wasm-c-api-early-exit.c new file mode 100644 index 00000000000..903fdb7db1e --- /dev/null +++ b/lib/c-api/tests/wasm-c-api-early-exit.c @@ -0,0 +1,162 @@ +#include +#include +#include +#include + +#include "wasm.h" +#include "wasmer_wasm.h" + +// Use the last_error API to retrieve error messages +void print_wasmer_error() { + int error_len = wasmer_last_error_length(); + printf("Error len: `%d`\n", error_len); + char *error_str = malloc(error_len); + wasmer_last_error_message(error_str, error_len); + printf("Error str: `%s`\n", error_str); +} + +void print_frame(wasm_frame_t* frame) { + printf("> %p @ 0x%zx = %"PRIu32".0x%zx\n", + wasm_frame_instance(frame), + wasm_frame_module_offset(frame), + wasm_frame_func_index(frame), + wasm_frame_func_offset(frame) + ); +} + +wasm_store_t *store = NULL; + +own wasm_trap_t *early_exit(const wasm_val_t args[], wasm_val_t results[]) { + own wasm_message_t trap_message; + wasm_name_new_from_string(&trap_message, "trapping from a host import"); + own wasm_trap_t *trap = wasm_trap_new(store, &trap_message); + wasm_name_delete(&trap_message); + return trap; +} + +int main(int argc, const char *argv[]) { + // Initialize. + printf("Initializing...\n"); + wasm_engine_t *engine = wasm_engine_new(); + store = wasm_store_new(engine); + + // Load binary. + printf("Loading binary...\n"); + FILE *file = fopen("assets/call_trap.wasm", "r"); + if (!file) { + printf("> Error loading module!\n"); + return 1; + } + fseek(file, 0L, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0L, SEEK_SET); + wasm_byte_vec_t binary; + wasm_byte_vec_new_uninitialized(&binary, file_size); + if (fread(binary.data, file_size, 1, file) != 1) { + printf("> Error loading module!\n"); + return 1; + } + fclose(file); + + // Compile. + printf("Compiling module...\n"); + own wasm_module_t *module = wasm_module_new(store, &binary); + if (!module) { + printf("> Error compiling module!\n"); + return 1; + } + + wasm_byte_vec_delete(&binary); + + // Instantiate. + printf("Instantiating module...\n"); + + wasm_functype_t *host_func_type = wasm_functype_new_0_0(); + wasm_func_t *host_func = wasm_func_new(store, host_func_type, early_exit); + + wasm_functype_delete(host_func_type); + + const wasm_extern_t *imports[] = {wasm_func_as_extern(host_func)}; + own wasm_instance_t *instance = + wasm_instance_new(store, module, imports, NULL); + if (!instance) { + printf("> Error instantiating module!\n"); + print_wasmer_error(); + return 1; + } + + wasm_func_delete(host_func); + + // Extract export. + printf("Extracting export...\n"); + own wasm_extern_vec_t exports; + wasm_instance_exports(instance, &exports); + if (exports.size == 0) { + printf("> Error accessing exports!\n"); + return 1; + } + fprintf(stderr, "found %zu exports\n", exports.size); + + wasm_module_delete(module); + wasm_instance_delete(instance); + + wasm_func_t *run_func = wasm_extern_as_func(exports.data[0]); + if (run_func == NULL) { + printf("> Error accessing export!\n"); + print_wasmer_error(); + return 1; + } + + // Call. + printf("Calling export...\n"); + own const wasm_val_t args[] = { + {.kind = WASM_I32, .of = {.i32 = 1}}, + {.kind = WASM_I32, .of = {.i32 = 7}}, + }; + own wasm_val_t rets[1] = {}; + own wasm_trap_t *trap = wasm_func_call(run_func, args, rets); + if (!trap) { + printf("> Error calling function: expected trap!\n"); + return 1; + } + + printf("Printing message...\n"); + own wasm_name_t message; + wasm_trap_message(trap, &message); + printf("> %s\n", message.data); + + printf("Printing origin...\n"); + own wasm_frame_t* frame = wasm_trap_origin(trap); + if (frame) { + print_frame(frame); + wasm_frame_delete(frame); + } else { + printf("> Empty origin.\n"); + } + + printf("Printing trace...\n"); + own wasm_frame_vec_t trace; + wasm_trap_trace(trap, &trace); + if (trace.size > 0) { + for (size_t i = 0; i < trace.size; ++i) { + print_frame(trace.data[i]); + } + } else { + printf("> Empty trace.\n"); + } + + wasm_frame_vec_delete(&trace); + wasm_trap_delete(trap); + wasm_name_delete(&message); + + wasm_extern_vec_delete(&exports); + + // Shut down. + printf("Shutting down...\n"); + wasm_store_delete(store); + wasm_engine_delete(engine); + + // All done. + printf("Done.\n"); + return 0; +} diff --git a/lib/engine/src/trap/frame_info.rs b/lib/engine/src/trap/frame_info.rs index 310dc56796d..464131f1949 100644 --- a/lib/engine/src/trap/frame_info.rs +++ b/lib/engine/src/trap/frame_info.rs @@ -282,7 +282,7 @@ pub fn register( /// each frame is described by this structure. /// /// [`Trap`]: crate::Trap -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FrameInfo { module_name: String, func_index: u32,