Skip to main content

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//!     version: WASM_BENCH_CONFIG_VERSION,
77//!     size: std::mem::size_of::<WasmBenchConfig>(),
78//!     working_dir_ptr: working_dir.as_ptr(),
79//!     working_dir_len: working_dir.len(),
80//!     stdout_path_ptr: stdout_path.as_ptr(),
81//!     stdout_path_len: stdout_path.len(),
82//!     stderr_path_ptr: stderr_path.as_ptr(),
83//!     stderr_path_len: stderr_path.len(),
84//!     stdin_path_ptr: ptr::null(),
85//!     stdin_path_len: 0,
86//!     compilation_timer: ptr::null_mut(),
87//!     compilation_start,
88//!     compilation_end,
89//!     instantiation_timer: ptr::null_mut(),
90//!     instantiation_start,
91//!     instantiation_end,
92//!     execution_timer: ptr::null_mut(),
93//!     execution_start,
94//!     execution_end,
95//!     execution_flags_ptr: ptr::null(),
96//!     execution_flags_len: 0,
97//!     cache: false,
98//! };
99//!
100//! let mut bench_api = ptr::null_mut();
101//! unsafe {
102//!     let code = wasm_bench_create(&config, &mut bench_api);
103//!     assert_eq!(code, OK);
104//!     assert!(!bench_api.is_null());
105//! };
106//!
107//! let wasm = wat::parse_bytes(br#"
108//!     (module
109//!         (func $bench_start (import "bench" "start"))
110//!         (func $bench_end (import "bench" "end"))
111//!         (func $start (export "_start")
112//!             call $bench_start
113//!             i32.const 1
114//!             i32.const 2
115//!             i32.add
116//!             drop
117//!             call $bench_end
118//!         )
119//!     )
120//! "#).unwrap();
121//!
122//! // This will call the `compilation_{start,end}` timing functions on success.
123//! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) };
124//! assert_eq!(code, OK);
125//!
126//! // This will call the `instantiation_{start,end}` timing functions on success.
127//! let code = unsafe { wasm_bench_instantiate(bench_api) };
128//! assert_eq!(code, OK);
129//!
130//! // This will call the `execution_{start,end}` timing functions on success.
131//! let code = unsafe { wasm_bench_execute(bench_api) };
132//! assert_eq!(code, OK);
133//!
134//! unsafe {
135//!     wasm_bench_free(bench_api);
136//! }
137//! ```
138
139mod unsafe_send_sync;
140
141use crate::unsafe_send_sync::UnsafeSendSync;
142use clap::Parser;
143use std::os::raw::{c_int, c_void};
144use std::slice;
145use std::{env, path::PathBuf};
146use wasmtime::{
147    CodeBuilder, CodeHint, Engine, Linker, Module, Result, Store, error::Context as _, format_err,
148};
149use wasmtime_cli_flags::CommonOptions;
150use wasmtime_wasi::cli::{InputFile, OutputFile};
151use wasmtime_wasi::{FsPerms, I32Exit, WasiCtx, WasiCtxView, WasiView, p1::WasiP1Ctx};
152
153pub type ExitCode = c_int;
154pub const OK: ExitCode = 0;
155pub const ERR: ExitCode = -1;
156
157/// The current version of `WasmBenchConfig`.
158///
159/// Bumped once per release that appends a field, and never otherwise, so a
160/// field added in version `N` is present exactly when the caller's `version` is
161/// at least `N`.
162pub const WASM_BENCH_CONFIG_VERSION: u32 = 1;
163
164// Randomize the location of heap objects to avoid accidental locality being an
165// uncontrolled variable that obscures performance evaluation in our
166// experiments.
167#[cfg(feature = "shuffling-allocator")]
168#[global_allocator]
169static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> =
170    shuffling_allocator::wrap!(&std::alloc::System);
171
172/// Generate `WasmBenchConfig` and a `Config` accessor for each of its fields.
173///
174/// Tag each field with the `#[since_version = N]` that introduced it. Version 0
175/// fields are mandatory, so their accessors return the field directly; later
176/// fields may be missing from an older caller, so theirs return `Option`.
177macro_rules! define_config {
178    (
179        $(#[doc = $struct_doc:expr])*
180        pub struct $name:ident {
181            $(
182                $(#[doc = $field_doc:expr])*
183                #[since_version = $version:tt]
184                pub $field:ident : $ty:ty,
185            )*
186        }
187    ) => {
188        $(#[doc = $struct_doc])*
189        #[repr(C)]
190        pub struct $name {
191            $(
192                $(#[doc = $field_doc])*
193                pub $field : $ty,
194            )*
195        }
196
197        impl Config<'_> {
198            $(
199                define_config!(@accessor $name, $field, $ty, $version);
200            )*
201        }
202    };
203
204    // Version 0 fields are mandatory. `Config::new` rejects any caller too
205    // small to hold them, so they are always in bounds.
206    (@accessor $name:ident, $field:ident, $ty:ty, 0) => {
207        fn $field(&self) -> $ty {
208            unsafe { self.read(std::ptr::addr_of!((*self.ptr).$field)) }
209        }
210    };
211
212    // Fields added later may lie outside an older caller's allocation, so they
213    // are only read once `has_field` confirms this caller provided them.
214    (@accessor $name:ident, $field:ident, $ty:ty, $version:tt) => {
215        fn $field(&self) -> Option<$ty> {
216            let offset = std::mem::offset_of!($name, $field);
217            if self.has_field($version, offset, size_of::<$ty>()) {
218                Some(unsafe { self.read(std::ptr::addr_of!((*self.ptr).$field)) })
219            } else {
220                None
221            }
222        }
223    };
224}
225
226define_config! {
227    /// Configuration options for the benchmark.
228    ///
229    /// This structure is always passed across the bench API by pointer, and new
230    /// fields are only ever appended to the end, so that a caller built against
231    /// an older version of this structure can still drive a newer bench API
232    /// library.
233    ///
234    /// We only ever read one through [`Config`], never as a reference.
235    pub struct WasmBenchConfig {
236        /// The version of this structure that the caller was built against.
237        ///
238        /// Each field's documentation names the version that added it; a field
239        /// is only present when `version` is at least that.
240        #[since_version = 0]
241        pub version: u32,
242
243        /// The size of this structure, in bytes, as the caller knows it.
244        ///
245        /// Fields beyond these first `size` bytes were not allocated by the
246        /// caller and must not be read.
247        ///
248        /// This acts as a secondary backstop to `version`.
249        #[since_version = 0]
250        pub size: usize,
251
252        /// The working directory where benchmarks should be executed.
253        #[since_version = 0]
254        pub working_dir_ptr: *const u8,
255        #[since_version = 0]
256        pub working_dir_len: usize,
257
258        /// The file path that should be created and used as `stdout`.
259        #[since_version = 0]
260        pub stdout_path_ptr: *const u8,
261        #[since_version = 0]
262        pub stdout_path_len: usize,
263
264        /// The file path that should be created and used as `stderr`.
265        #[since_version = 0]
266        pub stderr_path_ptr: *const u8,
267        #[since_version = 0]
268        pub stderr_path_len: usize,
269
270        /// The (optional) file path that should be opened and used as `stdin`.
271        /// If not provided, then the WASI context will not have a `stdin`
272        /// initialized.
273        #[since_version = 0]
274        pub stdin_path_ptr: *const u8,
275        #[since_version = 0]
276        pub stdin_path_len: usize,
277
278        /// The functions to start and stop performance timers/counters during
279        /// Wasm compilation.
280        #[since_version = 0]
281        pub compilation_timer: *mut u8,
282        #[since_version = 0]
283        pub compilation_start: extern "C" fn(*mut u8),
284        #[since_version = 0]
285        pub compilation_end: extern "C" fn(*mut u8),
286
287        /// The functions to start and stop performance timers/counters during
288        /// Wasm instantiation.
289        #[since_version = 0]
290        pub instantiation_timer: *mut u8,
291        #[since_version = 0]
292        pub instantiation_start: extern "C" fn(*mut u8),
293        #[since_version = 0]
294        pub instantiation_end: extern "C" fn(*mut u8),
295
296        /// The functions to start and stop performance timers/counters during
297        /// Wasm execution.
298        #[since_version = 0]
299        pub execution_timer: *mut u8,
300        #[since_version = 0]
301        pub execution_start: extern "C" fn(*mut u8),
302        #[since_version = 0]
303        pub execution_end: extern "C" fn(*mut u8),
304
305        /// The (optional) flags to use when running Wasmtime. These correspond
306        /// to the flags used when running Wasmtime from the command line.
307        #[since_version = 0]
308        pub execution_flags_ptr: *const u8,
309        #[since_version = 0]
310        pub execution_flags_len: usize,
311
312        /// Whether to enable Wasmtime's compilation cache.
313        ///
314        /// Benchmark runs that measure compilation want this off, so that they
315        /// measure compilation rather than a cache lookup. Benchmark runs that
316        /// measure only instantiation or execution want this on, to avoid
317        /// spending time recompiling the Wasm.
318        #[since_version = 1]
319        pub cache: bool,
320    }
321}
322
323/// A caller-provided [`WasmBenchConfig`], read through its pointer.
324///
325/// Holding the pointer rather than a reference is what keeps this sound. A
326/// `&WasmBenchConfig` would assert that the whole struct is initialized, which
327/// is not true of its padding bytes, nor of the fields belonging to versions
328/// newer than the caller's.
329pub struct Config<'a> {
330    ptr: *const WasmBenchConfig,
331    _lifetime: std::marker::PhantomData<&'a WasmBenchConfig>,
332}
333
334impl<'a> Config<'a> {
335    /// The fields every caller must provide, regardless of how old it is.
336    const MIN_SIZE: usize =
337        std::mem::offset_of!(WasmBenchConfig, execution_flags_len) + size_of::<usize>();
338
339    /// Wrap a caller-provided config pointer, after checking it is large enough
340    /// to hold every mandatory field.
341    ///
342    /// # Safety
343    ///
344    /// `ptr` must be non-null, aligned, and point to a `version` and `size`
345    /// that accurately describe the fields that follow, all of which must
346    /// remain valid for `'a`.
347    unsafe fn new(ptr: *const WasmBenchConfig) -> Result<Config<'a>> {
348        assert!(!ptr.is_null());
349        let config = Config {
350            ptr,
351            _lifetime: std::marker::PhantomData,
352        };
353
354        // Reads `size`, which every caller provides, so this is in bounds even
355        // for the oldest one.
356        let size = config.size();
357        if size < Self::MIN_SIZE {
358            return Err(format_err!(
359                "given `WasmBenchConfig` is {} bytes, but at least {} are required",
360                size,
361                Self::MIN_SIZE,
362            ));
363        }
364
365        Ok(config)
366    }
367
368    /// Read one field out of the caller's config.
369    ///
370    /// # Safety
371    ///
372    /// `field` must be a pointer to a field this caller actually provided,
373    /// derived from `self.ptr`.
374    unsafe fn read<T>(&self, field: *const T) -> T {
375        assert!(!field.is_null());
376        assert!(field.is_aligned());
377        unsafe { field.read() }
378    }
379
380    /// Did the caller provide the field that version `version` added, which
381    /// occupies `field_size` bytes at `offset`?
382    ///
383    /// Both checks must pass. `version` alone is the field's real name, but
384    /// trusting it would let a caller that bumped its version without growing
385    /// the struct send us past its allocation; `size` alone cannot tell a field
386    /// apart from the padding that may follow the one before it.
387    fn has_field(&self, version: u32, offset: usize, field_size: usize) -> bool {
388        self.version() >= version && self.size() >= offset + field_size
389    }
390
391    /// Whether to enable the compilation cache.
392    ///
393    /// Conservatively defaults to disabled for callers that predate the `cache`
394    /// field, so we don't break compilation measurements.
395    fn cache_enabled(&self) -> bool {
396        self.cache().unwrap_or(false)
397    }
398
399    fn working_dir(&self) -> Result<PathBuf> {
400        let working_dir =
401            unsafe { std::slice::from_raw_parts(self.working_dir_ptr(), self.working_dir_len()) };
402        let working_dir = std::str::from_utf8(working_dir)
403            .context("given working directory is not valid UTF-8")?;
404        Ok(working_dir.into())
405    }
406
407    fn stdout_path(&self) -> Result<PathBuf> {
408        let stdout_path =
409            unsafe { std::slice::from_raw_parts(self.stdout_path_ptr(), self.stdout_path_len()) };
410        let stdout_path =
411            std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?;
412        Ok(stdout_path.into())
413    }
414
415    fn stderr_path(&self) -> Result<PathBuf> {
416        let stderr_path =
417            unsafe { std::slice::from_raw_parts(self.stderr_path_ptr(), self.stderr_path_len()) };
418        let stderr_path =
419            std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?;
420        Ok(stderr_path.into())
421    }
422
423    fn stdin_path(&self) -> Result<Option<PathBuf>> {
424        let stdin_path_ptr = self.stdin_path_ptr();
425        if stdin_path_ptr.is_null() {
426            return Ok(None);
427        }
428
429        let stdin_path =
430            unsafe { std::slice::from_raw_parts(stdin_path_ptr, self.stdin_path_len()) };
431        let stdin_path =
432            std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?;
433        Ok(Some(stdin_path.into()))
434    }
435
436    fn execution_flags(&self) -> Result<CommonOptions> {
437        let execution_flags_ptr = self.execution_flags_ptr();
438        let flags = if execution_flags_ptr.is_null() {
439            ""
440        } else {
441            let execution_flags = unsafe {
442                std::slice::from_raw_parts(execution_flags_ptr, self.execution_flags_len())
443            };
444            std::str::from_utf8(execution_flags)
445                .context("given execution flags string is not valid UTF-8")?
446        };
447        let options = CommonOptions::try_parse_from(
448            ["wasmtime"]
449                .into_iter()
450                .chain(flags.split(' ').filter(|s| !s.is_empty())),
451        )
452        .context("failed to parse options")?;
453        Ok(options)
454    }
455}
456
457/// Exposes a C-compatible way of creating the engine from the bytes of a single
458/// Wasm module.
459///
460/// On success, the `out_bench_ptr` is initialized to a pointer to a structure
461/// that contains the engine's initialized state, and `0` is returned. On
462/// failure, a non-zero status code is returned and `out_bench_ptr` is left
463/// untouched.
464///
465/// The `config` pointer only needs to remain valid for the duration of this
466/// call, as does everything it points to.
467#[unsafe(no_mangle)]
468pub extern "C" fn wasm_bench_create(
469    config: *const WasmBenchConfig,
470    out_bench_ptr: *mut *mut c_void,
471) -> ExitCode {
472    let result = (|| -> Result<_> {
473        let config = unsafe { Config::new(config)? };
474
475        // Clone paths for the core Wasm WASI closure.
476        let working_dir = config.working_dir()?;
477        let stdout_path = config.stdout_path()?;
478        let stderr_path = config.stderr_path()?;
479        let stdin_path = config.stdin_path()?;
480        let options = config.execution_flags()?;
481
482        let state = Box::new(BenchState::new(
483            options,
484            config.cache_enabled(),
485            config.compilation_timer(),
486            config.compilation_start(),
487            config.compilation_end(),
488            config.instantiation_timer(),
489            config.instantiation_start(),
490            config.instantiation_end(),
491            config.execution_timer(),
492            config.execution_start(),
493            config.execution_end(),
494            move || {
495                let mut cx = WasiCtx::builder();
496
497                let stdout = std::fs::File::create(&stdout_path)
498                    .with_context(|| format!("failed to create {}", stdout_path.display()))?;
499                cx.stdout(OutputFile::new(stdout));
500
501                let stderr = std::fs::File::create(&stderr_path)
502                    .with_context(|| format!("failed to create {}", stderr_path.display()))?;
503                cx.stderr(OutputFile::new(stderr));
504
505                if let Some(stdin_path) = &stdin_path {
506                    let stdin = std::fs::File::open(stdin_path)
507                        .with_context(|| format!("failed to open {}", stdin_path.display()))?;
508                    cx.stdin(InputFile::new(stdin));
509                }
510
511                // Allow access to the working directory so that the benchmark can read
512                // its input workload(s).
513                cx.preopened_dir(working_dir.clone(), ".", FsPerms::ReadOnly)?;
514
515                // Pass this env var along so that the benchmark program can use smaller
516                // input workload(s) if it has them and that has been requested.
517                if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
518                    cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val);
519                }
520
521                Ok(cx.build_p1())
522            },
523        )?);
524        Ok(Box::into_raw(state) as _)
525    })();
526
527    if let Ok(bench_ptr) = result {
528        unsafe {
529            assert!(!out_bench_ptr.is_null());
530            *out_bench_ptr = bench_ptr;
531        }
532    }
533
534    to_exit_code(result.map(|_| ()))
535}
536
537/// Free the engine state allocated by this library.
538#[unsafe(no_mangle)]
539pub extern "C" fn wasm_bench_free(state: *mut c_void) {
540    assert!(!state.is_null());
541    unsafe {
542        drop(Box::from_raw(state as *mut BenchState));
543    }
544}
545
546/// Compile the Wasm benchmark module.
547#[unsafe(no_mangle)]
548pub extern "C" fn wasm_bench_compile(
549    state: *mut c_void,
550    wasm_bytes: *const u8,
551    wasm_bytes_length: usize,
552) -> ExitCode {
553    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
554    let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) };
555    let result = state.compile(wasm_bytes).context("failed to compile");
556    to_exit_code(result)
557}
558
559/// Instantiate the Wasm benchmark module.
560#[unsafe(no_mangle)]
561pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
562    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
563    let result = state.instantiate().context("failed to instantiate");
564    to_exit_code(result)
565}
566
567/// Execute the Wasm benchmark module.
568#[unsafe(no_mangle)]
569pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
570    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
571    let result = state.execute().context("failed to execute");
572    to_exit_code(result)
573}
574
575/// Helper function for converting a Rust result to a C error code.
576///
577/// This will print an error indicating some information regarding the failure.
578fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
579    match result.into() {
580        Ok(_) => OK,
581        Err(error) => {
582            eprintln!("{error:?}");
583            ERR
584        }
585    }
586}
587
588/// This structure contains the actual Rust implementation of the state required
589/// to manage the Wasmtime engine between calls.
590struct BenchState {
591    linker: Linker<HostState>,
592    component_linker: wasmtime::component::Linker<HostState>,
593    compilation_timer: *mut u8,
594    compilation_start: extern "C" fn(*mut u8),
595    compilation_end: extern "C" fn(*mut u8),
596    instantiation_timer: *mut u8,
597    instantiation_start: extern "C" fn(*mut u8),
598    instantiation_end: extern "C" fn(*mut u8),
599    make_wasi_cx: Box<dyn FnMut() -> Result<WasiP1Ctx>>,
600    module: Option<Module>,
601    component: Option<wasmtime::component::Component>,
602    store_and_instance: Option<(Store<HostState>, Instance)>,
603    epoch_interruption: bool,
604    fuel: Option<u64>,
605}
606
607enum Instance {
608    Core(wasmtime::Instance),
609    Component(wasmtime::component::Instance),
610}
611
612struct HostState {
613    wasi: WasiP1Ctx,
614    #[cfg(feature = "wasi-nn")]
615    wasi_nn: wasmtime_wasi_nn::witx::WasiNnCtx,
616}
617
618impl WasiView for HostState {
619    fn ctx(&mut self) -> WasiCtxView<'_> {
620        self.wasi.ctx()
621    }
622}
623
624impl BenchState {
625    fn new(
626        mut options: CommonOptions,
627        cache: bool,
628        compilation_timer: *mut u8,
629        compilation_start: extern "C" fn(*mut u8),
630        compilation_end: extern "C" fn(*mut u8),
631        instantiation_timer: *mut u8,
632        instantiation_start: extern "C" fn(*mut u8),
633        instantiation_end: extern "C" fn(*mut u8),
634        execution_timer: *mut u8,
635        execution_start: extern "C" fn(*mut u8),
636        execution_end: extern "C" fn(*mut u8),
637        make_wasi_cx: impl FnMut() -> Result<WasiP1Ctx> + 'static,
638    ) -> Result<Self> {
639        let mut config = options.config(None)?;
640
641        // NB: set this either way, since `CommonOptions` enables the cache by
642        // default and we must not break compilation benchmarks.
643        config.cache(if cache {
644            Some(wasmtime::Cache::from_file(None)?)
645        } else {
646            None
647        });
648
649        let engine = Engine::new(&config)?;
650        let mut linker = Linker::<HostState>::new(&engine);
651        let mut component_linker = wasmtime::component::Linker::<HostState>::new(&engine);
652
653        // Define the benchmarking start/end functions.
654        let execution_timer = unsafe {
655            // Safe because this bench API's contract requires that its methods
656            // are only ever called from a single thread.
657            UnsafeSendSync::new(execution_timer)
658        };
659        linker.func_wrap("bench", "start", move || {
660            execution_start(*execution_timer.get());
661            Ok(())
662        })?;
663        linker.func_wrap("bench", "end", move || {
664            execution_end(*execution_timer.get());
665            Ok(())
666        })?;
667
668        // Define the same benchmarking functions on the component linker.
669        let mut bench_instance = component_linker.instance("bench")?;
670        bench_instance.func_wrap(
671            "start",
672            move |_: wasmtime::StoreContextMut<'_, _>, (): ()| {
673                execution_start(*execution_timer.get());
674                Ok(())
675            },
676        )?;
677        bench_instance.func_wrap("end", move |_: wasmtime::StoreContextMut<'_, _>, (): ()| {
678            execution_end(*execution_timer.get());
679            Ok(())
680        })?;
681
682        let epoch_interruption = options.wasm.epoch_interruption.unwrap_or(false);
683        let fuel = options.wasm.fuel;
684
685        if options.wasi.common != Some(false) {
686            wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |cx| &mut cx.wasi)?;
687            wasmtime_wasi::p2::add_to_linker_sync(&mut component_linker)?;
688        }
689
690        #[cfg(feature = "wasi-nn")]
691        if options.wasi.nn == Some(true) {
692            wasmtime_wasi_nn::witx::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
693        }
694
695        Ok(Self {
696            linker,
697            component_linker,
698            compilation_timer,
699            compilation_start,
700            compilation_end,
701            instantiation_timer,
702            instantiation_start,
703            instantiation_end,
704            make_wasi_cx: Box::new(make_wasi_cx) as _,
705            module: None,
706            component: None,
707            store_and_instance: None,
708            epoch_interruption,
709            fuel,
710        })
711    }
712
713    fn compile(&mut self, bytes: &[u8]) -> Result<()> {
714        self.module = None;
715        self.component = None;
716
717        let mut builder = CodeBuilder::new(self.linker.engine());
718        builder.wasm_binary(bytes, None)?;
719
720        match builder.hint() {
721            Some(CodeHint::Component) => {
722                (self.compilation_start)(self.compilation_timer);
723                let component = builder.compile_component()?;
724                (self.compilation_end)(self.compilation_timer);
725                self.component = Some(component);
726            }
727            Some(CodeHint::Module) | None => {
728                (self.compilation_start)(self.compilation_timer);
729                let module = builder.compile_module()?;
730                (self.compilation_end)(self.compilation_timer);
731                self.module = Some(module);
732            }
733        }
734
735        Ok(())
736    }
737
738    fn instantiate(&mut self) -> Result<()> {
739        self.store_and_instance = None;
740
741        let host = HostState {
742            wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
743            #[cfg(feature = "wasi-nn")]
744            wasi_nn: {
745                let (backends, registry) = wasmtime_wasi_nn::preload(&[])?;
746                wasmtime_wasi_nn::witx::WasiNnCtx::new(backends, registry)
747            },
748        };
749
750        // NB: Start measuring instantiation time *after* we've created the
751        // WASI context, since that needs to do file I/O to setup
752        // stdin/stdout/stderr.
753        (self.instantiation_start)(self.instantiation_timer);
754
755        let mut store = Store::new(self.linker.engine(), host);
756        if self.epoch_interruption {
757            store.set_epoch_deadline(1);
758        }
759        if let Some(fuel) = self.fuel {
760            store.set_fuel(fuel).unwrap();
761        }
762
763        let instance = match &self.component {
764            Some(component) => {
765                Instance::Component(self.component_linker.instantiate(&mut store, component)?)
766            }
767            None => {
768                let module = self
769                    .module
770                    .as_ref()
771                    .expect("compile the module before instantiating it");
772                Instance::Core(self.linker.instantiate(&mut store, &module)?)
773            }
774        };
775
776        (self.instantiation_end)(self.instantiation_timer);
777
778        self.store_and_instance = Some((store, instance));
779        Ok(())
780    }
781
782    fn execute(&mut self) -> Result<()> {
783        match self.store_and_instance.take() {
784            Some((mut store, Instance::Component(instance))) => {
785                let command =
786                    wasmtime_wasi::p2::bindings::sync::Command::new(&mut store, &instance)?;
787                match command.wasi_cli_run().call_run(&mut store)? {
788                    Ok(()) => Ok(()),
789                    Err(()) => Err(format_err!("calling `run` failed")),
790                }
791            }
792            Some((mut store, Instance::Core(instance))) => {
793                let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
794                match start_func.call(&mut store, ()) {
795                    Ok(_) => Ok(()),
796                    Err(trap) => {
797                        // Since _start will likely return by using the system `exit` call, we must
798                        // check the trap code to see if it actually represents a successful exit.
799                        if let Some(exit) = trap.downcast_ref::<I32Exit>() {
800                            if exit.0 == 0 {
801                                return Ok(());
802                            }
803                        }
804
805                        Err(trap)
806                    }
807                }
808            }
809            None => {
810                panic!("instantiate before execution")
811            }
812        }
813    }
814}