wasm3x 0.1.0

Safe, Wasmi/Wasmtime-shaped Rust bindings for the Wasm3 interpreter.
Documentation
//! The [`Linker`] resolves module imports and instantiates modules.

use core::marker::PhantomData;
use std::ffi::CString;
use std::os::raw::c_void;
use std::sync::Arc;

use wasm3x_sys as ffi;

use crate::caller::Caller;
use crate::engine::{CompilationMode, Engine};
use crate::error::{Error, Result};
use crate::func::IntoFunc;
use crate::instance::Instance;
use crate::module::Module;
use crate::store::Store;
use crate::trampoline::{HostFuncEntry, HostImpl, HostValFn, host_trampoline};
use crate::value::{FuncType, Val};

/// A registry of host functions used to satisfy module imports and instantiate
/// [`Module`]s into a [`Store`].
///
/// Unlike Wasmtime, Wasm3 links host functions by name onto a loaded module.
/// A [`Linker`] therefore records host-function definitions and, at
/// instantiation, links each one that the module actually imports (definitions
/// the module does not import are ignored).
pub struct Linker<T> {
    engine: Engine,
    defs: Vec<HostFuncDef>,
    _marker: PhantomData<fn() -> T>,
}

struct HostFuncDef {
    module: CString,
    name: CString,
    signature: CString,
    entry: Arc<HostFuncEntry>,
}

impl<T> Linker<T> {
    /// Creates a new, empty [`Linker`].
    pub fn new(engine: &Engine) -> Self {
        Self {
            engine: engine.clone(),
            defs: Vec::new(),
            _marker: PhantomData,
        }
    }

    /// Returns the [`Engine`] this linker belongs to.
    pub fn engine(&self) -> &Engine {
        &self.engine
    }

