fizzyx 0.1.1

Safe, ergonomic Rust bindings for the Fizzy WebAssembly interpreter.
Documentation
//! Parsed and validated WebAssembly modules.

use crate::error::{Error, Result, error_message};
use crate::value::{FuncType, GlobalType, MemoryType};
use fizzyx_sys as sys;

/// A parsed and validated WebAssembly module.
///
/// A module can be instantiated any number of times through a
/// [`Linker`](crate::Linker); each instantiation works on an internal clone, so
/// the [`Module`] remains reusable.
pub struct Module {
    inner: *const sys::FizzyModule,
}

impl Module {
    /// Parses and validates a WebAssembly binary into a [`Module`].
    ///
    /// # Errors
    ///
    /// Returns [`Error::Malformed`] if `wasm` is not a well-formed or valid
    /// WebAssembly 1.0 module.
    pub fn new(wasm: &[u8]) -> Result<Self> {
        let mut error = sys::FizzyError::default();
        // SAFETY: `wasm` is a valid slice and `error` is a valid out-pointer.
        let inner = unsafe { sys::fizzy_parse(wasm.as_ptr(), wasm.len(), &mut error) };
        if inner.is_null() {
            return Err(Error::Malformed(error_message(&error)));
        }
        Ok(Self { inner })
    }

    /// Validates a WebAssembly binary without retaining the parsed module.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Invalid`] if the module fails validation.
    pub fn validate(wasm: &[u8]) -> Result<()> {
        let mut error = sys::FizzyError::default();
        // SAFETY: `wasm` is a valid slice and `error` is a valid out-pointer.
        let ok = unsafe { sys::fizzy_validate(wasm.as_ptr(), wasm.len(), &mut error) };
        if ok {
            Ok(())
        } else {
            Err(Error::Invalid(error_message(&error)))
        }
    }

    /// Returns `true` if the module defines a start function.
    pub fn has_start_function(&self) -> bool {
        // SAFETY: `self.inner` is a valid, non-null module pointer.
        unsafe { sys::fizzy_module_has_start_function(self.inner) }
    }

    /// Returns the signature of the exported function named `name`, if present.
    pub fn func_type(&self, name: &str) -> Option<FuncType> {
        let c_name = std::ffi::CString::new(name).ok()?;
        let mut idx = 0u32;
        // SAFETY: valid module pointer and NUL-terminated name; `idx` is a valid
        // out-pointer.
        let found = unsafe {
            sys::fizzy_find_exported_function_index(self.inner, c_name.as_ptr(), &mut idx)
        };
        if !found {
            return None;
        }
        // SAFETY: `idx` was produced by a successful lookup, so it is in range.
        let raw = unsafe { sys::fizzy_get_function_type(self.inner, idx) };
        FuncType::from_sys(&raw).ok()
    }

    /// Returns the list of imports declared by the module.
    pub fn imports(&self) -> Vec<ImportType> {
        // SAFETY: valid module pointer.
        let count = unsafe { sys::fizzy_get_import_count(self.inner) };
        let mut imports = Vec::with_capacity(count as usize);
        for idx in 0..count {
            // SAFETY: `idx < count`, so the description is in range.
            let desc = unsafe { sys::fizzy_get_import_description(self.inner, idx) };
            let Some(ty) = extern_type_from_import(&desc) else {
                continue;
            };
            imports.push(ImportType {
                module: cstr_to_string(desc.module),
                name: cstr_to_string(desc.name),
                ty,
            });
        }
        imports
    }

    /// Returns the list of exports provided by the module.
    pub fn exports(&self) -> Vec<ExportType> {
        // SAFETY: valid module pointer.
        let count = unsafe { sys::fizzy_get_export_count(self.inner) };
        let mut exports = Vec::with_capacity(count as usize);
        for idx in 0..count {
            // SAFETY: `idx < count`, so the description is in range.
            let desc = unsafe { sys::fizzy_get_export_description(self.inner, idx) };
            let Some(kind) = ExternKind::from_sys(desc.kind) else {
                continue;
            };
            exports.push(ExportType {
                name: cstr_to_string(desc.name),
                kind,
                index: desc.index,
            });
        }
        exports
    }

