fizzyx 0.1.0

Safe, ergonomic Rust bindings for the Fizzy WebAssembly interpreter.
Documentation
//! Registration of host functions and instantiation of modules.

use crate::engine::Engine;
use crate::error::{Error, Result, error_message};
use crate::instance::Instance;
use crate::module::Module;
use crate::trampoline::{HostFunc, trampoline};
use crate::value::{FuncType, Val};
use fizzyx_sys as sys;
use std::ffi::CString;
use std::rc::Rc;

/// A registered host function together with its `module::name` key.
struct HostImport {
    module: CString,
    name: CString,
    host: Rc<HostFunc>,
}

/// Collects host functions to satisfy a module's imports and instantiates modules.
///
/// A [`Linker`] mirrors the role of `wasmtime::Linker`: host functions are
/// defined up front with [`Linker::func_new`], then a [`Module`] is turned into
/// an [`Instance`] with [`Linker::instantiate`]. Fizzy resolves imports by
/// `module::name`, so the order of definitions does not matter and extra
/// definitions are ignored.
///
/// # Note
///
/// Only function imports are supported. Instantiating a module that imports a
/// memory, table, or global fails with [`Error::Instantiation`].
pub struct Linker {
    engine: Engine,
    imports: Vec<HostImport>,
}

impl Linker {
    /// Creates a new, empty [`Linker`] for the given [`Engine`].
    pub fn new(engine: &Engine) -> Self {
        Self {
            engine: engine.clone(),
            imports: Vec::new(),
        }
    }

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

    /// Defines a host function under `module::name` with signature `ty`.
    ///
    /// The closure receives the call arguments in `params` and writes its result
    /// (if any) into `results`, whose length matches `ty.results()`. It must not
    /// read or write outside those slices.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Instantiation`] if `module` or `name` contain an interior
    /// NUL byte.
    pub fn func_new<F>(
        &mut self,
        module: &str,
        name: &str,
        ty: FuncType,
        func: F,
    ) -> Result<&mut Self>
    where
        F: Fn(&[Val], &mut [Val]) + 'static,
    {
        let module = CString::new(module).map_err(|_| {
            Error::Instantiation("module name contains an interior NUL byte".into())
        })?;
        let name = CString::new(name).map_err(|_| {
            Error::Instantiation("function name contains an interior NUL byte".into())
        })?;
        self.imports.push(HostImport {
            module,
            name,
            host: Rc::new(HostFunc::new(ty, Box::new(func))),
        });
        Ok(self)
    }

    /// Instantiates `module`, resolving its imports against the defined host
    /// functions and running its start function (if any).
    ///
    /// # Errors
    ///
    /// Returns [`Error::Instantiation`] if an import cannot be resolved, an
    /// import type does not match, or the start function traps.
    pub fn instantiate(&self, module: &Module) -> Result<Instance> {
        // Keep the per-function input type arrays alive across the FFI call; the
        // `FizzyFunctionType` structs below borrow their addresses.
        let inputs: Vec<Vec<sys::FizzyValueType>> = self
            .imports
            .iter()
            .map(|imp| imp.host.ty().to_sys_parts().0)
            .collect();

        let imported: Vec<sys::FizzyImportedFunction> = self
            .imports
            .iter()
            .zip(&inputs)
            .map(|(imp, inputs)| {
                let (_, output) = imp.host.ty().to_sys_parts();
                let func_type = sys::FizzyFunctionType {
                    output,
                    inputs: if inputs.is_empty() {
                        core::ptr::null()
                    } else {
                        inputs.as_ptr()
                    },
                    inputs_size: inputs.len(),
                };
                sys::FizzyImportedFunction {
                    module: imp.module.as_ptr(),
                    name: imp.name.as_ptr(),
                    external_function: sys::FizzyExternalFunction {
                        type_: func_type,
                        function: Some(trampoline),
                        // Stable heap address of the `HostFunc`; kept alive by the
                        // returned `Instance`.
                        context: Rc::as_ptr(&imp.host) as *mut core::ffi::c_void,
                    },
                }
            })
            .collect();

        // `fizzy_resolve_instantiate` takes ownership of the module (freeing it
        // even on failure), so hand it an owned clone and keep `module` reusable.
        let module_ptr = module.clone_raw();
        if module_ptr.is_null() {
            return Err(Error::Instantiation("failed to clone module".into()));
        }

        let mut error = sys::FizzyError::default();
        // SAFETY: `module_ptr` is a freshly cloned, owned module. `imported` and
        // `inputs` outlive the call. No table/memory/global imports are provided.
        let inst = unsafe {
            sys::fizzy_resolve_instantiate(
                module_ptr,
                if imported.is_empty() {
                    core::ptr::null()
                } else {
                    imported.as_ptr()
                },
                imported.len(),
                core::ptr::null(),
                core::ptr::null(),
                core::ptr::null(),
                0,
                self.engine.memory_pages_limit(),
                &mut error,
            )
        };
        if inst.is_null() {
            return Err(Error::Instantiation(error_message(&error)));
        }

        // Keep the host closures alive for as long as the instance exists.
        let keepalive: Vec<Rc<HostFunc>> =
            self.imports.iter().map(|imp| imp.host.clone()).collect();
        // SAFETY: `inst` is a valid, owned instance pointer returned by Fizzy.
        Ok(unsafe { Instance::from_raw(inst, keepalive) })
    }
}