pyodide_webassembly_runtime_layer/
memory.rs

1use pyo3::{intern, prelude::*, sync::GILOnceCell, 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::with_gil(|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::with_gil(|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::with_gil(|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::with_gil(|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::with_gil(|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.call_method0(intern!(py, "to_bytes"))?.extract()?;
110            buffer.copy_from_slice(bytes.as_bytes());
111
112            Ok(())
113        })
114    }
115
116    fn write(
117        &self,
118        _ctx: impl AsContextMut<Engine>,
119        offset: usize,
120        buffer: &[u8],
121    ) -> anyhow::Result<()> {
122        Python::with_gil(|py| {
123            let memory = self.memory.bind(py);
124
125            #[cfg(feature = "tracing")]
126            tracing::debug!(memory = %memory, ?self.ty, offset, len = buffer.len(), "Memory::write");
127
128            let memory = memory.getattr(intern!(py, "buffer"))?;
129            let memory = js_uint8_array_new(py)?.call1((memory, offset, buffer.len()))?;
130
131            memory.call_method1(intern!(py, "assign"), (buffer,))?;
132
133            Ok(())
134        })
135    }
136}
137
138impl ToPy for Memory {
139    fn to_py(&self, py: Python) -> Py<PyAny> {
140        #[cfg(feature = "tracing")]
141        tracing::trace!(value = %self.memory.bind(py), ?self.ty, "Memory::to_py");
142
143        self.memory.clone_ref(py)
144    }
145}
146
147impl Memory {
148    /// Construct a memory from an exported memory object
149    pub(crate) fn from_exported_memory(
150        memory: Bound<PyAny>,
151        ty: MemoryType,
152    ) -> anyhow::Result<Self> {
153        if !instanceof(&memory, web_assembly_memory(memory.py())?)? {
154            anyhow::bail!("expected WebAssembly.Memory but found {memory}");
155        }
156
157        #[cfg(feature = "tracing")]
158        tracing::debug!(memory = %memory, ?ty, "Memory::from_exported_memory");
159
160        Ok(Self {
161            memory: memory.unbind(),
162            ty,
163        })
164    }
165}
166
167fn web_assembly_memory(py: Python) -> Result<&Bound<PyAny>, PyErr> {
168    static WEB_ASSEMBLY_MEMORY: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
169    WEB_ASSEMBLY_MEMORY.import(py, "js.WebAssembly", "Memory")
170}
171
172fn web_assembly_memory_new(py: Python) -> Result<&Bound<PyAny>, PyErr> {
173    static WEB_ASSEMBLY_MEMORY_NEW: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
174    WEB_ASSEMBLY_MEMORY_NEW.import(py, "js.WebAssembly.Memory", "new")
175}