Metadata-Version: 2.4
Name: wasmhost
Version: 0.0.1a1
Summary: WebAssembly for CPython, PyPy and Pythonista, run in JavaScriptCore or Node, with the JavaScript WebAssembly API
Author-email: o-murphy <thehelixpg@gmail.com>
License-Expression: MIT
Keywords: webassembly,wasm,javascriptcore,jscontext,pythonista,node
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Operating System :: iOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: wasmtime
Requires-Dist: wasmtime>=49.0.0; extra == "wasmtime"
Dynamic: license-file

# wasmhost

WebAssembly from **CPython, PyPy and Pythonista**, with the JavaScript WebAssembly API. It runs in
whichever JavaScript engine is available: JavaScriptCore's `JSContext` in Pythonista on iOS, WebKitGTK's
JavaScriptCore on Linux, or Node. Plain Python, no dependencies, no C extension.

```python
import wasmhost

module = wasmhost.Module(open("lib.wasm", "rb").read())  # WebAssembly.Module
instance = wasmhost.Instance(module)  # WebAssembly.Instance
print(instance.exports.add(2, 3))  # i32/i64 -> int, f32/f64 -> float
instance.exports.memory.write(ptr, b"data")  # WebAssembly.Memory
print(instance.exports.counter.value)  # WebAssembly.Global
```

See `examples/basic.py`.

- **Types.** The JavaScript API can't tell a function's signature, and it matters (an `i64` argument must reach
  JavaScript as a BigInt), so the binary's type, import, function, global and export sections are read in
  Python. `Module.exports(module)` and `Module.imports(module)` describe a module with them.
- **Errors** are the API's: `CompileError`, `LinkError` and `Trap` (`WebAssembly.RuntimeError`, which is also a
  `RuntimeError`); an out-of-bounds memory access is an `IndexError`.
- **Memory** is copied, not shared: `memory.read(offset, n)`, `memory.write(offset, data)`, `memory[a:b]`,
  `memory.grow(pages)`.

## Batches

On a JavaScript engine every call into it has a fixed cost (a pipe to `node`, a bridged Objective-C call in
Pythonista). A batch does several steps in one trip (on `wasmtime` and `wasm3`, in Python), and a step can use the results of the earlier ones:

```python
batch = instance.batch()
ptr = batch.call(instance.exports.alloc, len(data))  # a Ref
batch.write(instance.exports.memory, ptr, data)
status = batch.call(instance.exports.run, ptr)
batch.stop_if_nonzero(status)  # leave the rest out on an error status
out = batch.read(instance.exports.memory, ptr, 16)
batch.run()
out.value  # bytes (`.done` says whether the step ran)
```

A failing step (a trap, an out-of-bounds access) raises from `run()`, after the earlier steps' results are set.
Only `i32` results can be used in arithmetic (`ptr * 8`, `ptr + 4`).

## Backends

| Backend | Where | How it is detected |
|---|---|---|
| `jscontext` | iOS (Pythonista, PythonIDE) | JavaScriptCore's `JSContext` through `objc_util` (both apps have it), or through [`rubicon-objc`](https://github.com/beeware/rubicon-objc) where that is missing (checked only against a fake bridge, not on a device) |
| `wasmtime` | anywhere with the `wasmtime` package | `import wasmtime` (`pip install wasmtime`) |
| `wasm3` | CPython 3.11+ with [pywasm3](https://github.com/wasm3/pywasm3) | `import wasm3`; install it from git: `uv add "pywasm3 @ git+https://github.com/wasm3/pywasm3"` (its PyPI release predates the API used here) |
| `gi-jsc` | Linux | WebKitGTK's JavaScriptCore through PyGObject (`apt install gir1.2-javascriptcoregtk-4.1 python3-gi`) |
| `node` | anywhere with Node.js | `node` on `PATH` |

With nothing configured, the first backend that starts wins, in the order shown. Each backend's constructor is its
own probe: it fails when its runtime is missing. Choose one with `WASMHOST_BACKEND=<name>`,
`wasmhost.set_backend("<name>")` or `Module(..., backend="<name>")`; `wasmhost.get_backend().name` says which is in
use. `wasmhost.close()` closes the backends it started. (In WebAssembly's words the *host* is the embedder, the
Python side that provides imports; what runs the module is the backend.)

Not every backend can do everything (`backend.supports("memory.grow")` and `supports("table.length")` say):
`wasm3` can't `Memory.grow` from Python (`NotImplementedError`; a module's own `memory.grow` works) and has no
tables API.

## Try it on a device

The package carries a self-test, since nothing else can be run in Pythonista to see whether this works there:

```python
import wasmhost
wasmhost.selftest()                 # or, from a shell: python -m wasmhost [--backend NAME] [--all]
```

It prints one line per check (the Objective-C bridge in use, `WebAssembly` and `BigInt` in the engine, calls,
`i64`, memory, globals, traps, batches, the cost of a call), then `N/M passed`. If something fails, send the whole
output. On a computer, `python -m wasmhost --all` runs it on every backend that starts.

### Where it has been run

| Where | Backend | Result | A call / a batch of 3 |
|---|---|---|---|
| Pythonista 3 (StaSh 0.7.5), Python 3.10.4, iPhone17,3 | `jscontext` (`objc_util`) | 23/23 | 53 / 97 us |
| PythonIDE, Python 3.14.7, `ios-13.0-arm64-iphoneos` | `jscontext` (`objc_util`) | 23/23 | 37 / 76 us |
| Linux, CPython 3.14t | `gi-jsc` | 23/23 | 34 / 78 us |
| Linux, CPython 3.14t | `node` | 23/23 | 82 / 116 us |
| Linux, CPython 3.14t | `wasmtime` | 18/18 | 69 / 233 us |
| Linux, CPython 3.14t | `wasm3` | 18/18 | 4 / 46 us |
| Linux, CPython 3.10 and PyPy 3.10 | `node` | 23/23 (and the test suite on 3.10) | |

The times are one run of the self-test each, so read them as an order of magnitude. Not run on a device: the
`rubicon-objc` bridge (both iOS apps above have `objc_util`, so it wasn't needed), and imports (see below).

## Not yet

- **Imports.** A module that imports host functions, memories, tables or globals can't be instantiated
  (`NotImplementedError`). Python callbacks need a synchronous bridge: native for `wasmtime` and `wasm3`, a JavaScript function
  made from Python for `gi-jsc`, an Objective-C block for `jscontext`, and for `node` a blocking read of the pipe.
- **Tables** beyond their length, `v128` and reference types, multi-value results in a batch.

## Test

```bash
uv run pytest                            # every backend that starts here
uv run pytest --wasm-backend node        # one backend: it must start, or the run stops with an error
uv run pytest --wasm-backend wasmtime    # or wasm3
uv run pytest --wasm-backend gi-jsc      # needs PyGObject: run it with a system-site-packages venv (see the CI job)
uv run pyright && uv run ruff check
```
