wasmtime_bench_api/
lib.rs

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
136mod unsafe_send_sync;
137
138use crate::unsafe_send_sync::UnsafeSendSync;
139use anyhow::{Context, Result};
140use clap::Parser;
141use std::os::raw::{c_int, c_void};
142use std::slice;
143use std::{env, path::PathBuf};
144use wasi_common::{sync::WasiCtxBuilder, I32Exit, WasiCtx};
145use wasmtime::{Engine, Instance, Linker, Module, Store};
146use wasmtime_cli_flags::CommonOptions;
147
148pub type ExitCode = c_int;
149pub const OK: ExitCode = 0;
150pub 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]
157static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> =
158    shuffling_allocator::wrap!(&std::alloc::System);
159
160/// Configuration options for the benchmark.
161#[repr(C)]
162pub 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
204impl 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<CommonOptions> {
242        let flags = if self.execution_flags_ptr.is_null() {
243            ""
244        } else {
245            let execution_flags = unsafe {
246                std::slice::from_raw_parts(self.execution_flags_ptr, self.execution_flags_len)
247            };
248            std::str::from_utf8(execution_flags)
249                .context("given execution flags string is not valid UTF-8")?
250        };
251        let options = CommonOptions::try_parse_from(
252            ["wasmtime"]
253                .into_iter()
254                .chain(flags.split(' ').filter(|s| !s.is_empty())),
255        )
256        .context("failed to parse options")?;
257        Ok(options)
258    }
259}
260
261/// Exposes a C-compatible way of creating the engine from the bytes of a single
262/// Wasm module.
263///
264/// On success, the `out_bench_ptr` is initialized to a pointer to a structure
265/// that contains the engine's initialized state, and `0` is returned. On
266/// failure, a non-zero status code is returned and `out_bench_ptr` is left
267/// untouched.
268#[unsafe(no_mangle)]
269pub extern "C" fn wasm_bench_create(
270    config: WasmBenchConfig,
271    out_bench_ptr: *mut *mut c_void,
272) -> ExitCode {
273    let result = (|| -> Result<_> {
274        let working_dir = config.working_dir()?;
275        let working_dir =
276            cap_std::fs::Dir::open_ambient_dir(&working_dir, cap_std::ambient_authority())
277                .with_context(|| {
278                    format!(
279                        "failed to preopen the working directory: {}",
280                        working_dir.display(),
281                    )
282                })?;
283
284        let stdout_path = config.stdout_path()?;
285        let stderr_path = config.stderr_path()?;
286        let stdin_path = config.stdin_path()?;
287        let options = config.execution_flags()?;
288
289        let state = Box::new(BenchState::new(
290            options,
291            config.compilation_timer,
292            config.compilation_start,
293            config.compilation_end,
294            config.instantiation_timer,
295            config.instantiation_start,
296            config.instantiation_end,
297            config.execution_timer,
298            config.execution_start,
299            config.execution_end,
300            move || {
301                let mut cx = WasiCtxBuilder::new();
302
303                let stdout = std::fs::File::create(&stdout_path)
304                    .with_context(|| format!("failed to create {}", stdout_path.display()))?;
305                let stdout = cap_std::fs::File::from_std(stdout);
306                let stdout = wasi_common::sync::file::File::from_cap_std(stdout);
307                cx.stdout(Box::new(stdout));
308
309                let stderr = std::fs::File::create(&stderr_path)
310                    .with_context(|| format!("failed to create {}", stderr_path.display()))?;
311                let stderr = cap_std::fs::File::from_std(stderr);
312                let stderr = wasi_common::sync::file::File::from_cap_std(stderr);
313                cx.stderr(Box::new(stderr));
314
315                if let Some(stdin_path) = &stdin_path {
316                    let stdin = std::fs::File::open(stdin_path)
317                        .with_context(|| format!("failed to open {}", stdin_path.display()))?;
318                    let stdin = cap_std::fs::File::from_std(stdin);
319                    let stdin = wasi_common::sync::file::File::from_cap_std(stdin);
320                    cx.stdin(Box::new(stdin));
321                }
322
323                // Allow access to the working directory so that the benchmark can read
324                // its input workload(s).
325                cx.preopened_dir(working_dir.try_clone()?, ".")?;
326
327                // Pass this env var along so that the benchmark program can use smaller
328                // input workload(s) if it has them and that has been requested.
329                if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
330                    cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?;
331                }
332
333                Ok(cx.build())
334            },
335        )?);
336        Ok(Box::into_raw(state) as _)
337    })();
338
339    if let Ok(bench_ptr) = result {
340        unsafe {
341            assert!(!out_bench_ptr.is_null());
342            *out_bench_ptr = bench_ptr;
343        }
344    }
345
346    to_exit_code(result.map(|_| ()))
347}
348
349/// Free the engine state allocated by this library.
350#[unsafe(no_mangle)]
351pub extern "C" fn wasm_bench_free(state: *mut c_void) {
352    assert!(!state.is_null());
353    unsafe {
354        drop(Box::from_raw(state as *mut BenchState));
355    }
356}
357
358/// Compile the Wasm benchmark module.
359#[unsafe(no_mangle)]
360pub extern "C" fn wasm_bench_compile(
361    state: *mut c_void,
362    wasm_bytes: *const u8,
363    wasm_bytes_length: usize,
364) -> ExitCode {
365    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
366    let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) };
367    let result = state.compile(wasm_bytes).context("failed to compile");
368    to_exit_code(result)
369}
370
371/// Instantiate the Wasm benchmark module.
372#[unsafe(no_mangle)]
373pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
374    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
375    let result = state.instantiate().context("failed to instantiate");
376    to_exit_code(result)
377}
378
379/// Execute the Wasm benchmark module.
380#[unsafe(no_mangle)]
381pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
382    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
383    let result = state.execute().context("failed to execute");
384    to_exit_code(result)
385}
386
387/// Helper function for converting a Rust result to a C error code.
388///
389/// This will print an error indicating some information regarding the failure.
390fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
391    match result.into() {
392        Ok(_) => OK,
393        Err(error) => {
394            eprintln!("{error:?}");
395            ERR
396        }
397    }
398}
399
400/// This structure contains the actual Rust implementation of the state required
401/// to manage the Wasmtime engine between calls.
402struct BenchState {
403    linker: Linker<HostState>,
404    compilation_timer: *mut u8,
405    compilation_start: extern "C" fn(*mut u8),
406    compilation_end: extern "C" fn(*mut u8),
407    instantiation_timer: *mut u8,
408    instantiation_start: extern "C" fn(*mut u8),
409    instantiation_end: extern "C" fn(*mut u8),
410    make_wasi_cx: Box<dyn FnMut() -> Result<WasiCtx>>,
411    module: Option<Module>,
412    store_and_instance: Option<(Store<HostState>, Instance)>,
413    epoch_interruption: bool,
414    fuel: Option<u64>,
415}
416
417struct HostState {
418    wasi: WasiCtx,
419    #[cfg(feature = "wasi-nn")]
420    wasi_nn: wasmtime_wasi_nn::witx::WasiNnCtx,
421}
422
423impl BenchState {
424    fn new(
425        mut options: CommonOptions,
426        compilation_timer: *mut u8,
427        compilation_start: extern "C" fn(*mut u8),
428        compilation_end: extern "C" fn(*mut u8),
429        instantiation_timer: *mut u8,
430        instantiation_start: extern "C" fn(*mut u8),
431        instantiation_end: extern "C" fn(*mut u8),
432        execution_timer: *mut u8,
433        execution_start: extern "C" fn(*mut u8),
434        execution_end: extern "C" fn(*mut u8),
435        make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static,
436    ) -> Result<Self> {
437        let mut config = options.config(None)?;
438        // NB: always disable the compilation cache.
439        config.disable_cache();
440        let engine = Engine::new(&config)?;
441        let mut linker = Linker::<HostState>::new(&engine);
442
443        // Define the benchmarking start/end functions.
444        let execution_timer = unsafe {
445            // Safe because this bench API's contract requires that its methods
446            // are only ever called from a single thread.
447            UnsafeSendSync::new(execution_timer)
448        };
449        linker.func_wrap("bench", "start", move || {
450            execution_start(*execution_timer.get());
451            Ok(())
452        })?;
453        linker.func_wrap("bench", "end", move || {
454            execution_end(*execution_timer.get());
455            Ok(())
456        })?;
457
458        let epoch_interruption = options.wasm.epoch_interruption.unwrap_or(false);
459        let fuel = options.wasm.fuel;
460
461        if options.wasi.common != Some(false) {
462            wasi_common::sync::add_to_linker(&mut linker, |cx| &mut cx.wasi)?;
463        }
464
465        #[cfg(feature = "wasi-nn")]
466        if options.wasi.nn == Some(true) {
467            wasmtime_wasi_nn::witx::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
468        }
469
470        Ok(Self {
471            linker,
472            compilation_timer,
473            compilation_start,
474            compilation_end,
475            instantiation_timer,
476            instantiation_start,
477            instantiation_end,
478            make_wasi_cx: Box::new(make_wasi_cx) as _,
479            module: None,
480            store_and_instance: None,
481            epoch_interruption,
482            fuel,
483        })
484    }
485
486    fn compile(&mut self, bytes: &[u8]) -> Result<()> {
487        self.module = None;
488
489        (self.compilation_start)(self.compilation_timer);
490        let module = Module::from_binary(self.linker.engine(), bytes)?;
491        (self.compilation_end)(self.compilation_timer);
492
493        self.module = Some(module);
494        Ok(())
495    }
496
497    fn instantiate(&mut self) -> Result<()> {
498        self.store_and_instance = None;
499
500        let module = self
501            .module
502            .as_ref()
503            .expect("compile the module before instantiating it");
504
505        let host = HostState {
506            wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
507            #[cfg(feature = "wasi-nn")]
508            wasi_nn: {
509                let (backends, registry) = wasmtime_wasi_nn::preload(&[])?;
510                wasmtime_wasi_nn::witx::WasiNnCtx::new(backends, registry)
511            },
512        };
513
514        // NB: Start measuring instantiation time *after* we've created the WASI
515        // context, since that needs to do file I/O to setup
516        // stdin/stdout/stderr.
517        (self.instantiation_start)(self.instantiation_timer);
518        let mut store = Store::new(self.linker.engine(), host);
519        if self.epoch_interruption {
520            store.set_epoch_deadline(1);
521        }
522        if let Some(fuel) = self.fuel {
523            store.set_fuel(fuel).unwrap();
524        }
525
526        let instance = self.linker.instantiate(&mut store, &module)?;
527        (self.instantiation_end)(self.instantiation_timer);
528
529        self.store_and_instance = Some((store, instance));
530        Ok(())
531    }
532
533    fn execute(&mut self) -> Result<()> {
534        let (mut store, instance) = self
535            .store_and_instance
536            .take()
537            .expect("instantiate the module before executing it");
538
539        let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
540        match start_func.call(&mut store, ()) {
541            Ok(_) => Ok(()),
542            Err(trap) => {
543                // Since _start will likely return by using the system `exit` call, we must
544                // check the trap code to see if it actually represents a successful exit.
545                if let Some(exit) = trap.downcast_ref::<I32Exit>() {
546                    if exit.0 == 0 {
547                        return Ok(());
548                    }
549                }
550
551                Err(trap)
552            }
553        }
554    }
555}