    /// Returns a clone of the raw module pointer, transferring ownership to the
    /// caller (who must instantiate or free it).
    pub(crate) fn clone_raw(&self) -> *const sys::FizzyModule {
        // SAFETY: `self.inner` is a valid, non-null module pointer.
        unsafe { sys::fizzy_clone_module(self.inner) }
    }
}

impl Drop for Module {
    fn drop(&mut self) {
        // SAFETY: `self.inner` is owned by this `Module` and was never consumed
        // by instantiation (instantiation always clones first). `fizzy_free_module`
        // is NULL-safe regardless.
        unsafe { sys::fizzy_free_module(self.inner) }
    }
}

/// The kind of an imported or exported entity.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ExternKind {
    /// A function.
    Func,
    /// A table.
    Table,
    /// A linear memory.
    Memory,
    /// A global variable.
    Global,
}

impl ExternKind {
    fn from_sys(kind: sys::FizzyExternalKind) -> Option<Self> {
        match kind {
            sys::FizzyExternalKindFunction => Some(Self::Func),
            sys::FizzyExternalKindTable => Some(Self::Table),
            sys::FizzyExternalKindMemory => Some(Self::Memory),
            sys::FizzyExternalKindGlobal => Some(Self::Global),
            _ => None,
        }
    }
}

/// The resolved type of an imported entity.
#[derive(Debug, Clone, PartialEq)]
pub enum ExternType {
    /// A function with the given signature.
    Func(FuncType),
    /// A table. WebAssembly 1.0 tables carry no element type of interest here.
    Table,
    /// A linear memory with the given limits.
    Memory(MemoryType),
    /// A global with the given type.
    Global(GlobalType),
}

impl ExternType {
    /// Returns the [`ExternKind`] of this type.
    pub fn kind(&self) -> ExternKind {
        match self {
            Self::Func(_) => ExternKind::Func,
            Self::Table => ExternKind::Table,
            Self::Memory(_) => ExternKind::Memory,
            Self::Global(_) => ExternKind::Global,
        }
    }
}

/// A description of a single module import.
#[derive(Debug, Clone, PartialEq)]
pub struct ImportType {
    module: String,
    name: String,
    ty: ExternType,
}

impl ImportType {
    /// Returns the module name of the import (the `module` in `module::name`).
    pub fn module(&self) -> &str {
        &self.module
    }

    /// Returns the field name of the import (the `name` in `module::name`).
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the resolved type of the import.
    pub fn ty(&self) -> &ExternType {
        &self.ty
    }
}

/// A description of a single module export.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportType {
    name: String,
    kind: ExternKind,
    index: u32,
}

impl ExportType {
    /// Returns the name of the export.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the kind of the exported entity.
    pub fn kind(&self) -> ExternKind {
        self.kind
    }

    /// Returns the module-local index of the exported entity.
    pub fn index(&self) -> u32 {
        self.index
    }
}

fn extern_type_from_import(desc: &sys::FizzyImportDescription) -> Option<ExternType> {
    // SAFETY: the active union member is determined by `desc.kind`, as documented
    // by the Fizzy C API.
    unsafe {
        match desc.kind {
            sys::FizzyExternalKindFunction => FuncType::from_sys(&desc.desc.function_type)
                .ok()
                .map(ExternType::Func),
            sys::FizzyExternalKindTable => Some(ExternType::Table),
            sys::FizzyExternalKindMemory => Some(ExternType::Memory(MemoryType::from_sys(
                desc.desc.memory_limits,
            ))),
            sys::FizzyExternalKindGlobal => GlobalType::from_sys(desc.desc.global_type)
                .ok()
                .map(ExternType::Global),
            _ => None,
        }
    }
}

fn cstr_to_string(ptr: *const core::ffi::c_char) -> String {
    if ptr.is_null() {
        return String::new();
    }
    // SAFETY: Fizzy hands out NUL-terminated strings that live as long as the
    // module; we copy the contents immediately.
    unsafe { core::ffi::CStr::from_ptr(ptr) }
        .to_string_lossy()
        .into_owned()
}