wasmhost 0.0.1a1__tar.gz

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.
Files changed (39) hide show
  1. wasmhost-0.0.1a1/.github/dependabot.yml +24 -0
  2. wasmhost-0.0.1a1/.github/workflows/pre-commit.yml +83 -0
  3. wasmhost-0.0.1a1/.github/workflows/publish.yml +110 -0
  4. wasmhost-0.0.1a1/.github/workflows/tests.yml +105 -0
  5. wasmhost-0.0.1a1/.gitignore +17 -0
  6. wasmhost-0.0.1a1/.pre-commit-config.yaml +56 -0
  7. wasmhost-0.0.1a1/LICENSE +21 -0
  8. wasmhost-0.0.1a1/PKG-INFO +139 -0
  9. wasmhost-0.0.1a1/README.md +109 -0
  10. wasmhost-0.0.1a1/examples/basic.py +38 -0
  11. wasmhost-0.0.1a1/pyproject.toml +80 -0
  12. wasmhost-0.0.1a1/setup.cfg +4 -0
  13. wasmhost-0.0.1a1/src/wasmhost/__init__.py +70 -0
  14. wasmhost-0.0.1a1/src/wasmhost/__main__.py +5 -0
  15. wasmhost-0.0.1a1/src/wasmhost/_api.py +462 -0
  16. wasmhost-0.0.1a1/src/wasmhost/_backend.py +163 -0
  17. wasmhost-0.0.1a1/src/wasmhost/_binary.py +169 -0
  18. wasmhost-0.0.1a1/src/wasmhost/_errors.py +21 -0
  19. wasmhost-0.0.1a1/src/wasmhost/_js.py +382 -0
  20. wasmhost-0.0.1a1/src/wasmhost/_native.py +195 -0
  21. wasmhost-0.0.1a1/src/wasmhost/_registry.py +60 -0
  22. wasmhost-0.0.1a1/src/wasmhost/_selftest.py +290 -0
  23. wasmhost-0.0.1a1/src/wasmhost/_version.py +24 -0
  24. wasmhost-0.0.1a1/src/wasmhost/py.typed +0 -0
  25. wasmhost-0.0.1a1/src/wasmhost.egg-info/PKG-INFO +139 -0
  26. wasmhost-0.0.1a1/src/wasmhost.egg-info/SOURCES.txt +37 -0
  27. wasmhost-0.0.1a1/src/wasmhost.egg-info/dependency_links.txt +1 -0
  28. wasmhost-0.0.1a1/src/wasmhost.egg-info/requires.txt +3 -0
  29. wasmhost-0.0.1a1/src/wasmhost.egg-info/scm_file_list.json +33 -0
  30. wasmhost-0.0.1a1/src/wasmhost.egg-info/scm_version.json +8 -0
  31. wasmhost-0.0.1a1/src/wasmhost.egg-info/top_level.txt +1 -0
  32. wasmhost-0.0.1a1/tests/conftest.py +69 -0
  33. wasmhost-0.0.1a1/tests/fake_objc.py +98 -0
  34. wasmhost-0.0.1a1/tests/test_api.py +227 -0
  35. wasmhost-0.0.1a1/tests/test_binary.py +32 -0
  36. wasmhost-0.0.1a1/tests/test_jscontext.py +67 -0
  37. wasmhost-0.0.1a1/tests/test_selftest.py +37 -0
  38. wasmhost-0.0.1a1/tests/wasm_builder.py +112 -0
  39. wasmhost-0.0.1a1/uv.lock +403 -0
