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 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
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from ._selftest import main
4
+
5
+ sys.exit(main())
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))