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 //! execution_flags_ptr: ptr::null(), 94 //! execution_flags_len: 0, 95 //! }; 96 //! 97 //! let mut bench_api = ptr::null_mut(); 98 //! unsafe { 99 //! let code = wasm_bench_create(config, &mut bench_api); 100 //! assert_eq!(code, OK); 101 //! assert!(!bench_api.is_null()); 102 //! }; 103 //! 104 //! let wasm = wat::parse_bytes(br#" 105 //! (module 106 //! (func $bench_start (import "bench" "start")) 107 //! (func $bench_end (import "bench" "end")) 108 //! (func $start (export "_start") 109 //! call $bench_start 110 //! i32.const 1 111 //! i32.const 2 112 //! i32.add 113 //! drop 114 //! call $bench_end 115 //! ) 116 //! ) 117 //! "#).unwrap(); 118 //! 119 //! // This will call the `compilation_{start,end}` timing functions on success. 120 //! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) }; 121 //! assert_eq!(code, OK); 122 //! 123 //! // This will call the `instantiation_{start,end}` timing functions on success. 124 //! let code = unsafe { wasm_bench_instantiate(bench_api) }; 125 //! assert_eq!(code, OK); 126 //! 127 //! // This will call the `execution_{start,end}` timing functions on success. 128 //! let code = unsafe { wasm_bench_execute(bench_api) }; 129 //! assert_eq!(code, OK); 130 //! 131 //! unsafe { 132 //! wasm_bench_free(bench_api); 133 //! } 134 //! ``` 135 136 mod unsafe_send_sync; 137 138 use crate::unsafe_send_sync::UnsafeSendSync; 139 use anyhow::{Context, Result}; 140 use std::os::raw::{c_int, c_void}; 141 use std::slice; 142 use std::{env, path::PathBuf}; 143 use target_lexicon::Triple; 144 use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; 145 use wasmtime_cli_flags::{CommonOptions, WasiModules}; 146 use wasmtime_wasi::{sync::WasiCtxBuilder, I32Exit, WasiCtx}; 147 148 pub type ExitCode = c_int; 149 pub const OK: ExitCode = 0; 150 pub const ERR: ExitCode = -1; 151 152 // Randomize the location of heap objects to avoid accidental locality being an 153 // uncontrolled variable that obscures performance evaluation in our 154 // experiments. 155 #[cfg(feature = "shuffling-allocator")] 156 #[global_allocator] 157 static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> = 158 shuffling_allocator::wrap!(&std::alloc::System); 159 160 /// Configuration options for the benchmark. 161 #[repr(C)] 162 pub struct WasmBenchConfig { 163 /// The working directory where benchmarks should be executed. 164 pub working_dir_ptr: *const u8, 165 pub working_dir_len: usize, 166 167 /// The file path that should be created and used as `stdout`. 168 pub stdout_path_ptr: *const u8, 169 pub stdout_path_len: usize, 170 171 /// The file path that should be created and used as `stderr`. 172 pub stderr_path_ptr: *const u8, 173 pub stderr_path_len: usize, 174 175 /// The (optional) file path that should be opened and used as `stdin`. If 176 /// not provided, then the WASI context will not have a `stdin` initialized. 177 pub stdin_path_ptr: *const u8, 178 pub stdin_path_len: usize, 179 180 /// The functions to start and stop performance timers/counters during Wasm 181 /// compilation. 182 pub compilation_timer: *mut u8, 183 pub compilation_start: extern "C" fn(*mut u8), 184 pub compilation_end: extern "C" fn(*mut u8), 185 186 /// The functions to start and stop performance timers/counters during Wasm 187 /// instantiation. 188 pub instantiation_timer: *mut u8, 189 pub instantiation_start: extern "C" fn(*mut u8), 190 pub instantiation_end: extern "C" fn(*mut u8), 191 192 /// The functions to start and stop performance timers/counters during Wasm 193 /// execution. 194 pub execution_timer: *mut u8, 195 pub execution_start: extern "C" fn(*mut u8), 196 pub execution_end: extern "C" fn(*mut u8), 197 198 /// The (optional) flags to use when running Wasmtime. These correspond to 199 /// the flags used when running Wasmtime from the command line. 200 pub execution_flags_ptr: *const u8, 201 pub execution_flags_len: usize, 202 } 203 204 impl WasmBenchConfig { 205 fn working_dir(&self) -> Result<PathBuf> { 206 let working_dir = 207 unsafe { std::slice::from_raw_parts(self.working_dir_ptr, self.working_dir_len) }; 208 let working_dir = std::str::from_utf8(working_dir) 209 .context("given working directory is not valid UTF-8")?; 210 Ok(working_dir.into()) 211 } 212 213 fn stdout_path(&self) -> Result<PathBuf> { 214 let stdout_path = 215 unsafe { std::slice::from_raw_parts(self.stdout_path_ptr, self.stdout_path_len) }; 216 let stdout_path = 217 std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?; 218 Ok(stdout_path.into()) 219 } 220 221 fn stderr_path(&self) -> Result<PathBuf> { 222 let stderr_path = 223 unsafe { std::slice::from_raw_parts(self.stderr_path_ptr, self.stderr_path_len) }; 224 let stderr_path = 225 std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?; 226 Ok(stderr_path.into()) 227 } 228 229 fn stdin_path(&self) -> Result<Option<PathBuf>> { 230 if self.stdin_path_ptr.is_null() { 231 return Ok(None); 232 } 233 234 let stdin_path = 235 unsafe { std::slice::from_raw_parts(self.stdin_path_ptr, self.stdin_path_len) }; 236 let stdin_path = 237 std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?; 238 Ok(Some(stdin_path.into())) 239 } 240 241 fn execution_flags(&self) -> Result<Option<CommonOptions>> { 242 if self.execution_flags_ptr.is_null() { 243 return Ok(None); 244 } 245 246 let execution_flags = unsafe { 247 std::slice::from_raw_parts(self.execution_flags_ptr, self.execution_flags_len) 248 }; 249 let execution_flags = std::str::from_utf8(execution_flags) 250 .context("given execution flags string is not valid UTF-8")?; 251 252 let options = CommonOptions::parse_from_str(execution_flags)?; 253 Ok(Some(options)) 254 } 255 } 256 257 /// Exposes a C-compatible way of creating the engine from the bytes of a single 258 /// Wasm module. 259 /// 260 /// On success, the `out_bench_ptr` is initialized to a pointer to a structure 261 /// that contains the engine's initialized state, and `0` is returned. On 262 /// failure, a non-zero status code is returned and `out_bench_ptr` is left 263 /// untouched. 264 #[no_mangle] 265 pub extern "C" fn wasm_bench_create( 266 config: WasmBenchConfig, 267 out_bench_ptr: *mut *mut c_void, 268 ) -> ExitCode { 269 let result = (|| -> Result<_> { 270 let working_dir = config.working_dir()?; 271 let working_dir = 272 cap_std::fs::Dir::open_ambient_dir(&working_dir, cap_std::ambient_authority()) 273 .with_context(|| { 274 format!( 275 "failed to preopen the working directory: {}", 276 working_dir.display(), 277 ) 278 })?; 279 280 let stdout_path = config.stdout_path()?; 281 let stderr_path = config.stderr_path()?; 282 let stdin_path = config.stdin_path()?; 283 let options = config.execution_flags()?; 284 285 let state = Box::new(BenchState::new( 286 options, 287 config.compilation_timer, 288 config.compilation_start, 289 config.compilation_end, 290 config.instantiation_timer, 291 config.instantiation_start, 292 config.instantiation_end, 293 config.execution_timer, 294 config.execution_start, 295 config.execution_end, 296 move || { 297 let mut cx = WasiCtxBuilder::new(); 298 299 let stdout = std::fs::File::create(&stdout_path) 300 .with_context(|| format!("failed to create {}", stdout_path.display()))?; 301 let stdout = cap_std::fs::File::from_std(stdout); 302 let stdout = wasi_cap_std_sync::file::File::from_cap_std(stdout); 303 cx = cx.stdout(Box::new(stdout)); 304 305 let stderr = std::fs::File::create(&stderr_path) 306 .with_context(|| format!("failed to create {}", stderr_path.display()))?; 307 let stderr = cap_std::fs::File::from_std(stderr); 308 let stderr = wasi_cap_std_sync::file::File::from_cap_std(stderr); 309 cx = cx.stderr(Box::new(stderr)); 310 311 if let Some(stdin_path) = &stdin_path { 312 let stdin = std::fs::File::open(stdin_path) 313 .with_context(|| format!("failed to open {}", stdin_path.display()))?; 314 let stdin = cap_std::fs::File::from_std(stdin); 315 let stdin = wasi_cap_std_sync::file::File::from_cap_std(stdin); 316 cx = cx.stdin(Box::new(stdin)); 317 } 318 319 // Allow access to the working directory so that the benchmark can read 320 // its input workload(s). 321 cx = cx.preopened_dir(working_dir.try_clone()?, ".")?; 322 323 // Pass this env var along so that the benchmark program can use smaller 324 // input workload(s) if it has them and that has been requested. 325 if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") { 326 cx = cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?; 327 } 328 329 Ok(cx.build()) 330 }, 331 )?); 332 Ok(Box::into_raw(state) as _) 333 })(); 334 335 if let Ok(bench_ptr) = result { 336 unsafe { 337 assert!(!out_bench_ptr.is_null()); 338 *out_bench_ptr = bench_ptr; 339 } 340 } 341 342 to_exit_code(result.map(|_| ())) 343 } 344 345 /// Free the engine state allocated by this library. 346 #[no_mangle] 347 pub extern "C" fn wasm_bench_free(state: *mut c_void) { 348 assert!(!state.is_null()); 349 unsafe { 350 drop(Box::from_raw(state as *mut BenchState)); 351 } 352 } 353 354 /// Compile the Wasm benchmark module. 355 #[no_mangle] 356 pub extern "C" fn wasm_bench_compile( 357 state: *mut c_void, 358 wasm_bytes: *const u8, 359 wasm_bytes_length: usize, 360 ) -> ExitCode { 361 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 362 let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) }; 363 let result = state.compile(wasm_bytes).context("failed to compile"); 364 to_exit_code(result) 365 } 366 367 /// Instantiate the Wasm benchmark module. 368 #[no_mangle] 369 pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode { 370 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 371 let result = state.instantiate().context("failed to instantiate"); 372 to_exit_code(result) 373 } 374 375 /// Execute the Wasm benchmark module. 376 #[no_mangle] 377 pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode { 378 let state = unsafe { (state as *mut BenchState).as_mut().unwrap() }; 379 let result = state.execute().context("failed to execute"); 380 to_exit_code(result) 381 } 382 383 /// Helper function for converting a Rust result to a C error code. 384 /// 385 /// This will print an error indicating some information regarding the failure. 386 fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode { 387 match result.into() { 388 Ok(_) => OK, 389 Err(error) => { 390 eprintln!("{:?}", error); 391 ERR 392 } 393 } 394 } 395 396 /// This structure contains the actual Rust implementation of the state required 397 /// to manage the Wasmtime engine between calls. 398 struct BenchState { 399 linker: Linker<HostState>, 400 compilation_timer: *mut u8, 401 compilation_start: extern "C" fn(*mut u8), 402 compilation_end: extern "C" fn(*mut u8), 403 instantiation_timer: *mut u8, 404 instantiation_start: extern "C" fn(*mut u8), 405 instantiation_end: extern "C" fn(*mut u8), 406 make_wasi_cx: Box<dyn FnMut() -> Result<WasiCtx>>, 407 module: Option<Module>, 408 store_and_instance: Option<(Store<HostState>, Instance)>, 409 epoch_interruption: bool, 410 fuel: Option<u64>, 411 } 412 413 struct HostState { 414 wasi: WasiCtx, 415 #[cfg(feature = "wasi-nn")] 416 wasi_nn: wasmtime_wasi_nn::WasiNnCtx, 417 #[cfg(feature = "wasi-crypto")] 418 wasi_crypto: wasmtime_wasi_crypto::WasiCryptoCtx, 419 } 420 421 impl BenchState { 422 fn new( 423 options: Option<CommonOptions>, 424 compilation_timer: *mut u8, 425 compilation_start: extern "C" fn(*mut u8), 426 compilation_end: extern "C" fn(*mut u8), 427 instantiation_timer: *mut u8, 428 instantiation_start: extern "C" fn(*mut u8), 429 instantiation_end: extern "C" fn(*mut u8), 430 execution_timer: *mut u8, 431 execution_start: extern "C" fn(*mut u8), 432 execution_end: extern "C" fn(*mut u8), 433 make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static, 434 ) -> Result<Self> { 435 let config = if let Some(o) = &options { 436 o.config(Some(&Triple::host().to_string()))? 437 } else { 438 Config::new() 439 }; 440 // NB: do not configure a code cache. 441 let engine = Engine::new(&config)?; 442 let mut linker = Linker::<HostState>::new(&engine); 443 444 // Define the benchmarking start/end functions. 445 let execution_timer = unsafe { 446 // Safe because this bench API's contract requires that its methods 447 // are only ever called from a single thread. 448 UnsafeSendSync::new(execution_timer) 449 }; 450 linker.func_wrap("bench", "start", move || { 451 execution_start(*execution_timer.get()); 452 Ok(()) 453 })?; 454 linker.func_wrap("bench", "end", move || { 455 execution_end(*execution_timer.get()); 456 Ok(()) 457 })?; 458 459 let mut epoch_interruption = false; 460 let mut fuel = None; 461 if let Some(opts) = &options { 462 epoch_interruption = opts.epoch_interruption; 463 fuel = opts.fuel; 464 } 465 466 let wasi_modules = options 467 .map(|o| o.wasi_modules) 468 .flatten() 469 .unwrap_or(WasiModules::default()); 470 471 if wasi_modules.wasi_common { 472 wasmtime_wasi::add_to_linker(&mut linker, |cx| &mut cx.wasi)?; 473 } 474 475 #[cfg(feature = "wasi-nn")] 476 if wasi_modules.wasi_nn { 477 wasmtime_wasi_nn::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?; 478 } 479 480 #[cfg(feature = "wasi-crypto")] 481 if wasi_modules.wasi_crypto { 482 wasmtime_wasi_crypto::add_to_linker(&mut linker, |cx| &mut cx.wasi_crypto)?; 483 } 484 485 Ok(Self { 486 linker, 487 compilation_timer, 488 compilation_start, 489 compilation_end, 490 instantiation_timer, 491 instantiation_start, 492 instantiation_end, 493 make_wasi_cx: Box::new(make_wasi_cx) as _, 494 module: None, 495 store_and_instance: None, 496 epoch_interruption, 497 fuel, 498 }) 499 } 500 501 fn compile(&mut self, bytes: &[u8]) -> Result<()> { 502 assert!( 503 self.module.is_none(), 504 "create a new engine to repeat compilation" 505 ); 506 507 (self.compilation_start)(self.compilation_timer); 508 let module = Module::from_binary(self.linker.engine(), bytes)?; 509 (self.compilation_end)(self.compilation_timer); 510 511 self.module = Some(module); 512 Ok(()) 513 } 514 515 fn instantiate(&mut self) -> Result<()> { 516 let module = self 517 .module 518 .as_ref() 519 .expect("compile the module before instantiating it"); 520 521 let host = HostState { 522 wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?, 523 #[cfg(feature = "wasi-nn")] 524 wasi_nn: wasmtime_wasi_nn::WasiNnCtx::new()?, 525 #[cfg(feature = "wasi-crypto")] 526 wasi_crypto: wasmtime_wasi_nn::WasiCryptoCtx::new(), 527 }; 528 529 // NB: Start measuring instantiation time *after* we've created the WASI 530 // context, since that needs to do file I/O to setup 531 // stdin/stdout/stderr. 532 (self.instantiation_start)(self.instantiation_timer); 533 let mut store = Store::new(self.linker.engine(), host); 534 if self.epoch_interruption { 535 store.set_epoch_deadline(1); 536 } 537 if let Some(fuel) = self.fuel { 538 store.add_fuel(fuel).unwrap(); 539 } 540 541 let instance = self.linker.instantiate(&mut store, &module)?; 542 (self.instantiation_end)(self.instantiation_timer); 543 544 self.store_and_instance = Some((store, instance)); 545 Ok(()) 546 } 547 548 fn execute(&mut self) -> Result<()> { 549 let (mut store, instance) = self 550 .store_and_instance 551 .take() 552 .expect("instantiate the module before executing it"); 553 554 let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?; 555 match start_func.call(&mut store, ()) { 556 Ok(_) => Ok(()), 557 Err(trap) => { 558 // Since _start will likely return by using the system `exit` call, we must 559 // check the trap code to see if it actually represents a successful exit. 560 if let Some(exit) = trap.downcast_ref::<I32Exit>() { 561 if exit.0 == 0 { 562 return Ok(()); 563 } 564 } 565 566 Err(trap) 567 } 568 } 569 } 570 } 571