diff --git a/crates/bench-api/src/lib.rs b/crates/bench-api/src/lib.rs index a12268e6de..a2958f09a8 100644 --- a/crates/bench-api/src/lib.rs +++ b/crates/bench-api/src/lib.rs @@ -1,145 +1,189 @@ -//! Expose a C-compatible API for controlling the Wasmtime engine during benchmarking. The API expects very sequential -//! use: -//! - `engine_create` -//! - `engine_compile_module` -//! - `engine_instantiate_module` -//! - `engine_execute_module` -//! - `engine_free` +//! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation, +//! and execution. //! -//! An example of this C-style usage, without error checking, is shown below: +//! The API expects sequential calls to: +//! +//! - `wasm_bench_create` +//! - `wasm_bench_compile` +//! - `wasm_bench_instantiate` +//! - `wasm_bench_execute` +//! - `wasm_bench_free` +//! +//! You may repeat this sequence of calls multiple times to take multiple +//! measurements of compilation, instantiation, and execution time within a +//! single process. +//! +//! All API calls must happen on the same thread. +//! +//! Functions which return pointers use null as an error value. Function which +//! return `int` use `0` as OK and non-zero as an error value. +//! +//! # Example //! //! ``` //! use wasmtime_bench_api::*; -//! let module = wat::parse_bytes(br#"(module -//! (func $bench_start (import "bench" "start")) -//! (func $bench_end (import "bench" "end")) -//! (func $start (export "_start") -//! (call $bench_start) (i32.const 2) (i32.const 2) (i32.add) (drop) (call $bench_end)) -//! )"#).unwrap(); -//! let engine = unsafe { engine_create(module.as_ptr(), module.len()) }; //! -//! // Start compilation timer. -//! unsafe { engine_compile_module(engine) }; -//! // End compilation timer. +//! let engine = unsafe { wasm_bench_create() }; +//! assert!(!engine.is_null()); //! -//! // The Wasm benchmark will expect us to provide functions to start ("bench" "start") and stop ("bench" "stop") the -//! // measurement counters/timers during execution; here we provide a no-op implementation. -//! extern "C" fn noop() {} +//! let wasm = wat::parse_bytes(br#" +//! (module +//! (func $bench_start (import "bench" "start")) +//! (func $bench_end (import "bench" "end")) +//! (func $start (export "_start") +//! call $bench_start +//! i32.const 1 +//! i32.const 2 +//! i32.add +//! drop +//! call $bench_end +//! ) +//! ) +//! "#).unwrap(); //! -//! // Start instantiation timer. -//! unsafe { engine_instantiate_module(engine, noop, noop) }; -//! // End instantiation timer. +//! // Start your compilation timer here. +//! let code = unsafe { wasm_bench_compile(engine, wasm.as_ptr(), wasm.len()) }; +//! // End your compilation timer here. +//! assert_eq!(code, OK); //! -//! // No need to start timers for the execution since, by convention, the timer functions we passed during -//! // instantiation will be called by the benchmark at the appropriate time (before and after the benchmarked section). -//! unsafe { engine_execute_module(engine) }; +//! // The Wasm benchmark will expect us to provide functions to start ("bench" +//! // "start") and stop ("bench" "stop") the measurement counters/timers during +//! // execution. +//! extern "C" fn bench_start() { +//! // Start your execution timer here. +//! } +//! extern "C" fn bench_stop() { +//! // End your execution timer here. +//! } //! -//! unsafe { engine_free(engine) } +//! // Start your instantiation timer here. +//! let code = unsafe { wasm_bench_instantiate(engine, bench_start, bench_stop) }; +//! // End your instantiation timer here. +//! assert_eq!(code, OK); +//! +//! // No need to start timers for the execution since, by convention, the timer +//! // functions we passed during instantiation will be called by the benchmark +//! // at the appropriate time (before and after the benchmarked section). +//! let code = unsafe { wasm_bench_execute(engine) }; +//! assert_eq!(code, OK); +//! +//! unsafe { +//! wasm_bench_free(engine); +//! } //! ``` -use anyhow::{anyhow, Result}; -use core::slice; -use std::os::raw::c_int; + +use anyhow::{anyhow, Context, Result}; +use std::os::raw::{c_int, c_void}; +use std::slice; use wasi_common::WasiCtxBuilder; use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; use wasmtime_wasi::Wasi; -/// Exposes a C-compatible way of creating the engine from the bytes of a single Wasm module. This function returns a -/// pointer to an opaque structure that contains the engine's initialized state. +pub type ExitCode = c_int; +pub const OK: ExitCode = 0; +pub const ERR: ExitCode = -1; + +/// Exposes a C-compatible way of creating the engine from the bytes of a single +/// Wasm module. +/// +/// This function returns a pointer to a structure that contains the engine's +/// initialized state. #[no_mangle] -pub extern "C" fn engine_create( - wasm_bytes: *const u8, - wasm_bytes_length: usize, -) -> *mut OpaqueEngineState { - let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; - let state = Box::new(EngineState::new(wasm_bytes)); - Box::into_raw(state) as *mut _ +pub extern "C" fn wasm_bench_create() -> *mut c_void { + let state = Box::new(BenchState::new()); + Box::into_raw(state) as _ } /// Free the engine state allocated by this library. #[no_mangle] -pub extern "C" fn engine_free(state: *mut OpaqueEngineState) { +pub extern "C" fn wasm_bench_free(state: *mut c_void) { + assert!(!state.is_null()); unsafe { - Box::from_raw(state); + Box::from_raw(state as *mut BenchState); } } /// Compile the Wasm benchmark module. #[no_mangle] -pub extern "C" fn engine_compile_module(state: *mut OpaqueEngineState) -> c_int { - let result = unsafe { OpaqueEngineState::convert(state) }.compile(); - to_c_error(result, "failed to compile") +pub extern "C" fn wasm_bench_compile( + state: *mut c_void, + wasm_bytes: *const u8, + wasm_bytes_length: usize, +) -> ExitCode { + let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; + let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; + let result = state.compile(wasm_bytes).context("failed to compile"); + to_exit_code(result) } /// Instantiate the Wasm benchmark module. #[no_mangle] -pub extern "C" fn engine_instantiate_module( - state: *mut OpaqueEngineState, +pub extern "C" fn wasm_bench_instantiate( + state: *mut c_void, bench_start: extern "C" fn(), bench_end: extern "C" fn(), -) -> c_int { - let result = unsafe { OpaqueEngineState::convert(state) }.instantiate(bench_start, bench_end); - to_c_error(result, "failed to instantiate") +) -> ExitCode { + let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; + let result = state + .instantiate(bench_start, bench_end) + .context("failed to instantiate"); + to_exit_code(result) } /// Execute the Wasm benchmark module. #[no_mangle] -pub extern "C" fn engine_execute_module(state: *mut OpaqueEngineState) -> c_int { - let result = unsafe { OpaqueEngineState::convert(state) }.execute(); - to_c_error(result, "failed to execute") +pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode { + let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; + let result = state.execute().context("failed to execute"); + to_exit_code(result) } -/// Helper function for converting a Rust result to a C error code (0 == success). Additionally, this will print an -/// error indicating some information regarding the failure. -fn to_c_error(result: Result, message: &str) -> c_int { - match result { - Ok(_) => 0, +/// Helper function for converting a Rust result to a C error code. +/// +/// This will print an error indicating some information regarding the failure. +fn to_exit_code(result: impl Into>) -> ExitCode { + match result.into() { + Ok(_) => OK, Err(error) => { - println!("{}: {:?}", message, error); - 1 + eprintln!("{:?}", error); + ERR } } } -/// Opaque pointer type for hiding the engine state details. -#[repr(C)] -pub struct OpaqueEngineState { - _private: [u8; 0], -} -impl OpaqueEngineState { - unsafe fn convert(ptr: *mut OpaqueEngineState) -> &'static mut EngineState<'static> { - assert!(!ptr.is_null()); - &mut *(ptr as *mut EngineState) - } -} - -/// This structure contains the actual Rust implementation of the state required to manage the Wasmtime engine between -/// calls. -struct EngineState<'a> { - bytes: &'a [u8], +/// This structure contains the actual Rust implementation of the state required +/// to manage the Wasmtime engine between calls. +struct BenchState { engine: Engine, store: Store, module: Option, instance: Option, + did_execute: bool, } -impl<'a> EngineState<'a> { - fn new(bytes: &'a [u8]) -> Self { - // TODO turn off caching? +impl BenchState { + fn new() -> Self { let mut config = Config::new(); config.wasm_simd(true); + // NB: do not configure a code cache. + let engine = Engine::new(&config); let store = Store::new(&engine); Self { - bytes, engine, store, module: None, instance: None, + did_execute: false, } } - fn compile(&mut self) -> Result<()> { - self.module = Some(Module::from_binary(&self.engine, self.bytes)?); + fn compile(&mut self, bytes: &[u8]) -> Result<()> { + assert!( + self.module.is_none(), + "create a new engine to repeat compilation" + ); + self.module = Some(Module::from_binary(&self.engine, bytes)?); Ok(()) } @@ -148,51 +192,57 @@ impl<'a> EngineState<'a> { bench_start: extern "C" fn(), bench_end: extern "C" fn(), ) -> Result<()> { - // TODO instantiate WASI modules? - match &self.module { - Some(module) => { - let mut linker = Linker::new(&self.store); + assert!( + self.instance.is_none(), + "create a new engine to repeat instantiation" + ); + let module = self + .module + .as_mut() + .expect("compile the module before instantiating it"); - // Import a very restricted WASI environment. - let mut cx = WasiCtxBuilder::new(); - cx.inherit_stdio(); - let cx = cx.build()?; - let wasi = Wasi::new(linker.store(), cx); - wasi.add_to_linker(&mut linker)?; + let mut linker = Linker::new(&self.store); - // Import the specialized benchmarking functions. - linker.func("bench", "start", move || bench_start())?; - linker.func("bench", "end", move || bench_end())?; + // Import a very restricted WASI environment. + let mut cx = WasiCtxBuilder::new(); + cx.inherit_stdio(); + let cx = cx.build()?; + let wasi = Wasi::new(linker.store(), cx); + wasi.add_to_linker(&mut linker)?; - self.instance = Some(linker.instantiate(module)?); - } - None => panic!("compile the module before instantiating it"), - } + // Import the specialized benchmarking functions. + linker.func("bench", "start", move || bench_start())?; + linker.func("bench", "end", move || bench_end())?; + + self.instance = Some(linker.instantiate(&module)?); Ok(()) } - fn execute(&self) -> Result<()> { - match &self.instance { - Some(instance) => { - let start_func = instance.get_func("_start").expect("a _start function"); - let runnable_func = start_func.get0::<()>()?; - match runnable_func() { - Ok(_) => {} - Err(trap) => { - // Since _start will likely return by using the system `exit` call, we must - // check the trap code to see if it actually represents a successful exit. - let status = trap.i32_exit_status(); - if status != Some(0) { - return Err(anyhow!( - "_start exited with a non-zero code: {}", - status.unwrap() - )); - } - } - }; + fn execute(&mut self) -> Result<()> { + assert!(!self.did_execute, "create a new engine to repeat execution"); + self.did_execute = true; + + let instance = self + .instance + .as_ref() + .expect("instantiate the module before executing it"); + + let start_func = instance.get_func("_start").expect("a _start function"); + let runnable_func = start_func.get0::<()>()?; + match runnable_func() { + Ok(_) => Ok(()), + Err(trap) => { + // Since _start will likely return by using the system `exit` call, we must + // check the trap code to see if it actually represents a successful exit. + match trap.i32_exit_status() { + Some(0) => Ok(()), + Some(n) => Err(anyhow!("_start exited with a non-zero code: {}", n)), + None => Err(anyhow!( + "executing the benchmark resulted in a trap: {}", + trap + )), + } } - None => panic!("instantiate the module before executing it"), } - Ok(()) } }