wasmhost 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wasmhost/__init__.py +70 -0
- wasmhost/__main__.py +5 -0
- wasmhost/_api.py +462 -0
- wasmhost/_backend.py +163 -0
- wasmhost/_binary.py +169 -0
- wasmhost/_errors.py +21 -0
- wasmhost/_js.py +500 -0
- wasmhost/_native.py +195 -0
- wasmhost/_registry.py +60 -0
- wasmhost/_selftest.py +303 -0
- wasmhost/_version.py +24 -0
- wasmhost/py.typed +0 -0
- wasmhost-0.0.1.dist-info/METADATA +213 -0
- wasmhost-0.0.1.dist-info/RECORD +17 -0
- wasmhost-0.0.1.dist-info/WHEEL +5 -0
- wasmhost-0.0.1.dist-info/licenses/LICENSE +21 -0
- wasmhost-0.0.1.dist-info/top_level.txt +1 -0
wasmhost/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""wasmhost: run WebAssembly from CPython, PyPy and Pythonista, with the JavaScript WebAssembly API.
|
|
2
|
+
|
|
3
|
+
`Module`, `Instance`, `Memory` and `Global` run on a backend: JavaScriptCore's `JSContext` (Pythonista on iOS),
|
|
4
|
+
WebKitGTK's JavaScriptCore (Linux), Node, or a native runtime (wasmtime, wasm3). Plain Python, no dependencies,
|
|
5
|
+
no C extension of its own.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._api import (
|
|
9
|
+
Batch,
|
|
10
|
+
CompileError,
|
|
11
|
+
Function,
|
|
12
|
+
Global,
|
|
13
|
+
Instance,
|
|
14
|
+
Instantiated,
|
|
15
|
+
LinkError,
|
|
16
|
+
Memory,
|
|
17
|
+
Module,
|
|
18
|
+
Ref,
|
|
19
|
+
Table,
|
|
20
|
+
Trap,
|
|
21
|
+
WasmError,
|
|
22
|
+
close,
|
|
23
|
+
get_backend,
|
|
24
|
+
instantiate,
|
|
25
|
+
set_backend,
|
|
26
|
+
validate,
|
|
27
|
+
)
|
|
28
|
+
from ._backend import Backend
|
|
29
|
+
from ._binary import ExportDescriptor, FuncType, ImportDescriptor
|
|
30
|
+
from ._js import GIJavaScriptCoreBackend, JSBackend, JSContextBackend, NodeBackend
|
|
31
|
+
from ._native import Wasm3Backend, WasmtimeBackend
|
|
32
|
+
from ._registry import AUTO_ORDER, BACKENDS, JS_AUTO_ORDER, JS_BACKENDS, default_backend
|
|
33
|
+
from ._selftest import selftest
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
"AUTO_ORDER",
|
|
37
|
+
"BACKENDS",
|
|
38
|
+
"JS_AUTO_ORDER",
|
|
39
|
+
"JS_BACKENDS",
|
|
40
|
+
"Backend",
|
|
41
|
+
"Batch",
|
|
42
|
+
"CompileError",
|
|
43
|
+
"ExportDescriptor",
|
|
44
|
+
"FuncType",
|
|
45
|
+
"Function",
|
|
46
|
+
"GIJavaScriptCoreBackend",
|
|
47
|
+
"Global",
|
|
48
|
+
"ImportDescriptor",
|
|
49
|
+
"Instance",
|
|
50
|
+
"Instantiated",
|
|
51
|
+
"JSBackend",
|
|
52
|
+
"JSContextBackend",
|
|
53
|
+
"LinkError",
|
|
54
|
+
"Memory",
|
|
55
|
+
"Module",
|
|
56
|
+
"NodeBackend",
|
|
57
|
+
"Ref",
|
|
58
|
+
"Table",
|
|
59
|
+
"Trap",
|
|
60
|
+
"Wasm3Backend",
|
|
61
|
+
"WasmError",
|
|
62
|
+
"WasmtimeBackend",
|
|
63
|
+
"close",
|
|
64
|
+
"default_backend",
|
|
65
|
+
"get_backend",
|
|
66
|
+
"instantiate",
|
|
67
|
+
"selftest",
|
|
68
|
+
"set_backend",
|
|
69
|
+
"validate",
|
|
70
|
+
)
|
wasmhost/__main__.py
ADDED
wasmhost/_api.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
# The classes of this module (Module, Instance, Function, Memory, Global) share their handles on purpose.
|
|
3
|
+
"""The public API, shaped like the JavaScript WebAssembly API (Module, Instance, Memory, Global).
|
|
4
|
+
|
|
5
|
+
mod = Module(wasm_bytes) # WebAssembly.Module
|
|
6
|
+
inst = Instance(mod) # WebAssembly.Instance
|
|
7
|
+
inst.exports.add(2, 3) # exported functions are callables; i64 is an int, f32/f64 a float
|
|
8
|
+
inst.exports.memory.write(ptr, data) # WebAssembly.Memory: read / write / slicing / grow
|
|
9
|
+
inst.exports.counter.value # WebAssembly.Global
|
|
10
|
+
|
|
11
|
+
It runs on a backend (see `_backend.py`): a JavaScript engine's own `WebAssembly` object, or a native runtime
|
|
12
|
+
(wasmtime, wasm3). The backend is whichever starts first, or the one you pick with `backend=`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Iterator, Mapping
|
|
18
|
+
from typing import Any, NamedTuple
|
|
19
|
+
|
|
20
|
+
from ._backend import Backend, CallStep, Expr, Operand, ReadStep, Step, StopStep, WriteStep, normalize
|
|
21
|
+
from ._binary import ExportDescriptor, FuncType, ImportDescriptor, ModuleInfo, parse
|
|
22
|
+
from ._errors import CompileError, LinkError, Trap, WasmError
|
|
23
|
+
from ._registry import BACKENDS, default_backend
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
"Batch",
|
|
27
|
+
"CompileError",
|
|
28
|
+
"Function",
|
|
29
|
+
"Global",
|
|
30
|
+
"Instance",
|
|
31
|
+
"Instantiated",
|
|
32
|
+
"LinkError",
|
|
33
|
+
"Memory",
|
|
34
|
+
"Module",
|
|
35
|
+
"Ref",
|
|
36
|
+
"Table",
|
|
37
|
+
"Trap",
|
|
38
|
+
"WasmError",
|
|
39
|
+
"close",
|
|
40
|
+
"get_backend",
|
|
41
|
+
"instantiate",
|
|
42
|
+
"set_backend",
|
|
43
|
+
"validate",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
PAGE_SIZE = 65536
|
|
47
|
+
|
|
48
|
+
_default: Backend | None = None
|
|
49
|
+
_started: dict[
|
|
50
|
+
str, Backend
|
|
51
|
+
] = {} # backends started by name, shared, so `backend="node"` doesn't start a process each time
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _backend(backend: Backend | str | None) -> Backend:
|
|
55
|
+
global _default
|
|
56
|
+
if backend is None:
|
|
57
|
+
if _default is None:
|
|
58
|
+
_default = default_backend()
|
|
59
|
+
return _default
|
|
60
|
+
if isinstance(backend, str):
|
|
61
|
+
if _default is not None and _default.name == backend:
|
|
62
|
+
return _default
|
|
63
|
+
if backend not in BACKENDS:
|
|
64
|
+
raise ValueError(f"unknown backend {backend!r}: expected one of {', '.join(BACKENDS)}")
|
|
65
|
+
if backend not in _started:
|
|
66
|
+
_started[backend] = BACKENDS[backend]()
|
|
67
|
+
return _started[backend]
|
|
68
|
+
return backend
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_backend(backend: Backend | str | None = None) -> Backend:
|
|
72
|
+
"""The default backend (started on first use); or, given a name, the backend of that name (started once and
|
|
73
|
+
shared, like `backend="node"` in `Module`)."""
|
|
74
|
+
return _backend(backend)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def set_backend(backend: Backend | str | None) -> None:
|
|
78
|
+
"""Choose the default backend by name (`"node"`, `"gi-jsc"`, `"jscontext"`, `"wasmtime"`, `"wasm3"`) or instance;
|
|
79
|
+
None forgets it, and the next use picks again."""
|
|
80
|
+
global _default
|
|
81
|
+
_default = None if backend is None else _backend(backend)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def close() -> None:
|
|
85
|
+
"""Close every backend this module started. Modules and instances made on them stop working."""
|
|
86
|
+
global _default
|
|
87
|
+
for b in {id(b): b for b in [*_started.values(), *([_default] if _default else [])]}.values():
|
|
88
|
+
b.close()
|
|
89
|
+
_started.clear()
|
|
90
|
+
_default = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _check(value: object, kind: str) -> int | float:
|
|
94
|
+
"""The argument for a parameter of this type, as WebAssembly takes it (integers wrap to their width)."""
|
|
95
|
+
if kind in ("i32", "i64"):
|
|
96
|
+
if not isinstance(value, int):
|
|
97
|
+
raise TypeError(f"expected an int for an {kind} argument, got {type(value).__name__}")
|
|
98
|
+
return normalize(value, kind)
|
|
99
|
+
if kind in ("f32", "f64"):
|
|
100
|
+
if not isinstance(value, (int, float)):
|
|
101
|
+
raise TypeError(f"expected a number for an {kind} argument, got {type(value).__name__}")
|
|
102
|
+
return normalize(value, kind)
|
|
103
|
+
raise NotImplementedError(f"{kind} values are not supported")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Function:
|
|
107
|
+
"""An exported function. Call it; `params` and `results` are its value types (`"i32"`, `"f64"`, ...)."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, instance: Instance, name: str, functype: FuncType) -> None:
|
|
110
|
+
self._instance = instance
|
|
111
|
+
self.name = name
|
|
112
|
+
self._type = functype
|
|
113
|
+
self.params = functype.params
|
|
114
|
+
self.results = functype.results
|
|
115
|
+
|
|
116
|
+
def __call__(self, *args: object) -> Any:
|
|
117
|
+
if len(args) != len(self.params):
|
|
118
|
+
raise TypeError(f"{self.name}() takes {len(self.params)} arguments ({len(args)} given)")
|
|
119
|
+
checked = [_check(a, k) for a, k in zip(args, self.params, strict=True)]
|
|
120
|
+
values = self._instance._backend.call(self._instance._handle, self.name, checked, self._type)
|
|
121
|
+
if not self.results:
|
|
122
|
+
return None
|
|
123
|
+
return values[0] if len(values) == 1 else tuple(values)
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"<wasmhost.Function {self.name}({', '.join(self.params)}) -> ({', '.join(self.results)})>"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Memory:
|
|
130
|
+
"""An exported linear memory. Bytes are copied in and out (nothing is shared with the runtime)."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, instance: Instance, name: str) -> None:
|
|
133
|
+
self._instance = instance
|
|
134
|
+
self.name = name
|
|
135
|
+
|
|
136
|
+
def __len__(self) -> int:
|
|
137
|
+
return self._instance._backend.memory_size(self._instance._handle, self.name)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def byte_length(self) -> int:
|
|
141
|
+
return len(self)
|
|
142
|
+
|
|
143
|
+
def grow(self, pages: int) -> int:
|
|
144
|
+
"""`WebAssembly.Memory.grow`: add `pages` 64 KiB pages and return the previous size in pages.
|
|
145
|
+
|
|
146
|
+
Not on every backend (wasm3 has no such call from Python): `NotImplementedError` there."""
|
|
147
|
+
return self._instance._backend.memory_grow(self._instance._handle, self.name, int(pages))
|
|
148
|
+
|
|
149
|
+
def read(self, offset: int, length: int) -> bytes:
|
|
150
|
+
if offset < 0 or length < 0 or offset + length > len(self):
|
|
151
|
+
raise IndexError("memory access out of bounds")
|
|
152
|
+
return (
|
|
153
|
+
self._instance._backend.memory_read(self._instance._handle, self.name, int(offset), int(length))
|
|
154
|
+
if length
|
|
155
|
+
else b""
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def write(self, offset: int, data: bytes | bytearray | memoryview) -> None:
|
|
159
|
+
self._instance._backend.memory_write(self._instance._handle, self.name, int(offset), bytes(data))
|
|
160
|
+
|
|
161
|
+
def __getitem__(self, key: int | slice) -> bytes | int:
|
|
162
|
+
if isinstance(key, slice):
|
|
163
|
+
start, stop, step = key.indices(len(self))
|
|
164
|
+
if step != 1:
|
|
165
|
+
raise ValueError("memory slices have no step")
|
|
166
|
+
return self.read(start, max(0, stop - start))
|
|
167
|
+
n = len(self)
|
|
168
|
+
i = key + n if key < 0 else key
|
|
169
|
+
if not 0 <= i < n:
|
|
170
|
+
raise IndexError("memory index out of range")
|
|
171
|
+
return self.read(i, 1)[0]
|
|
172
|
+
|
|
173
|
+
def __setitem__(self, key: int | slice, value: bytes | bytearray | memoryview | int) -> None:
|
|
174
|
+
if isinstance(key, slice):
|
|
175
|
+
start, stop, step = key.indices(len(self))
|
|
176
|
+
if step != 1:
|
|
177
|
+
raise ValueError("memory slices have no step")
|
|
178
|
+
if not isinstance(value, (bytes, bytearray, memoryview)):
|
|
179
|
+
raise TypeError("a slice takes bytes")
|
|
180
|
+
if len(value) != max(0, stop - start):
|
|
181
|
+
raise ValueError("a memory slice can't change size")
|
|
182
|
+
self.write(start, value)
|
|
183
|
+
else:
|
|
184
|
+
if not isinstance(value, int):
|
|
185
|
+
raise TypeError("an index takes an int")
|
|
186
|
+
n = len(self)
|
|
187
|
+
i = key + n if key < 0 else key
|
|
188
|
+
if not 0 <= i < n:
|
|
189
|
+
raise IndexError("memory index out of range")
|
|
190
|
+
self.write(i, bytes([value & 0xFF]))
|
|
191
|
+
|
|
192
|
+
def __repr__(self) -> str:
|
|
193
|
+
return f"<wasmhost.Memory {self.name} {len(self) // PAGE_SIZE} pages>"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Global:
|
|
197
|
+
"""An exported global; `value` reads it and, for a mutable one, writes it."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, instance: Instance, name: str, kind: str) -> None:
|
|
200
|
+
self._instance = instance
|
|
201
|
+
self.name = name
|
|
202
|
+
self.type = kind
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def value(self) -> int | float:
|
|
206
|
+
return self._instance._backend.global_get(self._instance._handle, self.name, self.type)
|
|
207
|
+
|
|
208
|
+
@value.setter
|
|
209
|
+
def value(self, new: int | float) -> None:
|
|
210
|
+
self._instance._backend.global_set(self._instance._handle, self.name, self.type, _check(new, self.type))
|
|
211
|
+
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
return f"<wasmhost.Global {self.name}: {self.type}>"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class Ref:
|
|
217
|
+
"""A value produced inside a batch, usable as an argument or offset of a later step (`ref * 8`, `ref + 4`)
|
|
218
|
+
and readable as `.value` once the batch has run (`.done` says whether its step ran)."""
|
|
219
|
+
|
|
220
|
+
def __init__(self, batch: Batch, index: int, kind: str) -> None:
|
|
221
|
+
self._batch = batch
|
|
222
|
+
self._index = index
|
|
223
|
+
self.kind = kind # "i32" | "i64" | "f32" | "f64" | "bytes" | "void" (a call without a result)
|
|
224
|
+
self._expr: Expr = ("ref", index)
|
|
225
|
+
self._value: Any = None
|
|
226
|
+
self.done = False
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def value(self) -> Any:
|
|
230
|
+
if not self.done:
|
|
231
|
+
raise RuntimeError("this step has not run (the batch hasn't run, stopped before it, or failed)")
|
|
232
|
+
return self._value
|
|
233
|
+
|
|
234
|
+
def _arith(self, op: str, other: object, reflected: bool = False) -> Ref:
|
|
235
|
+
if self.kind != "i32" or isinstance(other, bool) or not isinstance(other, int):
|
|
236
|
+
raise TypeError("only an i32 result can be used in arithmetic, and only with an int")
|
|
237
|
+
derived = Ref(self._batch, self._index, "i32")
|
|
238
|
+
derived._expr = (op, other, self._expr) if reflected else (op, self._expr, other)
|
|
239
|
+
return derived
|
|
240
|
+
|
|
241
|
+
def __add__(self, other: int) -> Ref:
|
|
242
|
+
return self._arith("+", other)
|
|
243
|
+
|
|
244
|
+
def __radd__(self, other: int) -> Ref:
|
|
245
|
+
return self._arith("+", other, True)
|
|
246
|
+
|
|
247
|
+
def __sub__(self, other: int) -> Ref:
|
|
248
|
+
return self._arith("-", other)
|
|
249
|
+
|
|
250
|
+
def __rsub__(self, other: int) -> Ref:
|
|
251
|
+
return self._arith("-", other, True)
|
|
252
|
+
|
|
253
|
+
def __mul__(self, other: int) -> Ref:
|
|
254
|
+
return self._arith("*", other)
|
|
255
|
+
|
|
256
|
+
def __rmul__(self, other: int) -> Ref:
|
|
257
|
+
return self._arith("*", other, True)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Batch:
|
|
261
|
+
"""Several steps done in one go (on a JavaScript engine, in one trip: each trip has a fixed cost).
|
|
262
|
+
|
|
263
|
+
b = inst.batch()
|
|
264
|
+
ptr = b.call(inst.exports.alloc, len(data)) # a Ref: later steps can use it
|
|
265
|
+
b.write(inst.exports.memory, ptr, data)
|
|
266
|
+
b.stop_if_nonzero(b.call(inst.exports.run, ptr)) # leave the rest out on a non-zero status
|
|
267
|
+
out = b.read(inst.exports.memory, ptr, 16)
|
|
268
|
+
b.run()
|
|
269
|
+
out.value # bytes
|
|
270
|
+
|
|
271
|
+
A step that fails (a trap, an out-of-bounds access) raises from `run()`, after the earlier steps' `Ref`s
|
|
272
|
+
have their values.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, instance: Instance) -> None:
|
|
276
|
+
self._instance = instance
|
|
277
|
+
self._steps: list[Step] = []
|
|
278
|
+
self._refs: list[Ref] = []
|
|
279
|
+
self._ran = False
|
|
280
|
+
|
|
281
|
+
def _operand(self, value: object, kind: str) -> Operand:
|
|
282
|
+
if isinstance(value, Ref):
|
|
283
|
+
if value.kind != kind:
|
|
284
|
+
raise TypeError(f"a {value.kind} result can't be used as an {kind}")
|
|
285
|
+
if value._batch is not self:
|
|
286
|
+
raise ValueError("a Ref from another batch")
|
|
287
|
+
return value._expr
|
|
288
|
+
return _check(value, kind)
|
|
289
|
+
|
|
290
|
+
def _new(self, kind: str) -> Ref:
|
|
291
|
+
ref = Ref(self, len(self._refs), kind)
|
|
292
|
+
self._refs.append(ref)
|
|
293
|
+
return ref
|
|
294
|
+
|
|
295
|
+
def call(self, function: Function, *args: object) -> Ref:
|
|
296
|
+
"""Call an exported function. The result is a `Ref`; for a function without one its value is None."""
|
|
297
|
+
if function._instance is not self._instance:
|
|
298
|
+
raise ValueError("a function of another instance")
|
|
299
|
+
if len(args) != len(function.params):
|
|
300
|
+
raise TypeError(f"{function.name}() takes {len(function.params)} arguments ({len(args)} given)")
|
|
301
|
+
if len(function.results) > 1:
|
|
302
|
+
raise NotImplementedError("multi-value results in a batch")
|
|
303
|
+
operands = tuple(self._operand(a, k) for a, k in zip(args, function.params, strict=True))
|
|
304
|
+
ref = self._new(function.results[0] if function.results else "void")
|
|
305
|
+
self._steps.append(CallStep(ref._index, function.name, operands, function._type))
|
|
306
|
+
return ref
|
|
307
|
+
|
|
308
|
+
def write(self, memory: Memory, offset: int | Ref, data: bytes | bytearray | memoryview) -> None:
|
|
309
|
+
self._steps.append(WriteStep(memory.name, self._operand(offset, "i32"), bytes(data)))
|
|
310
|
+
|
|
311
|
+
def read(self, memory: Memory, offset: int | Ref, length: int | Ref) -> Ref:
|
|
312
|
+
"""Bytes out of the memory; the `Ref` holds them (as `bytes`) after the batch has run."""
|
|
313
|
+
ref = self._new("bytes")
|
|
314
|
+
self._steps.append(
|
|
315
|
+
ReadStep(ref._index, memory.name, self._operand(offset, "i32"), self._operand(length, "i32"))
|
|
316
|
+
)
|
|
317
|
+
return ref
|
|
318
|
+
|
|
319
|
+
def stop_if_zero(self, ref: Ref) -> None:
|
|
320
|
+
"""Leave out the remaining steps when this result is 0 (an allocation that failed, say)."""
|
|
321
|
+
self._steps.append(StopStep(self._flag(ref), True))
|
|
322
|
+
|
|
323
|
+
def stop_if_nonzero(self, ref: Ref) -> None:
|
|
324
|
+
"""Leave out the remaining steps when this result isn't 0 (an error status, say)."""
|
|
325
|
+
self._steps.append(StopStep(self._flag(ref), False))
|
|
326
|
+
|
|
327
|
+
def _flag(self, ref: Ref) -> Operand:
|
|
328
|
+
if ref.kind not in ("i32", "f32", "f64"):
|
|
329
|
+
raise TypeError("expected the number result of a call")
|
|
330
|
+
return ref._expr
|
|
331
|
+
|
|
332
|
+
def run(self) -> None:
|
|
333
|
+
"""Do the steps. Raises the error of a failed step; `Ref.done` tells which steps ran."""
|
|
334
|
+
if self._ran:
|
|
335
|
+
raise RuntimeError("this batch has already run")
|
|
336
|
+
self._ran = True
|
|
337
|
+
result = self._instance._backend.run_batch(self._instance._handle, self._steps)
|
|
338
|
+
for ref in self._refs:
|
|
339
|
+
if ref._index in result.values:
|
|
340
|
+
ref._value = result.values[ref._index]
|
|
341
|
+
ref.done = True
|
|
342
|
+
if result.error is not None:
|
|
343
|
+
raise result.error
|
|
344
|
+
|
|
345
|
+
def __enter__(self) -> Batch:
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
349
|
+
if exc_type is None:
|
|
350
|
+
self.run()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Table:
|
|
354
|
+
"""An exported table: only its length is available so far."""
|
|
355
|
+
|
|
356
|
+
def __init__(self, instance: Instance, name: str) -> None:
|
|
357
|
+
self._instance = instance
|
|
358
|
+
self.name = name
|
|
359
|
+
|
|
360
|
+
def __len__(self) -> int:
|
|
361
|
+
return self._instance._backend.table_length(self._instance._handle, self.name)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
Export = Function | Memory | Global | Table
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class _Exports:
|
|
368
|
+
"""`instance.exports`: attribute and item access, and iteration over the names (like the JS object)."""
|
|
369
|
+
|
|
370
|
+
def __init__(self, items: dict[str, Export]) -> None:
|
|
371
|
+
self._items = items
|
|
372
|
+
|
|
373
|
+
def __getattr__(self, name: str) -> Any: # like the JS object: any export, typed by what it is
|
|
374
|
+
try:
|
|
375
|
+
return self._items[name]
|
|
376
|
+
except KeyError:
|
|
377
|
+
raise AttributeError(name) from None
|
|
378
|
+
|
|
379
|
+
def __getitem__(self, name: str) -> Export:
|
|
380
|
+
return self._items[name]
|
|
381
|
+
|
|
382
|
+
def __contains__(self, name: object) -> bool:
|
|
383
|
+
return name in self._items
|
|
384
|
+
|
|
385
|
+
def __iter__(self) -> Iterator[str]:
|
|
386
|
+
return iter(self._items)
|
|
387
|
+
|
|
388
|
+
def __len__(self) -> int:
|
|
389
|
+
return len(self._items)
|
|
390
|
+
|
|
391
|
+
def __repr__(self) -> str:
|
|
392
|
+
return f"<wasmhost exports {', '.join(self._items)}>"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class Module:
|
|
396
|
+
"""A compiled module. `Module.exports(m)` and `Module.imports(m)` describe it, with types."""
|
|
397
|
+
|
|
398
|
+
def __init__(self, wasm: bytes | bytearray | memoryview, *, backend: Backend | str | None = None) -> None:
|
|
399
|
+
data = bytes(wasm)
|
|
400
|
+
try:
|
|
401
|
+
self._info: ModuleInfo = parse(data)
|
|
402
|
+
except ValueError as exc:
|
|
403
|
+
raise CompileError(str(exc)) from None
|
|
404
|
+
self._backend = _backend(backend)
|
|
405
|
+
self._handle = self._backend.compile(data)
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def exports(module: Module) -> list[ExportDescriptor]:
|
|
409
|
+
return list(module._info.exports)
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def imports(module: Module) -> list[ImportDescriptor]:
|
|
413
|
+
return list(module._info.imports)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class Instance:
|
|
417
|
+
"""`WebAssembly.Instance`. Modules that import things can't be instantiated yet."""
|
|
418
|
+
|
|
419
|
+
def __init__(self, module: Module, imports: Mapping[str, Mapping[str, object]] | None = None) -> None:
|
|
420
|
+
if module._info.imports:
|
|
421
|
+
if imports:
|
|
422
|
+
raise NotImplementedError("imports (host functions, memories, tables, globals) are not supported yet")
|
|
423
|
+
wanted = ", ".join(f"{i.module}.{i.name}" for i in module._info.imports)
|
|
424
|
+
raise TypeError(f"the module imports {wanted}: an import object is needed")
|
|
425
|
+
self._backend = module._backend
|
|
426
|
+
self._handle = self._backend.instantiate(module._handle)
|
|
427
|
+
items: dict[str, Export] = {}
|
|
428
|
+
for e in module._info.exports:
|
|
429
|
+
if e.kind == "function" and isinstance(e.type, FuncType):
|
|
430
|
+
items[e.name] = Function(self, e.name, e.type)
|
|
431
|
+
elif e.kind == "memory":
|
|
432
|
+
items[e.name] = Memory(self, e.name)
|
|
433
|
+
elif e.kind == "global" and isinstance(e.type, str):
|
|
434
|
+
items[e.name] = Global(self, e.name, e.type)
|
|
435
|
+
elif e.kind == "table":
|
|
436
|
+
items[e.name] = Table(self, e.name)
|
|
437
|
+
self.exports = _Exports(items)
|
|
438
|
+
|
|
439
|
+
def batch(self) -> Batch:
|
|
440
|
+
"""Steps done together (on a JavaScript engine, in one trip), the later ones using the earlier results."""
|
|
441
|
+
return Batch(self)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class Instantiated(NamedTuple):
|
|
445
|
+
module: Module
|
|
446
|
+
instance: Instance
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def instantiate(
|
|
450
|
+
wasm: bytes | bytearray | memoryview,
|
|
451
|
+
imports: Mapping[str, Mapping[str, object]] | None = None,
|
|
452
|
+
*,
|
|
453
|
+
backend: Backend | str | None = None,
|
|
454
|
+
) -> Instantiated:
|
|
455
|
+
"""`WebAssembly.instantiate(bytes, imports)`: compile and instantiate in one step."""
|
|
456
|
+
module = Module(wasm, backend=backend)
|
|
457
|
+
return Instantiated(module, Instance(module, imports))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def validate(wasm: bytes | bytearray | memoryview, *, backend: Backend | str | None = None) -> bool:
|
|
461
|
+
"""`WebAssembly.validate`: does the backend accept these bytes as a module?"""
|
|
462
|
+
return _backend(backend).validate(bytes(wasm))
|