pyodide_webassembly_runtime_layer/
instance.rs

1use std::{collections::BTreeMap, sync::Arc};
2
3use fxhash::FxHashMap;
4use pyo3::{intern, prelude::*, sync::GILOnceCell};
5use wasm_runtime_layer::{
6    backend::{AsContext, AsContextMut, Export, Extern, Imports, WasmInstance, WasmModule},
7    ExportType, ExternType,
8};
9
10use crate::{
11    conversion::{create_js_object, ToPy},
12    Engine, Func, Global, Memory, Module, Table,
13};
14
15/// An instantiated instance of a WASM [`Module`].
16///
17/// This type wraps a [`WebAssembly.Instance`] from the JavaScript API.
18///
19/// [`WebAssembly.Instance`]: https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance
20#[derive(Debug)]
21pub struct Instance {
22    /// The inner instance
23    instance: Py<PyAny>,
24    /// The exports of the instance
25    exports: Arc<FxHashMap<String, Extern<Engine>>>,
26}
27
28impl Clone for Instance {
29    fn clone(&self) -> Self {
30        Python::with_gil(|py| Self {
31            instance: self.instance.clone_ref(py),
32            exports: self.exports.clone(),
33        })
34    }
35}
36
37impl WasmInstance<Engine> for Instance {
38    fn new(
39        _store: impl AsContextMut<Engine>,
40        module: &Module,
41        imports: &Imports<Engine>,
42    ) -> anyhow::Result<Self> {
43        Python::with_gil(|py| {
44            #[cfg(feature = "tracing")]
45            let _span = tracing::debug_span!("Instance::new").entered();
46
47            let imports_object = create_imports_object(py, imports)?;
48
49            let instance =
50                web_assembly_instance_new(py)?.call1((module.module(py), imports_object))?;
51
52            let exports = instance.getattr(intern!(py, "exports"))?;
53            let exports = process_exports(&exports, module)?;
54
55            Ok(Self {
56                instance: instance.unbind(),
57                exports: Arc::new(exports),
58            })
59        })
60    }
61
62    fn exports(&self, _store: impl AsContext<Engine>) -> Box<dyn Iterator<Item = Export<Engine>>> {
63        Box::new(
64            self.exports
65                .iter()
66                .map(|(name, value)| Export {
67                    name: name.into(),
68                    value: value.clone(),
69                })
70                .collect::<Vec<_>>()
71                .into_iter(),
72        )
73    }
74
75    fn get_export(&self, _store: impl AsContext<Engine>, name: &str) -> Option<Extern<Engine>> {
76        self.exports.get(name).cloned()
77    }
78}
79
80/// Creates the js import map
81fn create_imports_object<'py>(
82    py: Python<'py>,
83    imports: &Imports<Engine>,
84) -> Result<Bound<'py, PyAny>, PyErr> {
85    #[cfg(feature = "tracing")]
86    let _span = tracing::debug_span!("process_imports").entered();
87
88    let imports = imports
89        .iter()
90        .map(|(module, name, import)| -> Result<_, PyErr> {
91            #[cfg(feature = "tracing")]
92            tracing::trace!(?module, ?name, ?import, "import");
93
94            let import = import.to_py(py);
95
96            #[cfg(feature = "tracing")]
97            tracing::trace!(module, name, "export");
98
99            Ok((module, (name, import)))
100        })
101        .try_fold(
102            BTreeMap::<&str, Vec<_>>::new(),
103            |mut acc, elem| -> Result<_, PyErr> {
104                let (module, value) = elem?;
105                acc.entry(module).or_default().push(value);
106                Ok(acc)
107            },
108        )?
109        .into_iter()
110        .try_fold(
111            create_js_object(py)?,
112            |acc, (module, imports)| -> Result<_, PyErr> {
113                let obj = create_js_object(py)?;
114                for (name, import) in imports {
115                    obj.setattr(name, import)?;
116                }
117                acc.setattr(module, obj)?;
118                Ok(acc)
119            },
120        )?;
121
122    Ok(imports)
123}
124
125/// Processes a wasm module's exports into a hashmap
126fn process_exports(
127    exports: &Bound<PyAny>,
128    module: &Module,
129) -> anyhow::Result<FxHashMap<String, Extern<Engine>>> {
130    #[cfg(feature = "tracing")]
131    let _span = tracing::debug_span!("process_exports").entered();
132
133    module
134        .exports()
135        .map(|ExportType { name, ty }| {
136            let export = match ty {
137                ExternType::Func(signature) => Extern::Func(Func::from_exported_function(
138                    exports.getattr(name)?,
139                    signature,
140                )?),
141                ExternType::Global(signature) => Extern::Global(Global::from_exported_global(
142                    exports.getattr(name)?,
143                    signature,
144                )?),
145                ExternType::Memory(ty) => {
146                    Extern::Memory(Memory::from_exported_memory(exports.getattr(name)?, ty)?)
147                },
148                ExternType::Table(ty) => {
149                    Extern::Table(Table::from_exported_table(exports.getattr(name)?, ty)?)
150                },
151            };
152
153            Ok((String::from(name), export))
154        })
155        .collect()
156}
157
158fn web_assembly_instance_new(py: Python) -> Result<&Bound<PyAny>, PyErr> {
159    static WEB_ASSEMBLY_INSTANCE: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
160    WEB_ASSEMBLY_INSTANCE.import(py, "js.WebAssembly.Instance", "new")
161}