1 //! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation, 2 //! and execution. 3 //! 4 //! The API expects calls that match the following state machine: 5 //! 6 //! ```text 7 //! | 8 //! | 9 //! V 10 //! .---> wasm_bench_create 11 //! | | | 12 //! | | | 13 //! | | V 14 //! | | wasm_bench_compile 15 //! | | | | 16 //! | | | | .----. 17 //! | | | | | | 18 //! | | | V V | 19 //! | | | wasm_bench_instantiate <------. 20 //! | | | | | | 21 //! | | | | | | 22 //! | | | | | | 23 //! | | | .------' '-----> wasm_bench_execute 24 //! | | | | | 25 //! | | | | | 26 //! | V V V | 27 //! '------ wasm_bench_free <--------------------------' 28 //! | 29 //! | 30 //! V 31 //! ``` 32 //! 33 //! All API calls must happen on the same thread. 34 //! 35 //! Functions which return pointers use null as an error value. Function which 36 //! return `int` use `0` as OK and non-zero as an error value. 37 //! 38 //! # Example 39 //! 40 //! ``` 41 //! use std::ptr; 42 //! use wasmtime_bench_api::*; 43 //! 44 //! let working_dir = std::env::current_dir().unwrap().display().to_string(); 45 //! let stdout_path = "./stdout.log"; 46 //! let stderr_path = "./stderr.log"; 47 //! 48 //! // Functions to start/end timers for compilation. 49 //! // 50 //! // The `compilation_timer` pointer configured in the `WasmBenchConfig` is 51 //! // passed through. 52 //! extern "C" fn compilation_start(timer: *mut u8) { 53 //! // Start your compilation timer here. 54 //! } 55 //! extern "C" fn compilation_end(timer: *mut u8) { 56 //! // End your compilation timer here. 57 //! } 58 //! 59 //! // Similar for instantiation. 60 //! extern "C" fn instantiation_start(timer: *mut u8) { 61 //! // Start your instantiation timer here. 62 //! } 63 //! extern "C" fn instantiation_end(timer: *mut u8) { 64 //! // End your instantiation timer here. 65 //! } 66 //! 67 //! // Similar for execution. 68 //! extern "C" fn execution_start(timer: *mut u8) { 69 //! // Start your execution timer here. 70 //! } 71 //! extern "C" fn execution_end(timer: *mut u8) { 72 //! // End your execution timer here. 73 //! } 74 //! 75 //! let config = WasmBenchConfig { 76 //! working_dir_ptr: working_dir.as_ptr(), 77 //! working_dir_len: working_dir.len(), 78 //! stdout_path_ptr: stdout_path.as_ptr(), 79 //! stdout_path_len: stdout_path.len(), 80 //! stderr_path_ptr: stderr_path.as_ptr(), 81 //! stderr_path_len: stderr_path.len(), 82 //! stdin_path_ptr: ptr::null(), 83 //! stdin_path_len: 0, 84 //! compilation_timer: ptr::null_mut(), 85 //! compilation_start, 86 //! compilation_end, 87 //! instantiation_timer: ptr::null_mut(), 88 //! instantiation_start, 89 //! instantiation_end, 90 //! execution_timer: ptr::null_mut(), 91 //! execution_start, 92 //! execution_end, 93 //! }; 94 //! 95 //! let mut bench_api = ptr::null_mut(); 96 //! unsafe { 97 //! let code = wasm_bench_create(config, &mut bench_api); 98 //! assert_eq!(code, OK); 99 //! assert!(!bench_api.is_null()); 100 //! }; 101 //! 102 //! let wasm = wat::parse_bytes(br#" 103 //! (module 104 //! (func $bench_start (import "bench" "start")) 105 //! (func $bench_end (import "bench" "end")) 106 //! (func $start (export "_start") 107 //! call $bench_start 108 //! i32.const 1 109 //! i32.const 2 110 //! i32.add 111 //! drop 112 //! call $bench_end 113 //! ) 114 //! ) 115 //! "#).unwrap(); 116 //! 117 //! // This will call the `compilation_{start,end}` timing functions on success. 118 //! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) }; 119 //! assert_eq!(code, OK); 120 //! 121 //! // This will call the `instantiation_{start,end}` timing functions on success. 122 //! let code = unsafe { wasm_bench_instantiate(bench_api) }; 123 //! assert_eq!(code, OK); 124 //! 125 //! // This will call the `execution_{start,end}` timing functions on success. 126 //! let code = unsafe { wasm_bench_execute(bench_api) }; 127 //! assert_eq!(code, OK); 128 //! 129 //! unsafe { 130 //! wasm_bench_free(bench_api); 131 //! } 132 //! ``` 133 134 mod unsafe_send_sync; 135 136 use crate::unsafe_send_sync::UnsafeSendSync; 137 use anyhow::{anyhow, Context, Result}; 138 use std::os::raw::{c_int, c_void}; 139 use std::slice; 140 use std::{env, path::PathBuf}; 141 use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; 142 use wasmtime_wasi::{sync::WasiCtxBuilder, WasiCtx}; 143 144 pub type ExitCode = c_int; 145 pub const OK: ExitCode = 0; 146 pub const ERR: ExitCode = -1; 147 148 // Randomize the location of heap objects to avoid accidental locality being an 149 // uncontrolled variable that obscures performance evaluation in our 150 // experiments. 151 #[cfg(feature = "shuffling-allocator")] 152 #[global_allocator] 153 static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> = 154 shuffling_allocator::wrap!(&std::alloc::System); 155 156 /// Configuration options for the benchmark. 157 #[repr(C)] 158 pub struct WasmBenchConfig { 159 /// The working directory where benchmarks should be executed. 160 pub working_dir_ptr: *const u8, 161 pub working_dir_len: usize, 162 163 /// The file path that should be created and used as `stdout`. 164 pub stdout_path_ptr: *const u8, 165 pub stdout_path_len: usize, 166 167 /// The file path that should be created and used as `stderr`. 168 pub stderr_path_ptr: *const u8, 169 pub stderr_path_len: usize, 170 171 /// The (optional) file path that should be opened and used as `stdin`. If 172 /// not provided, then the WASI context will not have a `stdin` initialized. 173 pub stdin_path_ptr: *const u8, 174 pub stdin_path_len: usize, 175 176 /// The functions to start and stop performance timers/counters during Wasm 177 /// compilation. 178 pub compilation_timer: *mut u8, 179 pub compilation_start: extern "C" fn(*mut u8), 180 pub compilation_end: extern "C" fn(*mut u8), 181 182 /// The functions to start and stop performance timers/counters during Wasm 183 /// instantiation. 184 pub instantiation_timer: *mut u8, 185 pub instantiation_start: extern "C" fn(*mut u8), 186 pub instantiation_end: extern "C" fn(*mut u8), 187 188 /// The functions to start and stop performance timers/counters during Wasm 189 /// execution. 190 pub execution_timer: *mut u8, 191 pub execution_start: extern "C" fn(*mut u8), 192 pub execution_end: extern "C" fn(*mut u8), 193 } 194 195 impl WasmBenchConfig { 196 fn working_dir(&self) -> Result<PathBuf> { 197 let working_dir = 198 unsafe { std::slice::from_raw_parts(self.working_dir_ptr, self.working_dir_len) }; 199 let working_dir = std::str::from_utf8(working_dir) 200 .context("given working directory is not valid UTF-8")?; 201 Ok(working_dir.into()) 202 } 203 204 fn stdout_path(&self) -> Result<PathBuf> { 205 let stdout_path = 206 unsafe { std::slice::from_raw_parts(self.stdout_path_ptr, self.stdout_path_len) }; 207 let stdout_path = 208 std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?; 209 Ok(stdout_path.into()) 210 } 211 212 fn stderr_path(&self) -> Result<PathBuf> { 213 let stderr_path = 214 unsafe { std::slice::from_raw_parts(self.stderr_path_ptr, self.stderr_path_len) }; 215 let stderr_path = 216 std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?; 217 Ok(stderr_path.into()) 218 } 219 220 fn stdin_path(&self) -> Result<Option<PathBuf>> { 221 if self.stdin_path_ptr.is_null() { 222 return Ok(None); 223 } 224 225 let stdin_path = 226 unsafe { std::slice::from_raw_parts(self.stdin_path_ptr, self.stdin_path_len) }; 227 let stdin_path = 228 std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?; 229 Ok(Some(stdin_path.into())) 230 } 231 } 232 233 /// Exposes a C-compatible way of creating the engine from the bytes of a single 234 /// Wasm module. 235 /// 236 /// On success, the `out_bench_ptr` is initialized to a pointer to a structure 237 /// that contains the engine's initialized state, and `0` is returned. On 238 /// failure, a non-zero status code is returned and `out_bench_ptr` is left 239 /// untouched. 240 #[no_mangle] 241 pub extern "C" fn wasm_bench_create( 242 config: WasmBenchConfig, 243 out_bench_ptr: *mut *mut c_void, 244 ) -> ExitCode { 245 let result = (|| -> Result<_> { 246 let working_dir = config.working_dir()?; 247 let working_dir = 248 cap_std::fs::Dir::open_ambient_dir(&working_dir, cap_std::ambient_authority()) 249 .with_context(|| { 250 format!( 251 "failed to preopen the working directory: {}", 252 working_dir.display(), 253 ) 254 })?; 255 256 let stdout_path = config.stdout_path()?; 257 let stderr_path = config.stderr_path()?; 258 let stdin_path = config.stdin_path()?; 259 260 let state = Box::new(BenchState::new( 261 config.compilation_timer, 262 config.compilation_start, 263 config.compilation_end, 264 config.instantiation_timer, 265 config.instantiation_start, 266 config.instantiation_end, 267 config.execution_timer, 268 config.execution_start, 269 config.execution_end, 270 move || { 271 let mut cx = WasiCtxBuilder::new(); 272 273 let stdout = std::fs::File::create(&stdout_path) 274 .with_context(|| format!("failed to create {}", stdout_path.display()))?; 275 let stdout = cap_std::fs::File::from_std(stdout, cap_std::ambient_authority()); 276 let stdout = wasi_cap_std_sync::file::File::from_cap_std(stdout); 277 cx = cx.stdout(Box::new(stdout)); 278 279 let stderr = std::fs::File::create(&stderr_path) 280 .with_context(|| format!("failed to create {}", stderr_path.display()))?; 281 let stderr = cap_std::fs::File::from_std(stderr, cap_std::ambient_authority()); 282 let stderr = wasi_cap_std_sync::file::File::from_cap_std(stderr); 283 cx = cx.stderr(Box::new(stderr)); 284 285 if let Some(stdin_path) = &stdin_path { 286 let stdin = std::fs::File::open(stdin_path) 287 .with_context(|| format!("failed to open {}", stdin_path.display()))?; 288 let stdin = cap_std::fs::File::from_std(stdin, cap_std::ambient_authority()); 289 let stdin = wasi_cap_std_sync::file::File::from_cap_std(stdin); 290 cx = cx.stdin(Box::new(stdin)); 291 } 292 293 // Allow access to the working directory so that the benchmark can read 294 // its input workload(s). 295 cx = cx.preopened_dir(working_dir.try_clone()?, ".")?; 296 297 // Pass this env var along so that the benchmark program can use smaller 298 // input workload(s) if it has them and that has been requested. 299 if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") { 300 cx = cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?; 301 } 302 303 Ok(cx.build()) 304 }, 305 )?); 306 Ok(Box::into_raw(state) as _) 307 })(); 308 309 if let Ok(bench_ptr) = result { 310 unsafe { 311 assert!(!out_bench_ptr.is_null()); 312 *out_bench_ptr = bench_ptr; 313 } 314 } 315 316 to_exit_code(result.map(|_| ())) 317 } 318 319 /// Free the engine state allocated by this library. 320 #[no_mangle] 321 pub extern "C" fn wasm_bench_free(state: *mut c_void) { 322 assert!(!state.is_null()); 323 unsafe { 324 Box::from_raw(state as *mut BenchState); 325 } 326 } 327 328 /// Compile the Wasm benchmark module. 329 #[no_mangle] 330 pub extern "C" fn wasm_bench_compile( 331 state: *mut c_void, 332 wasm_bytes: *const u8, 333 wasm_bytes_length: usize, 334 ) -> ExitCode { 335 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 336 let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; 337 let result = state.compile(wasm_bytes).context("failed to compile"); 338 to_exit_code(result) 339 } 340 341 /// Instantiate the Wasm benchmark module. 342 #[no_mangle] 343 pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode { 344 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 345 let result = state.instantiate().context("failed to instantiate"); 346 to_exit_code(result) 347 } 348 349 /// Execute the Wasm benchmark module. 350 #[no_mangle] 351 pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode { 352 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 353 let result = state.execute().context("failed to execute"); 354 to_exit_code(result) 355 } 356 357 /// Helper function for converting a Rust result to a C error code. 358 /// 359 /// This will print an error indicating some information regarding the failure. 360 fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode { 361 match result.into() { 362 Ok(_) => OK, 363 Err(error) => { 364 eprintln!("{:?}", error); 365 ERR 366 } 367 } 368 } 369 370 /// This structure contains the actual Rust implementation of the state required 371 /// to manage the Wasmtime engine between calls. 372 struct BenchState { 373 linker: Linker<HostState>, 374 compilation_timer: *mut u8, 375 compilation_start: extern "C" fn(*mut u8), 376 compilation_end: extern "C" fn(*mut u8), 377 instantiation_timer: *mut u8, 378 instantiation_start: extern "C" fn(*mut u8), 379 instantiation_end: extern "C" fn(*mut u8), 380 make_wasi_cx: Box<dyn FnMut() -> Result<WasiCtx>>, 381 module: Option<Module>, 382 store_and_instance: Option<(Store<HostState>, Instance)>, 383 } 384 385 struct HostState { 386 wasi: WasiCtx, 387 #[cfg(feature = "wasi-nn")] 388 wasi_nn: wasmtime_wasi_nn::WasiNnCtx, 389 390 #[cfg(feature = "wasi-crypto")] 391 wasi_crypto: wasmtime_wasi_crypto::WasiCryptoCtx, 392 } 393 394 impl BenchState { 395 fn new( 396 compilation_timer: *mut u8, 397 compilation_start: extern "C" fn(*mut u8), 398 compilation_end: extern "C" fn(*mut u8), 399 instantiation_timer: *mut u8, 400 instantiation_start: extern "C" fn(*mut u8), 401 instantiation_end: extern "C" fn(*mut u8), 402 execution_timer: *mut u8, 403 execution_start: extern "C" fn(*mut u8), 404 execution_end: extern "C" fn(*mut u8), 405 make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static, 406 ) -> Result<Self> { 407 // NB: do not configure a code cache. 408 let mut config = Config::new(); 409 config.wasm_simd(true); 410 let engine = Engine::new(&config)?; 411 let mut linker = Linker::<HostState>::new(&engine); 412 413 // Define the benchmarking start/end functions. 414 let execution_timer = unsafe { 415 // Safe because this bench API's contract requires that its methods 416 // are only ever called from a single thread. 417 UnsafeSendSync::new(execution_timer) 418 }; 419 linker.func_wrap("bench", "start", move || { 420 execution_start(*execution_timer.get()); 421 Ok(()) 422 })?; 423 linker.func_wrap("bench", "end", move || { 424 execution_end(*execution_timer.get()); 425 Ok(()) 426 })?; 427 428 wasmtime_wasi::add_to_linker(&mut linker, |cx| &mut cx.wasi)?; 429 430 #[cfg(feature = "wasi-nn")] 431 wasmtime_wasi_nn::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?; 432 433 #[cfg(feature = "wasi-crypto")] 434 wasmtime_wasi_crypto::add_to_linker(&mut linker, |cx| &mut cx.wasi_crypto)?; 435 436 Ok(Self { 437 linker, 438 compilation_timer, 439 compilation_start, 440 compilation_end, 441 instantiation_timer, 442 instantiation_start, 443 instantiation_end, 444 make_wasi_cx: Box::new(make_wasi_cx) as _, 445 module: None, 446 store_and_instance: None, 447 }) 448 } 449 450 fn compile(&mut self, bytes: &[u8]) -> Result<()> { 451 assert!( 452 self.module.is_none(), 453 "create a new engine to repeat compilation" 454 ); 455 456 (self.compilation_start)(self.compilation_timer); 457 let module = Module::from_binary(self.linker.engine(), bytes)?; 458 (self.compilation_end)(self.compilation_timer); 459 460 self.module = Some(module); 461 Ok(()) 462 } 463 464 fn instantiate(&mut self) -> Result<()> { 465 let module = self 466 .module 467 .as_ref() 468 .expect("compile the module before instantiating it"); 469 470 let host = HostState { 471 wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?, 472 #[cfg(feature = "wasi-nn")] 473 wasi_nn: wasmtime_wasi_nn::WasiNnCtx::new()?, 474 #[cfg(feature = "wasi-crypto")] 475 wasi_crypto: wasmtime_wasi_nn::WasiCryptoCtx::new(), 476 }; 477 478 // NB: Start measuring instantiation time *after* we've created the WASI 479 // context, since that needs to do file I/O to setup 480 // stdin/stdout/stderr. 481 (self.instantiation_start)(self.instantiation_timer); 482 let mut store = Store::new(self.linker.engine(), host); 483 let instance = self.linker.instantiate(&mut store, &module)?; 484 (self.instantiation_end)(self.instantiation_timer); 485 486 self.store_and_instance = Some((store, instance)); 487 Ok(()) 488 } 489 490 fn execute(&mut self) -> Result<()> { 491 let (mut store, instance) = self 492 .store_and_instance 493 .take() 494 .expect("instantiate the module before executing it"); 495 496 let start_func = instance.get_typed_func::<(), (), _>(&mut store, "_start")?; 497 match start_func.call(&mut store, ()) { 498 Ok(_) => Ok(()), 499 Err(trap) => { 500 // Since _start will likely return by using the system `exit` call, we must 501 // check the trap code to see if it actually represents a successful exit. 502 match trap.i32_exit_status() { 503 Some(0) => Ok(()), 504 Some(n) => Err(anyhow!("_start exited with a non-zero code: {}", n)), 505 None => Err(anyhow!( 506 "executing the benchmark resulted in a trap: {}", 507 trap 508 )), 509 } 510 } 511 } 512 } 513 } 514