Skip to main content

pyodide_webassembly_runtime_layer/
table.rs

1use pyo3::{intern, prelude::*, sync::PyOnceLock};
2use wasm_runtime_layer::{
3    backend::{AsContext, AsContextMut, Ref, WasmTable},
4    RefType, TableType,
5};
6
7use crate::{
8    conversion::{create_js_object, instanceof, RefExt, RefTypeExt, ToPy},
9    Engine,
10};
11
12#[derive(Debug)]
13/// A WASM table.
14///
15/// This type wraps a [`WebAssembly.Table`] from the JavaScript API.
16///
17/// [`WebAssembly.Table`]: https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Table
18pub struct Table {
19    /// Table reference
20    table: Py<PyAny>,
21    /// The table signature
22    ty: TableType,
23}
24
25impl Clone for Table {
26    fn clone(&self) -> Self {
27        Python::attach(|py| Self {
28            table: self.table.clone_ref(py),
29            ty: self.ty,
30        })
31    }
32}
33
34impl WasmTable<Engine> for Table {
35    fn new(
36        _ctx: impl AsContextMut<Engine>,
37        ty: TableType,
38        init: Ref<Engine>,
39    ) -> anyhow::Result<Self> {
40        Python::attach(|py| -> anyhow::Result<Self> {
41            #[cfg(feature = "tracing")]
42            tracing::debug!(?ty, ?init, "Table::new");
43
44            let desc = create_js_object(py)?;
45            desc.setattr(intern!(py, "element"), ty.element().as_js_descriptor())?;
46            desc.setattr(intern!(py, "initial"), ty.minimum())?;
47            if let Some(max) = ty.maximum() {
48                desc.setattr(intern!(py, "maximum"), max)?;
49            }
50
51            let init = init.to_py(py)?;
52
53            let table = web_assembly_table_new(py)?.call1((desc, init))?;
54
55            Ok(Self {
56                table: table.unbind(),
57                ty,
58            })
59        })
60    }
61
62    /// Returns the type and limits of the table.
63    fn ty(&self, _ctx: impl AsContext<Engine>) -> TableType {
64        self.ty
65    }
66
67    /// Returns the current size of the table.
68    fn size(&self, _ctx: impl AsContext<Engine>) -> u32 {
69        Python::attach(|py| -> Result<u32, PyErr> {
70            let table = self.table.bind(py);
71
72            #[cfg(feature = "tracing")]
73            tracing::debug!(table = %table, ?self.ty, "Table::size");
74
75            table.getattr(intern!(py, "length"))?.extract()
76        })
77        .expect("Table::size should not fail")
78    }
79
80    /// Grows the table by the given amount of elements.
81    fn grow(
82        &self,
83        _ctx: impl AsContextMut<Engine>,
84        delta: u32,
85        init: Ref<Engine>,
86    ) -> anyhow::Result<u32> {
87        Python::attach(|py| {
88            let table = self.table.bind(py);
89
90            #[cfg(feature = "tracing")]
91            tracing::debug!(table = %table, ?self.ty, delta, ?init, "Table::grow");
92
93            let init = init.to_py(py)?;
94
95            let old_len = table
96                .call_method1(intern!(py, "grow"), (delta, init))?
97                .extract()?;
98
99            Ok(old_len)
100        })
101    }
102
103    /// Returns the table element at `index`.
104    fn get(&self, _ctx: impl AsContextMut<Engine>, index: u32) -> Option<Ref<Engine>> {
105        Python::attach(|py| {
106            let table = self.table.bind(py);
107
108            #[cfg(feature = "tracing")]
109            tracing::debug!(table = %table, ?self.ty, index, "Table::get");
110
111            let value = table.call_method1(intern!(py, "get"), (index,)).ok()?;
112
113            Some(Ref::from_py_typed(value, self.ty.element()).expect("Table::get should not fail"))
114        })
115    }
116
117    /// Sets the element of this table at `index`.
118    fn set(
119        &self,
120        _ctx: impl AsContextMut<Engine>,
121        index: u32,
122        elem: Ref<Engine>,
123    ) -> anyhow::Result<()> {
124        Python::attach(|py| {
125            let table = self.table.bind(py);
126
127            #[cfg(feature = "tracing")]
128            tracing::debug!(table = %table, ?self.ty, index, ?elem, "Table::set");
129
130            let elem = elem.to_py(py)?;
131
132            table.call_method1(intern!(py, "set"), (index, elem))?;
133
134            Ok(())
135        })
136    }
137}
138
139impl ToPy for Table {
140    fn to_py(&self, py: Python) -> Result<Py<PyAny>, PyErr> {
141        #[cfg(feature = "tracing")]
142        tracing::trace!(table = %self.table, ?self.ty, "Table::to_py");
143
144        Ok(self.table.clone_ref(py))
145    }
146}
147
148impl Table {
149    /// Creates a new table from a Python value
150    pub(crate) fn from_exported_table(table: Bound<PyAny>, ty: TableType) -> anyhow::Result<Self> {
151        if !instanceof(&table, web_assembly_table(table.py())?)? {
152            anyhow::bail!("expected WebAssembly.Table but found {table}");
153        }
154
155        #[cfg(feature = "tracing")]
156        tracing::debug!(table = %table, ?ty, "Table::from_exported_table");
157
158        let table_length: u32 = table.getattr(intern!(table.py(), "length"))?.extract()?;
159
160        assert!(table_length >= ty.minimum());
161        assert_eq!(ty.element(), RefType::FuncRef);
162
163        Ok(Self {
164            table: table.unbind(),
165            ty,
166        })
167    }
168}
169
170fn web_assembly_table(py: Python<'_>) -> Result<&Bound<'_, PyAny>, PyErr> {
171    static WEB_ASSEMBLY_TABLE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
172    WEB_ASSEMBLY_TABLE.import(py, "js.WebAssembly", "Table")
173}
174
175fn web_assembly_table_new(py: Python<'_>) -> Result<&Bound<'_, PyAny>, PyErr> {
176    static WEB_ASSEMBLY_TABLE_NEW: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
177    WEB_ASSEMBLY_TABLE_NEW.import(py, "js.WebAssembly.Table", "new")
178}