use pyo3::{intern, prelude::*, sync::GILOnceCell, types::PyBytes};
use wasm_runtime_layer::{
backend::{AsContext, AsContextMut, WasmMemory},
MemoryType,
};
use crate::{
conversion::{create_js_object, instanceof, js_uint8_array, ToPy},
Engine,
};
#[derive(Debug)]
pub struct Memory {
memory: Py<PyAny>,
ty: MemoryType,
}
impl Clone for Memory {
fn clone(&self) -> Self {
Python::with_gil(|py| Self {
memory: self.memory.clone_ref(py),
ty: self.ty,
})
}
}
impl WasmMemory<Engine> for Memory {
fn new(_ctx: impl AsContextMut<Engine>, ty: MemoryType) -> anyhow::Result<Self> {
Python::with_gil(|py| {
#[cfg(feature = "tracing")]
tracing::debug!(?ty, "Memory::new");
let desc = create_js_object(py)?;
desc.setattr(intern!(py, "initial"), ty.initial_pages())?;
if let Some(maximum) = ty.maximum_pages() {
desc.setattr(intern!(py, "maximum"), maximum)?;
}
let memory = web_assembly_memory(py)?.call_method1(intern!(py, "new"), (desc,))?;
Ok(Self {
memory: memory.unbind(),
ty,
})
})
}
fn ty(&self, _ctx: impl AsContext<Engine>) -> MemoryType {
self.ty
}
fn grow(&self, _ctx: impl AsContextMut<Engine>, additional: u32) -> anyhow::Result<u32> {
Python::with_gil(|py| {
let memory = self.memory.bind(py);
#[cfg(feature = "tracing")]
tracing::debug!(memory = %memory, ?self.ty, additional, "Memory::grow");
let old_pages = memory
.call_method1(intern!(py, "grow"), (additional,))?
.extract()?;
Ok(old_pages)
})
}
fn current_pages(&self, _ctx: impl AsContext<Engine>) -> u32 {
const PAGE_SIZE: u64 = 1 << 16;
Python::with_gil(|py| -> Result<u32, PyErr> {
let memory = self.memory.bind(py);
#[cfg(feature = "tracing")]
tracing::debug!(memory = %memory, ?self.ty, "Memory::current_pages");
let byte_len: u64 = memory
.getattr(intern!(py, "buffer"))?
.getattr(intern!(py, "byteLength"))?
.extract()?;
let pages = u32::try_from(byte_len / PAGE_SIZE)?;
Ok(pages)
})
.unwrap()
}
fn read(
&self,
_ctx: impl AsContext<Engine>,
offset: usize,
buffer: &mut [u8],
) -> anyhow::Result<()> {
Python::with_gil(|py| {
let memory = self.memory.bind(py);
#[cfg(feature = "tracing")]
tracing::debug!(memory = %memory, ?self.ty, offset, len = buffer.len(), "Memory::read");
let memory = memory.getattr(intern!(py, "buffer"))?;
let memory = js_uint8_array(py)?
.call_method1(intern!(py, "new"), (memory, offset, buffer.len()))?;
let bytes: Bound<PyBytes> = memory.call_method0(intern!(py, "to_bytes"))?.extract()?;
buffer.copy_from_slice(bytes.as_bytes());
Ok(())
})
}
fn write(
&self,
_ctx: impl AsContextMut<Engine>,
offset: usize,
buffer: &[u8],
) -> anyhow::Result<()> {
Python::with_gil(|py| {
let memory = self.memory.bind(py);
#[cfg(feature = "tracing")]
tracing::debug!(memory = %memory, ?self.ty, offset, len = buffer.len(), "Memory::write");
let memory = memory.getattr(intern!(py, "buffer"))?;
let memory = js_uint8_array(py)?
.call_method1(intern!(py, "new"), (memory, offset, buffer.len()))?;
memory.call_method1(intern!(py, "assign"), (buffer,))?;
Ok(())
})
}
}
impl ToPy for Memory {
fn to_py(&self, py: Python) -> Py<PyAny> {
#[cfg(feature = "tracing")]
tracing::trace!(value = %self.memory.bind(py), ?self.ty, "Memory::to_py");
self.memory.clone_ref(py)
}
}
impl Memory {
pub(crate) fn from_exported_memory(
memory: Bound<PyAny>,
ty: MemoryType,
) -> anyhow::Result<Self> {
if !instanceof(&memory, web_assembly_memory(memory.py())?)? {
anyhow::bail!("expected WebAssembly.Memory but found {memory}");
}
#[cfg(feature = "tracing")]
tracing::debug!(memory = %memory, ?ty, "Memory::from_exported_memory");
Ok(Self {
memory: memory.unbind(),
ty,
})
}
}
fn web_assembly_memory(py: Python) -> Result<&Bound<PyAny>, PyErr> {
static WEB_ASSEMBLY_MEMORY: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
WEB_ASSEMBLY_MEMORY
.get_or_try_init(py, || {
Ok(py
.import_bound(intern!(py, "js"))?
.getattr(intern!(py, "WebAssembly"))?
.getattr(intern!(py, "Memory"))?
.unbind())
})
.map(|x| x.bind(py))
}