wasm-bridge 0.4.0

Run WASM modules on desktop or on the web using wasmtime's API
Documentation
use std::{collections::HashMap, rc::Rc};

use crate::{
    helpers::{map_js_error, static_str_to_js},
    *,
};
use anyhow::{bail, Context};
use js_sys::{
    Function, Object, Reflect,
    WebAssembly::{self},
};
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;

pub struct Instance {
    exports: HashMap<String, JsValue>,
    closures: Rc<Vec<DropHandle>>,
}

impl Instance {
    #[deprecated(
        since = "0.4.0",
        note = "Instantiating a module synchronously can panic on the web, please use `new_safe` instead."
    )]
    pub fn new(_store: impl AsContextMut, module: &Module, _imports: &[()]) -> Result<Self> {
        let imports = Object::new();
        Self::new_with_imports(module, &imports, vec![])
    }

    pub async fn new_safe(
        _store: impl AsContextMut,
        module: &Module,
        _imports: &[()],
    ) -> Result<Self> {
        let imports = Object::new();
        Self::new_with_imports_async(module, &imports, vec![]).await
    }

    pub(crate) fn new_with_imports(
        module: &Module,
        imports: &Object,
        closures: Vec<DropHandle>,
    ) -> Result<Self> {
        let instance = WebAssembly::Instance::new(&module.module, imports)
            .map_err(map_js_error("Instantiate WebAssembly module"))?;

        Self::from_js_object(instance.into(), closures)
    }

    pub(crate) async fn new_with_imports_async(
        module: &Module,
        imports: &Object,
        closures: Vec<DropHandle>,
    ) -> Result<Self> {
        let promise = WebAssembly::instantiate_module(&module.module, imports);

        let instance = JsFuture::from(promise)
            .await
            .map_err(map_js_error("Instantiate WebAssembly module"))?;

        Self::from_js_object(instance, closures)
    }

    fn from_js_object(instance: JsValue, closures: Vec<DropHandle>) -> Result<Self> {
        let exports = Reflect::get(&instance, static_str_to_js("exports"))
            .map_err(map_js_error("Get instance's exports"))?;

        Ok(Self {
            exports: process_exports(exports)?,
            closures: Rc::new(closures),
        })
    }

    pub fn get_memory(&self, _store: impl AsContextMut, name: &str) -> Option<Memory> {
        let memory = self.exports.get(name)?;

        if memory.is_object() {
            Some(Memory::new(memory.clone().into()))
        } else {
            None
        }
    }

    pub fn get_func(&self, _store: impl AsContextMut, name: &str) -> Option<Func> {
        let function = self.get_func_inner(name).ok()?;

        Some(Func::new(function, self.closures.clone()))
    }

    pub fn get_typed_func<Params: ToJsValue, Results: FromJsValue>(
        &self,
        _store: impl AsContextMut,
        name: &str,
    ) -> Result<TypedFunc<Params, Results>> {
        let function = self.get_func_inner(name)?;

        if function.length() != Params::number_of_args() {
            bail!(
                "Exported function {name} should have {} arguments, but it has {} instead.",
                Params::number_of_args(),
                function.length(),
            );
        }

        Ok(TypedFunc::new(function, self.closures.clone()))
    }

    fn get_func_inner(&self, name: &str) -> Result<Function> {
        let function = self
            .exports
            .get(name)
            .context("Exported function '{name}' not found")?;

        if !function.is_function() {
            bail!("Exported object '{function:?}' with name '{name}' is not a function");
        }

        Ok(function.clone().into())
    }
}

fn process_exports(js_exports: JsValue) -> Result<HashMap<String, JsValue>> {
    if !js_exports.is_object() {
        bail!(
            "WebAssembly exports must be an object, got '{:?}' instead",
            js_exports
        );
    }

    let js_exports: Object = js_exports.into();
    let names = Object::get_own_property_names(&js_exports);
    let len = names.length();

    let mut exports = HashMap::new();
    for i in 0..len {
        let name_js = Reflect::get_u32(&names, i).expect("names is array");
        let name = name_js.as_string().expect("name is string");
        let export = Reflect::get(&js_exports, &name_js).expect("js_exports is object");
        exports.insert(name, export);
    }
    Ok(exports)
}