//! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation,
//! and execution.
//!
//! The API expects calls that match the following state machine:
//!
//! ```text
//!               |
//!               |
//!               V
//! .---> wasm_bench_create
//! |        |        |
//! |        |        |
//! |        |        V
//! |        |   wasm_bench_compile
//! |        |     |            |
//! |        |     |            |     .----.
//! |        |     |            |     |    |
//! |        |     |            V     V    |
//! |        |     |     wasm_bench_instantiate <------.
//! |        |     |            |        |             |
//! |        |     |            |        |             |
//! |        |     |            |        |             |
//! |        |     |     .------'        '-----> wasm_bench_execute
//! |        |     |     |                             |
//! |        |     |     |                             |
//! |        V     V     V                             |
//! '------ wasm_bench_free <--------------------------'
//!               |
//!               |
//!               V
//! ```
//!
//! 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 std::ptr;
//! use wasmtime_bench_api::*;
//!
//! let working_dir = std::env::current_dir().unwrap().display().to_string();
//! let stdout_path = "./stdout.log";
//! let stderr_path = "./stderr.log";
//!
//! // Functions to start/end timers for compilation.
//! //
//! // The `compilation_timer` pointer configured in the `WasmBenchConfig` is
//! // passed through.
//! extern "C" fn compilation_start(timer: *mut u8) {
//!     // Start your compilation timer here.
//! }
//! extern "C" fn compilation_end(timer: *mut u8) {
//!     // End your compilation timer here.
//! }
//!
//! // Similar for instantiation.
//! extern "C" fn instantiation_start(timer: *mut u8) {
//!     // Start your instantiation timer here.
//! }
//! extern "C" fn instantiation_end(timer: *mut u8) {
//!     // End your instantiation timer here.
//! }
//!
//! // Similar for execution.
//! extern "C" fn execution_start(timer: *mut u8) {
//!     // Start your execution timer here.
//! }
//! extern "C" fn execution_end(timer: *mut u8) {
//!     // End your execution timer here.
//! }
//!
//! let config = WasmBenchConfig {
//!     working_dir_ptr: working_dir.as_ptr(),
//!     working_dir_len: working_dir.len(),
//!     stdout_path_ptr: stdout_path.as_ptr(),
//!     stdout_path_len: stdout_path.len(),
//!     stderr_path_ptr: stderr_path.as_ptr(),
//!     stderr_path_len: stderr_path.len(),
//!     stdin_path_ptr: ptr::null(),
//!     stdin_path_len: 0,
//!     compilation_timer: ptr::null_mut(),
//!     compilation_start,
//!     compilation_end,
//!     instantiation_timer: ptr::null_mut(),
//!     instantiation_start,
//!     instantiation_end,
//!     execution_timer: ptr::null_mut(),
//!     execution_start,
//!     execution_end,
//!     execution_flags_ptr: ptr::null(),
//!     execution_flags_len: 0,
//! };
//!
//! let mut bench_api = ptr::null_mut();
//! unsafe {
//!     let code = wasm_bench_create(config, &mut bench_api);
//!     assert_eq!(code, OK);
//!     assert!(!bench_api.is_null());
//! };
//!
//! 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();
//!
//! // This will call the `compilation_{start,end}` timing functions on success.
//! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) };
//! assert_eq!(code, OK);
//!
//! // This will call the `instantiation_{start,end}` timing functions on success.
//! let code = unsafe { wasm_bench_instantiate(bench_api) };
//! assert_eq!(code, OK);
//!
//! // This will call the `execution_{start,end}` timing functions on success.
//! let code = unsafe { wasm_bench_execute(bench_api) };
//! assert_eq!(code, OK);
//!
//! unsafe {
//!     wasm_bench_free(bench_api);
//! }
//! ```

mod unsafe_send_sync;

use crate::unsafe_send_sync::UnsafeSendSync;
use anyhow::{Context, Result};
use clap::Parser;
use std::os::raw::{c_int, c_void};
use std::slice;
use std::{env, path::PathBuf};
use target_lexicon::Triple;
use wasmtime::{Engine, Instance, Linker, Module, Store};
use wasmtime_cli_flags::{CommonOptions, WasiModules};
use wasmtime_wasi::{sync::WasiCtxBuilder, I32Exit, WasiCtx};

pub type ExitCode = c_int;
pub const OK: ExitCode = 0;
pub const ERR: ExitCode = -1;

