pyodide_webassembly_runtime_layer/
memory.rs

1use pyo3::{intern, prelude::*, sync::PyOnceLock, types::PyBytes};
2use wasm_runtime_layer::{
3    backend::{AsContext, AsContextMut, WasmMemory},
4    MemoryType,
5};
6
7use crate::{
8    conversion::{create_js_object, instanceof, js_uint8_array_new, ToPy},
9    Engine,
10};
11
12#[derive(Debug)]
13/// A WASM memory.
14///
15/// This type wraps a [`WebAssembly.Memory`] from the JavaScript API.
16///
17/// [`WebAssembly.Memory`]: https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory
18pub struct Memory {
19    /// The memory value
20    memory: Py<PyAny>,
21    /// The memory type
22    ty: MemoryType,
23}
24
25impl Clone for Memory {
26    fn clone(&self) -> Self {
27        Python::attach(|py| Self {
28            memory: self.memory.clone_ref(py),
29            ty: self.ty,
30        })
31    }
32}
33
34impl WasmMemory<Engine> for Memory {
35    fn new(_ctx: impl AsContextMut<Engine>, ty: MemoryType) -> anyhow::Result<Self> {
36        Python::attach(|py| {
37            #[cfg(feature = "tracing")]
38            tracing::debug!(?ty, "Memory::new");
39
40            let desc = create_js_object(py)?;
41            desc.setattr(intern!(py, "initial"), ty.initial_pages())?;
42            if let Some(maximum) = ty.maximum_pages() {
43                desc.setattr(intern!(py, "maximum"), maximum)?;
44            }
45
46            let memory = web_assembly_memory_new(py)?.call1((desc,))?;
47
48            Ok(Self {
49                memory: memory.unbind(),
50                ty,
51            })
52        })
53    }
54
55    fn ty(&self, _ctx: impl AsContext<Engine>) -> MemoryType {
56        self.ty
57    }
58
59    fn grow(&self, _ctx: impl AsContextMut<Engine>, additional: u32) -> anyhow::Result<u32> {
60        Python::attach(|py| {
61            let memory = self.memory.bind(py);
62
63            #[cfg(feature = "tracing")]
64            tracing::debug!(memory = %memory, ?self.ty, additional, "Memory::grow");
65
66            let old_pages = memory
67                .call_method1(intern!(py, "grow"), (additional,))?
68                .extract()?;
69
70            Ok(old_pages)
71        })
72    }
73
74    fn current_pages(&self, _ctx: impl AsContext<Engine>) -> u32 {
75        const PAGE_SIZE: u64 = 1 << 16;
76
77        Python::attach(|py| -> Result<u32, PyErr> {
78            let memory = self.memory.bind(py);
79
80            #[cfg(feature = "tracing")]
81            tracing::debug!(memory = %memory, ?self.ty, "Memory::current_pages");
82
83            let byte_len: u64 = memory
84                .getattr(intern!(py, "buffer"))?
85                .getattr(intern!(py, "byteLength"))?
86                .extract()?;
87
88            let pages = u32::try_from(byte_len / PAGE_SIZE)?;
89            Ok(pages)
90        })
91        .expect("Memory::current_pages should not fail")
92    }
93
94    fn read(
95        &self,
96        _ctx: impl AsContext<Engine>,
97        offset: usize,
98        buffer: &mut [u8],
99    ) -> anyhow::Result<()> {
100        Python::attach(|py| {
101            let memory = self.memory.bind(py);
102
103            #[cfg(feature = "tracing")]
104            tracing::debug!(memory = %memory, ?self.ty, offset, len = buffer.len(), "Memory::read");
105
106            let memory = memory.getattr(intern!(py, "buffer"))?;
107            let memory = js_uint8_array_new(py)?.call1((memory, offset, buffer.len()))?;
108
109            let bytes: Bound<PyBytes> = memory
110                .call_method0(intern!(py, "to_bytes"))?
111                .extract()
112                .map_err(PyErr::from)?;
113            buffer.copy_from_slice(bytes.as_bytes());
114
115            Ok(())
116        })
117    }
118
119    fn write(
120        &self,
121        _ctx: impl AsContextMut<Engine>,
122        offset: usize,
123        buffer: &[u8],
124    ) -> anyhow::Result<()> {
125        Python::attach(|py| {
126            let memory = self.memory.bind(py);
127
128            #[cfg(feature = "tracing")]
129            tracing::debug!(memory = %memory, ?self.ty, offset, len = buffer.len(), "Memory::write");
130
131            let memory = memory.getattr(intern!(py, "buffer"))?;
132            let memory = js_uint8_array_new(py)?.call1((memory, offset, buffer.len()))?;
133
134            memory.call_method1(intern!(py, "assign"), (buffer,))?;
135
136            Ok(())
137        })
138    }
139}
140
141impl ToPy for Memory {
142    fn to_py(&self, py: Python) -> Py<PyAny> {
143        #[cfg(feature = "tracing")]
144        tracing::trace!(value = %self.memory.bind(py), ?self.ty, "Memory::to_py");
145
146        self.memory.clone_ref(py)
147    }
148}
149
150impl Memory {
151    /// Construct a memory from an exported memory object
152    pub(crate) fn from_exported_memory(
153        memory: Bound<PyAny>,
154        ty: MemoryType,
155    ) -> anyhow::Result<Self> {
156        if !instanceof(&memory, web_assembly_memory(memory.py())?)? {
157            anyhow::bail!("expected WebAssembly.Memory but found {memory}");
158        }
159
160        #[cfg(feature = "tracing")]
161        tracing::debug!(memory = %memory, ?ty, "Memory::from_exported_memory");
162
163        Ok(Self {
164            memory: memory.unbind(),
165            ty,
166        })
167    }
168}
169
170fn web_assembly_memory(py: Python<'_>) -> Result<&Bound<'_, PyAny>, PyErr> {
171    static WEB_ASSEMBLY_MEMORY: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
172    WEB_ASSEMBLY_MEMORY.import(py, "js.WebAssembly", "Memory")
173}
174
175fn web_assembly_memory_new(py: Python<'_>) -> Result<&Bound<'_, PyAny>, PyErr> {
176    static WEB_ASSEMBLY_MEMORY_NEW: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
177    WEB_ASSEMBLY_MEMORY_NEW.import(py, "js.WebAssembly.Memory", "new")
178}