@@ -0,0 +1,24 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "uv"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ groups:
8
+ python-dependencies:
9
+ patterns:
10
+ - "*"
11
+
12
+ - package-ecosystem: "github-actions"
13
+ directory: "/"
14
+ schedule:
15
+ interval: "weekly"
16
+ groups:
17
+ actions:
18
+ patterns:
19
+ - "*"
20
+
21
+ - package-ecosystem: "pre-commit"
22
+ directory: "/"
23
+ schedule:
24
+ interval: "weekly"
@@ -0,0 +1,83 @@
1
+ name: Pre-commit
2
+
3
+ # Same flow as tiny-bclibc-wasm's: run every hook; on a pull request from this repo, commit what the
4
+ # auto-fixing hooks (ruff) changed and verify again; anywhere else, changed files are a failure.
5
+
6
+ on:
7
+ push:
8
+ pull_request:
9
+ workflow_dispatch:
10
+ workflow_call:
11
+
12
+ concurrency:
13
+ group: pre-commit-${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ permissions:
17
+ contents: write
18
+
19
+ jobs:
20
+ pre-commit:
21
+ name: pre-commit run --all-files
22
+ runs-on: ubuntu-latest
23
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+ with:
27
+ fetch-depth: 0 # setuptools_scm needs tags for the package version
28
+ ref: ${{ github.head_ref || github.ref_name }}
29
+
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
32
+ with:
33
+ python-version: "3.11"
34
+
35
+ - name: Install pre-commit
36
+ run: uv tool install pre-commit
37
+
38
+ - name: Run pre-commit
39
+ id: first_run
40
+ continue-on-error: true
41
+ run: pre-commit run --all-files --show-diff-on-failure
42
+
43
+ - name: Check for changes
44
+ id: diff
45
+ run: |
46
+ if [ -n "$(git status --porcelain)" ]; then
47
+ echo "changed=true" >> "$GITHUB_OUTPUT"
48
+ else
49
+ echo "changed=false" >> "$GITHUB_OUTPUT"
50
+ fi
51
+
52
+ - name: Fail (no auto-fixable changes, but hooks failed)
53
+ if: steps.diff.outputs.changed == 'false' && steps.first_run.outcome == 'failure'
54
+ run: exit 1
55
+
56
+ - name: Fail (changes produced, but not on a pull_request)
57
+ if: steps.diff.outputs.changed == 'true' && github.event_name != 'pull_request'
58
+ run: |
59
+ echo "pre-commit modified files, but auto-commit only runs on pull_request events."
60
+ git status --porcelain
61
+ exit 1
62
+
63
+ - name: Re-run pre-commit after auto-fixes
64
+ id: verify_run
65
+ if: steps.diff.outputs.changed == 'true' && github.event_name == 'pull_request'
66
+ continue-on-error: true
67
+ run: pre-commit run --all-files --show-diff-on-failure
68
+
69
+ - name: Fail (issues remain after auto-fix)
70
+ if: steps.diff.outputs.changed == 'true' && github.event_name == 'pull_request' && steps.verify_run.outcome == 'failure'
71
+ run: exit 1
72
+
73
+ - name: Commit and push auto-fixes
74
+ if: steps.diff.outputs.changed == 'true' && github.event_name == 'pull_request' && steps.verify_run.outcome == 'success'
75
+ env:
76
+ # through the environment, not inlined: a branch name is attacker-controlled text
77
+ BRANCH: ${{ github.head_ref || github.ref_name }}
78
+ run: |
79
+ git config user.name "github-actions[bot]"
80
+ git config user.email "github-actions[bot]@users.noreply.github.com"
81
+ git add -A
82
+ git commit -m "style: auto-fix via pre-commit"
83
+ git push origin "HEAD:$BRANCH"
@@ -0,0 +1,110 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ testpypi:
10
+ description: "Publish to TestPyPI"
11
+ required: false
12
+ type: boolean
13
+ default: false
14
+ version:
15
+ description: "Override version (e.g. 0.1.0rc1); TestPyPI needs one without a dev/local suffix"
16
+ required: false
17
+ type: string
18
+ default: ""
19
+
20
+ run-name: >
21
+ Publish → ${{ github.event_name == 'push' && 'PyPI' ||
22
+ inputs.testpypi && 'TestPyPI' || 'build only' }}
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ jobs:
28
+ build:
29
+ name: Build sdist + wheel
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ with:
34
+ fetch-depth: 0 # setuptools_scm needs tags
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
38
+
39
+ - name: Build (the wheel is built from the sdist, so this proves the sdist builds on its own)
40
+ env:
41
+ SETUPTOOLS_SCM_PRETEND_VERSION: ${{ inputs.version }}
42
+ # PyPI/TestPyPI reject local version segments (+g<hash>); keep them on build-only runs.
43
+ SETUPTOOLS_SCM_OVERRIDES_FOR_WASMHOST: >-
44
+ ${{ (github.event_name == 'push' || inputs.testpypi) && '{local_scheme = "no-local-version"}' || '' }}
45
+ run: uv build --out-dir dist
46
+
47
+ - name: Smoke-test the wheel in a clean environment
48
+ run: |
49
+ uv venv -q /tmp/smoke
50
+ uv pip install -q --python /tmp/smoke dist/*.whl
51
+ cd /tmp && WASMHOST_BACKEND=node /tmp/smoke/bin/python -c "
52
+ import wasmhost as wh
53
+ wasm = bytes.fromhex('0061736d0100000001070160027f7f017f03020100070701036164640000' '0a09010700200020016a0b')
54
+ assert wh.instantiate(wasm).instance.exports.add(2, 3) == 5
55
+ print('ok on', wh.get_backend().name)
56
+ "
57
+
58
+ - uses: actions/upload-artifact@v7
59
+ with:
60
+ name: dist
61
+ path: dist
62
+ retention-days: 14
63
+
64
+ publish:
65
+ needs: build
66
+ if: github.event_name == 'push' || inputs.testpypi
67
+ runs-on: ubuntu-latest
68
+ environment: ${{ github.event_name == 'push' && 'pypi' || 'testpypi' }}
69
+ permissions:
70
+ id-token: write
71
+ steps:
72
+ - uses: actions/download-artifact@v8
73
+ with:
74
+ name: dist
75
+ path: dist
76
+
77
+ - name: Publish to PyPI
78
+ if: github.event_name == 'push'
79
+ uses: pypa/gh-action-pypi-publish@release/v1
80
+ with:
81
+ skip-existing: true
82
+
83
+ - name: Publish to TestPyPI
84
+ if: github.event_name == 'workflow_dispatch'
85
+ uses: pypa/gh-action-pypi-publish@release/v1
86
+ with:
87
+ skip-existing: true
88
+ repository-url: https://test.pypi.org/legacy/
89
+
90
+ release:
91
+ needs: build
92
+ if: github.event_name == 'push'
93
+ runs-on: ubuntu-latest
94
+ permissions:
95
+ contents: write
96
+ steps:
97
+ - uses: actions/download-artifact@v8
98
+ with:
99
+ path: artifacts
100
+
101
+ - name: Create GitHub release (draft)
102
+ env:
103
+ GH_TOKEN: ${{ github.token }}
104
+ run: |
105
+ gh release create "${{ github.ref_name }}" \
106
+ --repo "${{ github.repository }}" \
107
+ --title "wasmhost ${{ github.ref_name }}" \
108
+ --generate-notes \
109
+ --draft \
110
+ artifacts/dist/*
@@ -0,0 +1,105 @@
1
+ name: Tests
2
+
3
+ # Builds the package from source and runs the suite once per backend, selected with pytest's --wasm-backend
4
+ # (see tests/conftest.py; a backend that can't start fails the step, it is never skipped). Coverage is not
5
+ # collected here: tiny-bclibc-wasm runs the same backends through wasmhost and combines it there.
6
+ #
7
+ # test: every OS x CPython oldest/newest (also free-threaded 3.14t) + PyPy, on wasmtime and on Node (both
8
+ # available on every runner), and on wasm3 where pywasm3 installs (CPython 3.11+; built from git).
9
+ # jsc: WebKitGTK's JavaScriptCore -- the engine behind Pythonista's JSContext -- with its JIT
10
+ # and without it (iOS apps like Pythonista run it without), as the stand-in for an iOS
11
+ # device.
12
+
13
+ on:
14
+ push:
15
+ paths-ignore: &non-code-paths
16
+ - "**.md"
17
+ - "LICENSE"
18
+ pull_request:
19
+ paths-ignore: *non-code-paths
20
+ workflow_dispatch:
21
+
22
+ concurrency:
23
+ group: tests-${{ github.workflow }}-${{ github.ref }}
24
+ cancel-in-progress: true
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ test:
31
+ name: ${{ matrix.os }} / ${{ matrix.python-version }}
32
+ runs-on: ${{ matrix.os }}
33
+ strategy:
34
+ fail-fast: false
35
+ matrix:
36
+ os: [ubuntu-latest, windows-latest, macos-latest]
37
+ python-version: ["3.10", "3.14", "3.14t", "pypy3.11"]
38
+ defaults:
39
+ run:
40
+ shell: bash
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ with:
44
+ fetch-depth: 0 # setuptools_scm needs tags for the package version
45
+
46
+ - name: Install uv
47
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
48
+ with:
49
+ python-version: ${{ matrix.python-version }}
50
+
51
+ - name: Install
52
+ run: uv sync --locked
53
+
54
+ - name: pytest --wasm-backend wasmtime
55
+ run: uv run --locked pytest -v --wasm-backend wasmtime
56
+
57
+ - name: pytest --wasm-backend node
58
+ if: ${{ !cancelled() }} # run every backend even if an earlier one failed
59
+ run: uv run --locked pytest -v --wasm-backend node
60
+
61
+ - name: pytest --wasm-backend wasm3
62
+ # pywasm3 is a dev dependency only on CPython >= 3.11 (see pyproject.toml). Warnings are errors so
63
+ # that, on 3.14t, an extension turning the GIL back on at import fails the run.
64
+ if: ${{ !cancelled() && matrix.python-version != '3.10' && !startsWith(matrix.python-version, 'pypy') }}
65
+ run: uv run --locked pytest -v -W error::RuntimeWarning --wasm-backend wasm3
66
+
67
+ - name: Example
68
+ run: WASMHOST_BACKEND=node uv run --locked python examples/basic.py
69
+
70
+ jsc:
71
+ name: WebKitGTK JavaScriptCore
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - uses: actions/checkout@v7
75
+ with:
76
+ fetch-depth: 0
77
+
78
+ - name: Install JavaScriptCore + PyGObject
79
+ run: |
80
+ sudo apt-get update -q
81
+ sudo apt-get install -y -q gir1.2-javascriptcoregtk-4.1 python3-gi
82
+
83
+ - name: Install uv
84
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
85
+
86
+ # python3-gi is built for the distribution's own Python, so use that interpreter with its
87
+ # site-packages visible.
88
+ - name: Install into a system-site-packages venv
89
+ run: |
90
+ uv venv --system-site-packages --python /usr/bin/python3 .venv
91
+ uv pip install -e . pytest
92
+
93
+ - name: pytest --wasm-backend gi-jsc (JIT on)
94
+ run: .venv/bin/python -m pytest -v --wasm-backend gi-jsc
95
+ env:
96
+ JSC_useJIT: "true"
97
+
98
+ - name: pytest --wasm-backend gi-jsc (JIT off, as on iOS)
99
+ if: ${{ !cancelled() }}
100
+ run: .venv/bin/python -m pytest -v --wasm-backend gi-jsc
101
+ env:
102
+ JSC_useJIT: "false"
103
+
104
+ - name: Example
105
+ run: WASMHOST_BACKEND=gi-jsc .venv/bin/python examples/basic.py
@@ -0,0 +1,17 @@
1
+ **/__pycache__
2
+ **/.ruff_cache/
3
+ **/.pytest_cache/
4
+ **/.idea
5
+ **/*.egg-info
6
+ **/.venv
7
+ **/build
8
+ **/dist
9
+ .vscode
10
+
11
+ # written by setuptools_scm at build time
12
+ src/wasmhost/_version.py
13
+ # coverage
14
+ .coverage
15
+ .coverage.*
16
+ coverage.xml
17
+ htmlcov/
@@ -0,0 +1,56 @@
1
+ default_install_hook_types: [pre-commit]
2
+ default_language_version:
3
+ python: python3
4
+
5
+ repos:
6
+ - repo: local
7
+ hooks:
8
+ - id: uv-lock
9
+ name: uv lock check
10
+ entry: uv
11
+ # --check only, not --upgrade: dependency bumps are Dependabot's job; --upgrade would re-resolve to
12
+ # whatever is newest on PyPI at run time and fail later pushes with no code change involved.
13
+ args: ["lock", "--check"]
14
+ language: system
15
+ pass_filenames: false
16
+ files: '^(pyproject\.toml|uv\.lock)$'
17
+
18
+ - id: uv-sync
19
+ name: uv sync before hooks
20
+ entry: uv
21
+ args: ["sync"]
22
+ language: system
23
+ pass_filenames: false
24
+
25
+ - id: uv-pyright
26
+ name: pyright
27
+ entry: uv
28
+ args: ["run", "pyright"]
29
+ language: system
30
+ pass_filenames: false
31
+ files: '\.pyi?$'
32
+
33
+ - id: uv-ruff-check
34
+ name: ruff lint & fix
35
+ entry: uv
36
+ args: ["run", "ruff", "check", "--fix"]
37
+ language: system
38
+ pass_filenames: false
39
+ files: '\.pyi?$'
40
+
41
+ - id: uv-ruff-format
42
+ name: ruff format
43
+ entry: uv
44
+ args: ["run", "ruff", "format"]
45
+ language: system
46
+ pass_filenames: false
47
+ files: '\.pyi?$'
48
+
49
+ - id: uv-pytest
50
+ name: pytest
51
+ entry: uv
52
+ # A few seconds: every JavaScript host that starts here (Node, and gi-jsc with PyGObject).
53
+ args: ["run", "pytest", "-q"]
54
+ language: system
55
+ pass_filenames: false
56
+ files: '(\.py|pyproject\.toml)$'
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 o-murphy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: wasmhost
3
+ Version: 0.0.1a1
4
+ Summary: WebAssembly for CPython, PyPy and Pythonista, run in JavaScriptCore or Node, with the JavaScript WebAssembly API
5
+ Author-email: o-murphy <thehelixpg@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: webassembly,wasm,javascriptcore,jscontext,pythonista,node
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Natural Language :: English
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Operating System :: iOS
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: wasmtime
28
+ Requires-Dist: wasmtime>=49.0.0; extra == "wasmtime"
29
+ Dynamic: license-file
30
+
31
+ # wasmhost
32
+
33
+ WebAssembly from **CPython, PyPy and Pythonista**, with the JavaScript WebAssembly API. It runs in
34
+ whichever JavaScript engine is available: JavaScriptCore's `JSContext` in Pythonista on iOS, WebKitGTK's
35
+ JavaScriptCore on Linux, or Node. Plain Python, no dependencies, no C extension.
36
+
37
+ ```python
38
+ import wasmhost
39
+
40
+ module = wasmhost.Module(open("lib.wasm", "rb").read()) # WebAssembly.Module
41
+ instance = wasmhost.Instance(module) # WebAssembly.Instance
42
+ print(instance.exports.add(2, 3)) # i32/i64 -> int, f32/f64 -> float
43
+ instance.exports.memory.write(ptr, b"data") # WebAssembly.Memory
44
+ print(instance.exports.counter.value) # WebAssembly.Global
45
+ ```
46
+
47
+ See `examples/basic.py`.
48
+
49
+ - **Types.** The JavaScript API can't tell a function's signature, and it matters (an `i64` argument must reach
50
+ JavaScript as a BigInt), so the binary's type, import, function, global and export sections are read in
51
+ Python. `Module.exports(module)` and `Module.imports(module)` describe a module with them.
52
+ - **Errors** are the API's: `CompileError`, `LinkError` and `Trap` (`WebAssembly.RuntimeError`, which is also a
53
+ `RuntimeError`); an out-of-bounds memory access is an `IndexError`.
54
+ - **Memory** is copied, not shared: `memory.read(offset, n)`, `memory.write(offset, data)`, `memory[a:b]`,
55
+ `memory.grow(pages)`.
56
+
57
+ ## Batches
58
+
59
+ On a JavaScript engine every call into it has a fixed cost (a pipe to `node`, a bridged Objective-C call in
60
+ 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:
61
+
62
+ ```python
63
+ batch = instance.batch()
64
+ ptr = batch.call(instance.exports.alloc, len(data)) # a Ref
65
+ batch.write(instance.exports.memory, ptr, data)
66
+ status = batch.call(instance.exports.run, ptr)
67
+ batch.stop_if_nonzero(status) # leave the rest out on an error status
68
+ out = batch.read(instance.exports.memory, ptr, 16)
69
+ batch.run()
70
+ out.value # bytes (`.done` says whether the step ran)
71
+ ```
72
+
73
+ A failing step (a trap, an out-of-bounds access) raises from `run()`, after the earlier steps' results are set.
74
+ Only `i32` results can be used in arithmetic (`ptr * 8`, `ptr + 4`).
75
+
76
+ ## Backends
77
+
78
+ | Backend | Where | How it is detected |
79
+ |---|---|---|
80
+ | `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) |
81
+ | `wasmtime` | anywhere with the `wasmtime` package | `import wasmtime` (`pip install wasmtime`) |
82
+ | `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) |
83
+ | `gi-jsc` | Linux | WebKitGTK's JavaScriptCore through PyGObject (`apt install gir1.2-javascriptcoregtk-4.1 python3-gi`) |
84
+ | `node` | anywhere with Node.js | `node` on `PATH` |
85
+
86
+ With nothing configured, the first backend that starts wins, in the order shown. Each backend's constructor is its
87
+ own probe: it fails when its runtime is missing. Choose one with `WASMHOST_BACKEND=<name>`,
88
+ `wasmhost.set_backend("<name>")` or `Module(..., backend="<name>")`; `wasmhost.get_backend().name` says which is in
89
+ use. `wasmhost.close()` closes the backends it started. (In WebAssembly's words the *host* is the embedder, the
90
+ Python side that provides imports; what runs the module is the backend.)
91
+
92
+ Not every backend can do everything (`backend.supports("memory.grow")` and `supports("table.length")` say):
93
+ `wasm3` can't `Memory.grow` from Python (`NotImplementedError`; a module's own `memory.grow` works) and has no
94
+ tables API.
95
+
96
+ ## Try it on a device
97
+
98
+ The package carries a self-test, since nothing else can be run in Pythonista to see whether this works there:
99
+
100
+ ```python
101
+ import wasmhost
102
+ wasmhost.selftest() # or, from a shell: python -m wasmhost [--backend NAME] [--all]
103
+ ```
104
+
105
+ It prints one line per check (the Objective-C bridge in use, `WebAssembly` and `BigInt` in the engine, calls,
106
+ `i64`, memory, globals, traps, batches, the cost of a call), then `N/M passed`. If something fails, send the whole
107
+ output. On a computer, `python -m wasmhost --all` runs it on every backend that starts.
108
+
109
+ ### Where it has been run
110
+
111
+ | Where | Backend | Result | A call / a batch of 3 |
112
+ |---|---|---|---|
113
+ | Pythonista 3 (StaSh 0.7.5), Python 3.10.4, iPhone17,3 | `jscontext` (`objc_util`) | 23/23 | 53 / 97 us |
114
+ | PythonIDE, Python 3.14.7, `ios-13.0-arm64-iphoneos` | `jscontext` (`objc_util`) | 23/23 | 37 / 76 us |
115
+ | Linux, CPython 3.14t | `gi-jsc` | 23/23 | 34 / 78 us |
116
+ | Linux, CPython 3.14t | `node` | 23/23 | 82 / 116 us |
117
+ | Linux, CPython 3.14t | `wasmtime` | 18/18 | 69 / 233 us |
118
+ | Linux, CPython 3.14t | `wasm3` | 18/18 | 4 / 46 us |
119
+ | Linux, CPython 3.10 and PyPy 3.10 | `node` | 23/23 (and the test suite on 3.10) | |
120
+
121
+ The times are one run of the self-test each, so read them as an order of magnitude. Not run on a device: the
122
+ `rubicon-objc` bridge (both iOS apps above have `objc_util`, so it wasn't needed), and imports (see below).
123
+
124
+ ## Not yet
125
+
126
+ - **Imports.** A module that imports host functions, memories, tables or globals can't be instantiated
127
+ (`NotImplementedError`). Python callbacks need a synchronous bridge: native for `wasmtime` and `wasm3`, a JavaScript function
128
+ made from Python for `gi-jsc`, an Objective-C block for `jscontext`, and for `node` a blocking read of the pipe.
129
+ - **Tables** beyond their length, `v128` and reference types, multi-value results in a batch.
130
+
131
+ ## Test
132
+
133
+ ```bash
134
+ uv run pytest # every backend that starts here
135
+ uv run pytest --wasm-backend node # one backend: it must start, or the run stops with an error
136
+ uv run pytest --wasm-backend wasmtime # or wasm3
137
+ uv run pytest --wasm-backend gi-jsc # needs PyGObject: run it with a system-site-packages venv (see the CI job)
138
+ uv run pyright && uv run ruff check
139
+ ```
@@ -0,0 +1,109 @@
1
+ # wasmhost
2
+
3
+ WebAssembly from **CPython, PyPy and Pythonista**, with the JavaScript WebAssembly API. It runs in
4
+ whichever JavaScript engine is available: JavaScriptCore's `JSContext` in Pythonista on iOS, WebKitGTK's
5
+ JavaScriptCore on Linux, or Node. Plain Python, no dependencies, no C extension.
6
+
7
+ ```python
8
+ import wasmhost
9
+
10
+ module = wasmhost.Module(open("lib.wasm", "rb").read()) # WebAssembly.Module
11
+ instance = wasmhost.Instance(module) # WebAssembly.Instance
12
+ print(instance.exports.add(2, 3)) # i32/i64 -> int, f32/f64 -> float
13
+ instance.exports.memory.write(ptr, b"data") # WebAssembly.Memory
14
+ print(instance.exports.counter.value) # WebAssembly.Global
15
+ ```
16
+
17
+ See `examples/basic.py`.
18
+
19
+ - **Types.** The JavaScript API can't tell a function's signature, and it matters (an `i64` argument must reach
20
+ JavaScript as a BigInt), so the binary's type, import, function, global and export sections are read in
21
+ Python. `Module.exports(module)` and `Module.imports(module)` describe a module with them.
22
+ - **Errors** are the API's: `CompileError`, `LinkError` and `Trap` (`WebAssembly.RuntimeError`, which is also a
23
+ `RuntimeError`); an out-of-bounds memory access is an `IndexError`.
24
+ - **Memory** is copied, not shared: `memory.read(offset, n)`, `memory.write(offset, data)`, `memory[a:b]`,
25
+ `memory.grow(pages)`.
26
+
27
+ ## Batches
28
+
29
+ On a JavaScript engine every call into it has a fixed cost (a pipe to `node`, a bridged Objective-C call in
30
+ 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:
31
+
32
+ ```python
33
+ batch = instance.batch()
34
+ ptr = batch.call(instance.exports.alloc, len(data)) # a Ref
35
+ batch.write(instance.exports.memory, ptr, data)
36
+ status = batch.call(instance.exports.run, ptr)
37
+ batch.stop_if_nonzero(status) # leave the rest out on an error status
38
+ out = batch.read(instance.exports.memory, ptr, 16)
39
+ batch.run()
40
+ out.value # bytes (`.done` says whether the step ran)
41
+ ```
42
+
43
+ A failing step (a trap, an out-of-bounds access) raises from `run()`, after the earlier steps' results are set.
44
+ Only `i32` results can be used in arithmetic (`ptr * 8`, `ptr + 4`).
45
+
46
+ ## Backends
47
+
48
+ | Backend | Where | How it is detected |
49
+ |---|---|---|
50
+ | `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) |
51
+ | `wasmtime` | anywhere with the `wasmtime` package | `import wasmtime` (`pip install wasmtime`) |
52
+ | `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) |
53
+ | `gi-jsc` | Linux | WebKitGTK's JavaScriptCore through PyGObject (`apt install gir1.2-javascriptcoregtk-4.1 python3-gi`) |
54
+ | `node` | anywhere with Node.js | `node` on `PATH` |
55
+
56
+ With nothing configured, the first backend that starts wins, in the order shown. Each backend's constructor is its
57
+ own probe: it fails when its runtime is missing. Choose one with `WASMHOST_BACKEND=<name>`,
58
+ `wasmhost.set_backend("<name>")` or `Module(..., backend="<name>")`; `wasmhost.get_backend().name` says which is in
59
+ use. `wasmhost.close()` closes the backends it started. (In WebAssembly's words the *host* is the embedder, the
60
+ Python side that provides imports; what runs the module is the backend.)
61
+
62
+ Not every backend can do everything (`backend.supports("memory.grow")` and `supports("table.length")` say):
63
+ `wasm3` can't `Memory.grow` from Python (`NotImplementedError`; a module's own `memory.grow` works) and has no
64
+ tables API.
65
+
66
+ ## Try it on a device
67
+
68
+ The package carries a self-test, since nothing else can be run in Pythonista to see whether this works there:
69
+
70
+ ```python
71
+ import wasmhost
72
+ wasmhost.selftest() # or, from a shell: python -m wasmhost [--backend NAME] [--all]
73
+ ```
74
+
75
+ It prints one line per check (the Objective-C bridge in use, `WebAssembly` and `BigInt` in the engine, calls,
76
+ `i64`, memory, globals, traps, batches, the cost of a call), then `N/M passed`. If something fails, send the whole
77
+ output. On a computer, `python -m wasmhost --all` runs it on every backend that starts.
78
+
79
+ ### Where it has been run
80
+
81
+ | Where | Backend | Result | A call / a batch of 3 |
82
+ |---|---|---|---|
83
+ | Pythonista 3 (StaSh 0.7.5), Python 3.10.4, iPhone17,3 | `jscontext` (`objc_util`) | 23/23 | 53 / 97 us |
84
+ | PythonIDE, Python 3.14.7, `ios-13.0-arm64-iphoneos` | `jscontext` (`objc_util`) | 23/23 | 37 / 76 us |
85
+ | Linux, CPython 3.14t | `gi-jsc` | 23/23 | 34 / 78 us |
86
+ | Linux, CPython 3.14t | `node` | 23/23 | 82 / 116 us |
87
+ | Linux, CPython 3.14t | `wasmtime` | 18/18 | 69 / 233 us |
88
+ | Linux, CPython 3.14t | `wasm3` | 18/18 | 4 / 46 us |
89
+ | Linux, CPython 3.10 and PyPy 3.10 | `node` | 23/23 (and the test suite on 3.10) | |
90
+
91
+ The times are one run of the self-test each, so read them as an order of magnitude. Not run on a device: the
92
+ `rubicon-objc` bridge (both iOS apps above have `objc_util`, so it wasn't needed), and imports (see below).
93
+
94
+ ## Not yet
95
+
96
+ - **Imports.** A module that imports host functions, memories, tables or globals can't be instantiated
97
+ (`NotImplementedError`). Python callbacks need a synchronous bridge: native for `wasmtime` and `wasm3`, a JavaScript function
98
+ made from Python for `gi-jsc`, an Objective-C block for `jscontext`, and for `node` a blocking read of the pipe.
99
+ - **Tables** beyond their length, `v128` and reference types, multi-value results in a batch.
100
+
101
+ ## Test
102
+
103
+ ```bash
104
+ uv run pytest # every backend that starts here
105
+ uv run pytest --wasm-backend node # one backend: it must start, or the run stops with an error
106
+ uv run pytest --wasm-backend wasmtime # or wasm3
107
+ uv run pytest --wasm-backend gi-jsc # needs PyGObject: run it with a system-site-packages venv (see the CI job)
108
+ uv run pyright && uv run ruff check
109
+ ```