// Randomize the location of heap objects to avoid accidental locality being an
// uncontrolled variable that obscures performance evaluation in our
// experiments.
#[cfg(feature = "shuffling-allocator")]
#[global_allocator]
static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> =
    shuffling_allocator::wrap!(&std::alloc::System);

/// Configuration options for the benchmark.
#[repr(C)]
pub struct WasmBenchConfig {
    /// The working directory where benchmarks should be executed.
    pub working_dir_ptr: *const u8,
    pub working_dir_len: usize,

    /// The file path that should be created and used as `stdout`.
    pub stdout_path_ptr: *const u8,
    pub stdout_path_len: usize,

    /// The file path that should be created and used as `stderr`.
    pub stderr_path_ptr: *const u8,
    pub stderr_path_len: usize,

    /// The (optional) file path that should be opened and used as `stdin`. If
    /// not provided, then the WASI context will not have a `stdin` initialized.
    pub stdin_path_ptr: *const u8,
    pub stdin_path_len: usize,

    /// The functions to start and stop performance timers/counters during Wasm
    /// compilation.
    pub compilation_timer: *mut u8,
    pub compilation_start: extern "C" fn(*mut u8),
    pub compilation_end: extern "C" fn(*mut u8),

    /// The functions to start and stop performance timers/counters during Wasm
    /// instantiation.
    pub instantiation_timer: *mut u8,
    pub instantiation_start: extern "C" fn(*mut u8),
    pub instantiation_end: extern "C" fn(*mut u8),

    /// The functions to start and stop performance timers/counters during Wasm
    /// execution.
    pub execution_timer: *mut u8,
    pub execution_start: extern "C" fn(*mut u8),
    pub execution_end: extern "C" fn(*mut u8),

    /// The (optional) flags to use when running Wasmtime. These correspond to
    /// the flags used when running Wasmtime from the command line.
    pub execution_flags_ptr: *const u8,
    pub execution_flags_len: usize,
}

impl WasmBenchConfig {
    fn working_dir(&self) -> Result<PathBuf> {
        let working_dir =
            unsafe { std::slice::from_raw_parts(self.working_dir_ptr, self.working_dir_len) };
        let working_dir = std::str::from_utf8(working_dir)
            .context("given working directory is not valid UTF-8")?;
        Ok(working_dir.into())
    }

    fn stdout_path(&self) -> Result<PathBuf> {
        let stdout_path =
            unsafe { std::slice::from_raw_parts(self.stdout_path_ptr, self.stdout_path_len) };
        let stdout_path =
            std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?;
        Ok(stdout_path.into())
    }

    fn stderr_path(&self) -> Result<PathBuf> {
        let stderr_path =
            unsafe { std::slice::from_raw_parts(self.stderr_path_ptr, self.stderr_path_len) };
        let stderr_path =
            std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?;
        Ok(stderr_path.into())
    }

    fn stdin_path(&self) -> Result<Option<PathBuf>> {
        if self.stdin_path_ptr.is_null() {
            return Ok(None);
        }

        let stdin_path =
            unsafe { std::slice::from_raw_parts(self.stdin_path_ptr, self.stdin_path_len) };
        let stdin_path =
            std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?;
        Ok(Some(stdin_path.into()))
    }

    fn execution_flags(&self) -> Result<CommonOptions> {
        let flags = if self.execution_flags_ptr.is_null() {
            ""
        } else {
            let execution_flags = unsafe {
                std::slice::from_raw_parts(self.execution_flags_ptr, self.execution_flags_len)
            };
            std::str::from_utf8(execution_flags)
                .context("given execution flags string is not valid UTF-8")?
        };
        let options = CommonOptions::try_parse_from(
            ["wasmtime"]
                .into_iter()
                .chain(flags.split(' ').filter(|s| !s.is_empty())),
        )
        .context("failed to parse options")?;
        Ok(options)
    }
}