    /// Defines a host function with a dynamically checked signature.
    ///
    /// The closure receives a [`Caller`] and the call arguments, and must write
    /// exactly `ty.results().len()` values into the results buffer.
    pub fn func_new(
        &mut self,
        module: &str,
        name: &str,
        ty: FuncType,
        func: impl Fn(Caller<'_, T>, &[Val], &mut [Val]) -> Result<()> + Send + Sync + 'static,
    ) -> Result<&mut Self> {
        let signature = ty.signature_cstring()?;
        let host: HostValFn = Box::new(move |raw, args, out| {
            let caller: Caller<'_, T> = Caller::from_raw(raw);
            func(caller, args, out)
        });
        self.push_def(module, name, signature, ty, HostImpl::Val(host))
    }

    /// Defines a host function from a Rust closure with a statically known
    /// signature (inferred from the closure's parameters and return type).
    ///
    /// The closure may optionally take a leading [`Caller<'_, T>`](Caller) to
    /// access the store's host data and linear memory; closures that do not need
    /// it can omit it entirely (e.g. `|x: i32| x * 2` or a plain `fn() -> u64`).
    pub fn func_wrap<Params, Results>(
        &mut self,
        module: &str,
        name: &str,
        func: impl IntoFunc<T, Params, Results>,
    ) -> Result<&mut Self> {
        let (ty, host) = func.into_host_func();
        let signature = ty.signature_cstring()?;
        self.push_def(module, name, signature, ty, HostImpl::Raw(host))
    }

    fn push_def(
        &mut self,
        module: &str,
        name: &str,
        signature: CString,
        ty: FuncType,
        func: HostImpl,
    ) -> Result<&mut Self> {
        let module = CString::new(module)
            .map_err(|_| Error::new("module name contains an interior nul byte"))?;
        let name = CString::new(name)
            .map_err(|_| Error::new("function name contains an interior nul byte"))?;
        self.defs.push(HostFuncDef {
            module,
            name,
            signature,
            entry: Arc::new(HostFuncEntry { ty, func }),
        });
        Ok(self)
    }

    /// Instantiates `module` into `store` and runs its start function.
    pub fn instantiate_and_start(&self, store: &mut Store<T>, module: &Module) -> Result<Instance> {
        let instance = self.instantiate_inner(store, module)?;
        // The start function may call host imports, so publish the host-data
        // pointer for the duration, just like a regular call.
        store.set_call_data();
        // SAFETY: `raw` is a freshly loaded module owned by the runtime.
        let result = unsafe { ffi::m3_RunStart(instance.raw()) };
        store.clear_call_data();
        Error::from_ffi(result)?;
        Ok(instance)
    }

    /// Instantiates `module` into `store` without running its start function.
    pub fn instantiate(&self, store: &mut Store<T>, module: &Module) -> Result<Instance> {
        self.instantiate_inner(store, module)
    }

    fn instantiate_inner(&self, store: &mut Store<T>, module: &Module) -> Result<Instance> {
        if !Engine::same(module.engine(), store.engine()) {
            return Err(Error::new("module and store belong to different engines"));
        }

        let bytes = module.bytes();
        let len = u32::try_from(bytes.len()).map_err(|_| Error::new("wasm module too large"))?;

        // Re-parse the module fresh for this instantiation.
        let mut raw: ffi::IM3Module = core::ptr::null_mut();
        // SAFETY: `bytes` outlives the parse call.
        unsafe {
            Error::from_ffi(ffi::m3_ParseModule(
                store.engine().raw(),
                &mut raw,
                bytes.as_ptr(),
                len,
            ))?;
        }

        // Keep the backing bytes alive: the loaded module references them.
        store.register_bytes(Arc::clone(&bytes));

        // Hand ownership of the module to the runtime.
        // SAFETY: `raw` was just parsed against this store's environment.
        if let Err(error) = Error::from_ffi(unsafe { ffi::m3_LoadModule(store.raw(), raw) }) {
            // On failure the runtime did not take ownership; free the module.
            unsafe { ffi::m3_FreeModule(raw) };
            return Err(error);
        }

        self.link_host_functions(store, raw)?;

        if store.engine().config().get_compilation_mode() == CompilationMode::Eager {
            // SAFETY: `raw` is loaded and all available imports are linked.
            Error::from_ffi(unsafe { ffi::m3_CompileModule(raw) })?;
        }

        Ok(Instance::new(raw, store.id()))
    }

    /// Links each defined host function that `module` imports; imports the
    /// module does not declare are silently skipped.
    fn link_host_functions(&self, store: &mut Store<T>, module: ffi::IM3Module) -> Result<()> {
        // SAFETY: reads the static `m3Err_functionLookupFailed` pointer.
        let lookup_failed = unsafe { ffi::m3Err_functionLookupFailed };
        for def in &self.defs {
            let entry = Arc::clone(&def.entry);
            let userdata = Arc::as_ptr(&entry) as *const c_void;
            // SAFETY: `module` is loaded; `def.entry` keeps `userdata` alive for
            // the duration of this call, and the store keeps it alive afterwards
            // once we know the link succeeded.
            let result = unsafe {
                ffi::m3_LinkRawFunctionEx(
                    module,
                    def.module.as_ptr(),
                    def.name.as_ptr(),
                    def.signature.as_ptr(),
                    Some(host_trampoline),
                    userdata,
                )
            };
            if result == lookup_failed {
                // The module does not import this function: nothing was linked,
                // so the entry does not need to outlive this loop iteration.
                continue;
            }
            Error::from_ffi(result)?;
            // The link succeeded and Wasm3 now holds `userdata`; keep the entry
            // alive for the runtime's lifetime.
            store.register_host_entry(entry);
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::caller::Caller;

    fn wasm(wat: &str) -> Vec<u8> {
        wat::parse_str(wat).expect("valid wat")
    }

    /// A linker may define host functions the module does not import; those must
    /// not be retained by the store (finding #3).
    #[test]
    fn unimported_host_functions_are_not_retained() {
        let engine = Engine::default();
        let mut store = Store::new(&engine, ());
        // The module imports only `env::used`, never `env::unused`.
        let module = Module::new(
            &engine,
            wasm(
                r#"(module
                      (import "env" "used" (func $used (param i32) (result i32)))
                      (func (export "go") (param i32) (result i32)
                        local.get 0 call $used))"#,
            ),
        )
        .unwrap();

        let mut linker = Linker::<()>::new(&engine);
        linker
            .func_wrap("env", "used", |_c: Caller<'_, ()>, x: i32| x)
            .unwrap();
        linker
            .func_wrap("env", "unused", |_c: Caller<'_, ()>, x: i32| x)
            .unwrap();

        linker.instantiate_and_start(&mut store, &module).unwrap();
        // Only `env::used` was linked, so only one entry is retained (not two).
        assert_eq!(store.host_entry_count(), 1);
    }

    /// A module that imports no host functions retains no entries, regardless of
    /// how many the linker defines.
    #[test]
    fn no_imports_retains_no_entries() {
        let engine = Engine::default();
        let mut store = Store::new(&engine, ());
        let module = Module::new(
            &engine,
            wasm(r#"(module (func (export "answer") (result i32) i32.const 42))"#),
        )
        .unwrap();

        let mut linker = Linker::<()>::new(&engine);
        linker
            .func_wrap("env", "unused", |_c: Caller<'_, ()>| 0i32)
            .unwrap();

        linker.instantiate_and_start(&mut store, &module).unwrap();
        assert_eq!(store.host_entry_count(), 0);
    }

    /// Host functions may be defined without a leading `Caller` parameter, both
    /// with and without arguments.
    #[test]
    fn func_wrap_without_caller() {
        let engine = Engine::default();
        let mut store = Store::new(&engine, ());
        let module = Module::new(
            &engine,
            wasm(
                r#"(module
                      (import "env" "double" (func $double (param i32) (result i32)))
                      (import "env" "now" (func $now (result i64)))
                      (func (export "go") (param i32) (result i32)
                        local.get 0 call $double)
                      (func (export "clock") (result i64)
                        call $now))"#,
            ),
        )
        .unwrap();

        let mut linker = Linker::<()>::new(&engine);
        // No `Caller`, with an argument.
        linker.func_wrap("env", "double", |x: i32| x * 2).unwrap();
        // No `Caller`, no arguments.
        linker.func_wrap("env", "now", || 42i64).unwrap();

        let instance = linker.instantiate_and_start(&mut store, &module).unwrap();

        let go = instance.get_typed_func::<i32, i32>(&store, "go").unwrap();
        assert_eq!(go.call(&mut store, 21).unwrap(), 42);

        let clock = instance.get_typed_func::<(), i64>(&store, "clock").unwrap();
        assert_eq!(clock.call(&mut store, ()).unwrap(), 42);
    }
}