/// Exposes a C-compatible way of creating the engine from the bytes of a single
/// Wasm module.
///
/// On success, the `out_bench_ptr` is initialized to a pointer to a structure
/// that contains the engine's initialized state, and `0` is returned. On
/// failure, a non-zero status code is returned and `out_bench_ptr` is left
/// untouched.
#[no_mangle]
pub extern "C" fn wasm_bench_create(
    config: WasmBenchConfig,
    out_bench_ptr: *mut *mut c_void,
) -> ExitCode {
    let result = (|| -> Result<_> {
        let working_dir = config.working_dir()?;
        let working_dir =
            cap_std::fs::Dir::open_ambient_dir(&working_dir, cap_std::ambient_authority())
                .with_context(|| {
                    format!(
                        "failed to preopen the working directory: {}",
                        working_dir.display(),
                    )
                })?;

        let stdout_path = config.stdout_path()?;
        let stderr_path = config.stderr_path()?;
        let stdin_path = config.stdin_path()?;
        let options = config.execution_flags()?;

        let state = Box::new(BenchState::new(
            options,
            config.compilation_timer,
            config.compilation_start,
            config.compilation_end,
            config.instantiation_timer,
            config.instantiation_start,
            config.instantiation_end,
            config.execution_timer,
            config.execution_start,
            config.execution_end,
            move || {
                let mut cx = WasiCtxBuilder::new();

                let stdout = std::fs::File::create(&stdout_path)
                    .with_context(|| format!("failed to create {}", stdout_path.display()))?;
                let stdout = cap_std::fs::File::from_std(stdout);
                let stdout = wasi_cap_std_sync::file::File::from_cap_std(stdout);
                cx = cx.stdout(Box::new(stdout));

                let stderr = std::fs::File::create(&stderr_path)
                    .with_context(|| format!("failed to create {}", stderr_path.display()))?;
                let stderr = cap_std::fs::File::from_std(stderr);
                let stderr = wasi_cap_std_sync::file::File::from_cap_std(stderr);
                cx = cx.stderr(Box::new(stderr));

                if let Some(stdin_path) = &stdin_path {
                    let stdin = std::fs::File::open(stdin_path)
                        .with_context(|| format!("failed to open {}", stdin_path.display()))?;
                    let stdin = cap_std::fs::File::from_std(stdin);
                    let stdin = wasi_cap_std_sync::file::File::from_cap_std(stdin);
                    cx = cx.stdin(Box::new(stdin));
                }

                // Allow access to the working directory so that the benchmark can read
                // its input workload(s).
                cx = cx.preopened_dir(working_dir.try_clone()?, ".")?;

                // Pass this env var along so that the benchmark program can use smaller
                // input workload(s) if it has them and that has been requested.
                if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
                    cx = cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val)?;
                }

                Ok(cx.build())
            },
        )?);
        Ok(Box::into_raw(state) as _)
    })();

    if let Ok(bench_ptr) = result {
        unsafe {
            assert!(!out_bench_ptr.is_null());
            *out_bench_ptr = bench_ptr;
        }
    }

    to_exit_code(result.map(|_| ()))
}

/// Free the engine state allocated by this library.
#[no_mangle]
pub extern "C" fn wasm_bench_free(state: *mut c_void) {
    assert!(!state.is_null());
    unsafe {
        drop(Box::from_raw(state as *mut BenchState));
    }
}

/// Compile the Wasm benchmark module.
#[no_mangle]
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 wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
    let result = state.instantiate().context("failed to instantiate");
    to_exit_code(result)
}

/// Execute the Wasm benchmark module.
#[no_mangle]
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.
///
/// This will print an error indicating some information regarding the failure.
fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
    match result.into() {
        Ok(_) => OK,
        Err(error) => {
            eprintln!("{:?}", error);
            ERR
        }
    }
}

/// This structure contains the actual Rust implementation of the state required
/// to manage the Wasmtime engine between calls.
struct BenchState {
    linker: Linker<HostState>,
    compilation_timer: *mut u8,
    compilation_start: extern "C" fn(*mut u8),
    compilation_end: extern "C" fn(*mut u8),
    instantiation_timer: *mut u8,
    instantiation_start: extern "C" fn(*mut u8),
    instantiation_end: extern "C" fn(*mut u8),
    make_wasi_cx: Box<dyn FnMut() -> Result<WasiCtx>>,
    module: Option<Module>,
    store_and_instance: Option<(Store<HostState>, Instance)>,
    epoch_interruption: bool,
    fuel: Option<u64>,
}

struct HostState {
    wasi: WasiCtx,
    #[cfg(feature = "wasi-nn")]
    wasi_nn: wasmtime_wasi_nn::WasiNnCtx,
    #[cfg(feature = "wasi-crypto")]
    wasi_crypto: wasmtime_wasi_crypto::WasiCryptoCtx,
}

impl BenchState {
    fn new(
        options: CommonOptions,
        compilation_timer: *mut u8,
        compilation_start: extern "C" fn(*mut u8),
        compilation_end: extern "C" fn(*mut u8),
        instantiation_timer: *mut u8,
        instantiation_start: extern "C" fn(*mut u8),
        instantiation_end: extern "C" fn(*mut u8),
        execution_timer: *mut u8,
        execution_start: extern "C" fn(*mut u8),
        execution_end: extern "C" fn(*mut u8),
        make_wasi_cx: impl FnMut() -> Result<WasiCtx> + 'static,
    ) -> Result<Self> {
        let mut config = options.config(Some(&Triple::host().to_string()))?;
        // NB: always disable the compilation cache.
        config.disable_cache();
        let engine = Engine::new(&config)?;
        let mut linker = Linker::<HostState>::new(&engine);

        // Define the benchmarking start/end functions.
        let execution_timer = unsafe {
            // Safe because this bench API's contract requires that its methods
            // are only ever called from a single thread.
            UnsafeSendSync::new(execution_timer)
        };
        linker.func_wrap("bench", "start", move || {
            execution_start(*execution_timer.get());
            Ok(())
        })?;
        linker.func_wrap("bench", "end", move || {
            execution_end(*execution_timer.get());
            Ok(())
        })?;

        let epoch_interruption = options.epoch_interruption;
        let fuel = options.fuel;

        let wasi_modules = options.wasi_modules.unwrap_or(WasiModules::default());

        if wasi_modules.wasi_common {
            wasmtime_wasi::add_to_linker(&mut linker, |cx| &mut cx.wasi)?;
        }

        #[cfg(feature = "wasi-nn")]
        if wasi_modules.wasi_nn {
            wasmtime_wasi_nn::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
        }

        #[cfg(feature = "wasi-crypto")]
        if wasi_modules.wasi_crypto {
            wasmtime_wasi_crypto::add_to_linker(&mut linker, |cx| &mut cx.wasi_crypto)?;
        }

        Ok(Self {
            linker,
            compilation_timer,
            compilation_start,
            compilation_end,
            instantiation_timer,
            instantiation_start,
            instantiation_end,
            make_wasi_cx: Box::new(make_wasi_cx) as _,
            module: None,
            store_and_instance: None,
            epoch_interruption,
            fuel,
        })
    }

    fn compile(&mut self, bytes: &[u8]) -> Result<()> {
        assert!(
            self.module.is_none(),
            "create a new engine to repeat compilation"
        );

        (self.compilation_start)(self.compilation_timer);
        let module = Module::from_binary(self.linker.engine(), bytes)?;
        (self.compilation_end)(self.compilation_timer);

        self.module = Some(module);
        Ok(())
    }

    fn instantiate(&mut self) -> Result<()> {
        let module = self
            .module
            .as_ref()
            .expect("compile the module before instantiating it");

        let host = HostState {
            wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
            #[cfg(feature = "wasi-nn")]
            wasi_nn: wasmtime_wasi_nn::WasiNnCtx::new()?,
            #[cfg(feature = "wasi-crypto")]
            wasi_crypto: wasmtime_wasi_nn::WasiCryptoCtx::new(),
        };

        // NB: Start measuring instantiation time *after* we've created the WASI
        // context, since that needs to do file I/O to setup
        // stdin/stdout/stderr.
        (self.instantiation_start)(self.instantiation_timer);
        let mut store = Store::new(self.linker.engine(), host);
        if self.epoch_interruption {
            store.set_epoch_deadline(1);
        }
        if let Some(fuel) = self.fuel {
            store.add_fuel(fuel).unwrap();
        }

        let instance = self.linker.instantiate(&mut store, &module)?;
        (self.instantiation_end)(self.instantiation_timer);

        self.store_and_instance = Some((store, instance));
        Ok(())
    }

    fn execute(&mut self) -> Result<()> {
        let (mut store, instance) = self
            .store_and_instance
            .take()
            .expect("instantiate the module before executing it");

        let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
        match start_func.call(&mut store, ()) {
            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.
                if let Some(exit) = trap.downcast_ref::<I32Exit>() {
                    if exit.0 == 0 {
                        return Ok(());
                    }
                }

                Err(trap)
            }
        }
    }
}
