wasmhost 0.0.1__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 (42) hide show
  1. wasmhost-0.0.1/.github/dependabot.yml +24 -0
  2. wasmhost-0.0.1/.github/workflows/pre-commit.yml +83 -0
  3. wasmhost-0.0.1/.github/workflows/publish.yml +111 -0
  4. wasmhost-0.0.1/.github/workflows/tests.yml +105 -0
  5. wasmhost-0.0.1/.gitignore +17 -0
  6. wasmhost-0.0.1/.pre-commit-config.yaml +56 -0
  7. wasmhost-0.0.1/LICENSE +21 -0
  8. wasmhost-0.0.1/PKG-INFO +213 -0
  9. wasmhost-0.0.1/README.md +183 -0
  10. wasmhost-0.0.1/examples/basic.py +38 -0
  11. wasmhost-0.0.1/examples/jslinux.py +463 -0
  12. wasmhost-0.0.1/examples/pyodide.py +746 -0
  13. wasmhost-0.0.1/pyproject.toml +80 -0
  14. wasmhost-0.0.1/setup.cfg +4 -0
  15. wasmhost-0.0.1/src/wasmhost/__init__.py +70 -0
  16. wasmhost-0.0.1/src/wasmhost/__main__.py +5 -0
  17. wasmhost-0.0.1/src/wasmhost/_api.py +462 -0
  18. wasmhost-0.0.1/src/wasmhost/_backend.py +163 -0
  19. wasmhost-0.0.1/src/wasmhost/_binary.py +169 -0
  20. wasmhost-0.0.1/src/wasmhost/_errors.py +21 -0
  21. wasmhost-0.0.1/src/wasmhost/_js.py +500 -0
  22. wasmhost-0.0.1/src/wasmhost/_native.py +195 -0
  23. wasmhost-0.0.1/src/wasmhost/_registry.py +60 -0
  24. wasmhost-0.0.1/src/wasmhost/_selftest.py +303 -0
  25. wasmhost-0.0.1/src/wasmhost/_version.py +24 -0
  26. wasmhost-0.0.1/src/wasmhost/py.typed +0 -0
  27. wasmhost-0.0.1/src/wasmhost.egg-info/PKG-INFO +213 -0
  28. wasmhost-0.0.1/src/wasmhost.egg-info/SOURCES.txt +40 -0
  29. wasmhost-0.0.1/src/wasmhost.egg-info/dependency_links.txt +1 -0
  30. wasmhost-0.0.1/src/wasmhost.egg-info/requires.txt +3 -0
  31. wasmhost-0.0.1/src/wasmhost.egg-info/scm_file_list.json +36 -0
  32. wasmhost-0.0.1/src/wasmhost.egg-info/scm_version.json +8 -0
  33. wasmhost-0.0.1/src/wasmhost.egg-info/top_level.txt +1 -0
  34. wasmhost-0.0.1/tests/conftest.py +84 -0
  35. wasmhost-0.0.1/tests/fake_objc.py +98 -0
  36. wasmhost-0.0.1/tests/test_api.py +227 -0
  37. wasmhost-0.0.1/tests/test_binary.py +32 -0
  38. wasmhost-0.0.1/tests/test_bytes.py +79 -0
  39. wasmhost-0.0.1/tests/test_jscontext.py +67 -0
  40. wasmhost-0.0.1/tests/test_selftest.py +37 -0
  41. wasmhost-0.0.1/tests/wasm_builder.py +112 -0
  42. wasmhost-0.0.1/uv.lock +402 -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@c18668ad3cf93ea998bef934396af7bb5c839dc7
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,111 @@
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@c18668ad3cf93ea998bef934396af7bb5c839dc7
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
+ name: dist # by name: without one, a single artifact is put straight into `path`, not into artifacts/dist
100
+ path: artifacts/dist
101
+
102
+ - name: Create GitHub release (draft)
103
+ env:
104
+ GH_TOKEN: ${{ github.token }}
105
+ run: |
106
+ gh release create "${{ github.ref_name }}" \
107
+ --repo "${{ github.repository }}" \
108
+ --title "wasmhost ${{ github.ref_name }}" \
109
+ --generate-notes \
110
+ --draft \
111
+ 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@c18668ad3cf93ea998bef934396af7bb5c839dc7
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@c18668ad3cf93ea998bef934396af7bb5c839dc7
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)$'
wasmhost-0.0.1/LICENSE ADDED
@@ -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,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: wasmhost
3
+ Version: 0.0.1
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 on whichever
34
+ backend is available: JavaScriptCore's `JSContext` in Pythonista on iOS, WebKitGTK's JavaScriptCore on Linux, Node,
35
+ or, when installed, wasmtime or wasm3. Plain Python, no dependencies, no C extension of its own.
36
+
37
+ [![license]][MIT]
38
+ [![pypi]][PyPiUrl]
39
+ [![py-versions]][sources]
40
+ [![Made in Ukraine]][SWUBadge]
41
+
42
+ [![powered by webassembly]][WebAssembly]
43
+
44
+ [![Tests](https://github.com/ballistics-lab/py-wasmhost/actions/workflows/tests.yml/badge.svg)](https://github.com/ballistics-lab/py-wasmhost/actions/workflows/tests.yml)
45
+ [![Pre-commit](https://github.com/ballistics-lab/py-wasmhost/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/ballistics-lab/py-wasmhost/actions/workflows/pre-commit.yml)
46
+
47
+ ```python
48
+ import wasmhost
49
+
50
+ module = wasmhost.Module(open("lib.wasm", "rb").read()) # WebAssembly.Module
51
+ instance = wasmhost.Instance(module) # WebAssembly.Instance
52
+ print(instance.exports.add(2, 3)) # i32/i64 -> int, f32/f64 -> float
53
+ instance.exports.memory.write(ptr, b"data") # WebAssembly.Memory
54
+ print(instance.exports.counter.value) # WebAssembly.Global
55
+ ```
56
+
57
+ See `examples/basic.py`, and for something bigger, both in a bare JavaScript engine:
58
+
59
+ - `examples/pyodide.py`: a Python REPL that runs in [Pyodide](https://pyodide.org) (CPython built to WebAssembly),
60
+ with a memory snapshot per backend, `fetch` served by Python and packages kept between sessions. It started as a
61
+ [gist](https://gist.github.com/o-murphy/dd898e490094eaddab0875187e27f11a) for a Pythonista `JSContext`.
62
+ - `examples/jslinux.py`: a Linux virtual machine (JSLinux, riscv64 or x86_64 Alpine), with its console on your
63
+ terminal.
64
+
65
+ Both keep their downloads in `$WASMHOST_CACHE`, else `~/.cache/wasmhost`, else `./.cache` where there is no usable
66
+ home directory (PythonIDE).
67
+
68
+ - **Types.** The JavaScript API can't tell a function's signature, and it matters (an `i64` argument must reach
69
+ JavaScript as a BigInt), so the binary's type, import, function, global and export sections are read in
70
+ Python. `Module.exports(module)` and `Module.imports(module)` describe a module with them.
71
+ - **Errors** are the API's: `CompileError`, `LinkError` and `Trap` (`WebAssembly.RuntimeError`, which is also a
72
+ `RuntimeError`); an out-of-bounds memory access is an `IndexError`.
73
+ - **Memory** is copied, not shared: `memory.read(offset, n)`, `memory.write(offset, data)`, `memory[a:b]`,
74
+ `memory.grow(pages)`.
75
+
76
+ ## Installation
77
+
78
+ ### uv
79
+
80
+ ```shell
81
+ uv add wasmhost
82
+
83
+ # With wasmtime, the in-process JIT (otherwise Node or WebKitGTK JavaScriptCore is used)
84
+ uv add wasmhost[wasmtime]
85
+ ```
86
+
87
+ ### pip
88
+
89
+ ```shell
90
+ pip install wasmhost
91
+
92
+ # With wasmtime, the in-process JIT (otherwise Node or WebKitGTK JavaScriptCore is used)
93
+ pip install wasmhost[wasmtime]
94
+ ```
95
+
96
+ The `wasm3` backend has no extra: pywasm3's PyPI release is years behind the API used here, so install it from git
97
+ (CPython 3.11+, needs a C compiler): `pip install "pywasm3 @ git+https://github.com/wasm3/pywasm3"`.
98
+
99
+ ### Pythonista and PythonIDE (iOS)
100
+
101
+ The ordinary wheel: it is pure Python (`py3-none-any`). In StaSh (Pythonista) or PythonIDE's pip,
102
+ `pip install wasmhost`, then run the self-test (see [Try it on a device](#try-it-on-a-device)).
103
+
104
+ ## Batches
105
+
106
+ On a JavaScript engine every call into it has a fixed cost (a pipe to `node`, a bridged Objective-C call in
107
+ 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:
108
+
109
+ ```python
110
+ batch = instance.batch()
111
+ ptr = batch.call(instance.exports.alloc, len(data)) # a Ref
112
+ batch.write(instance.exports.memory, ptr, data)
113
+ status = batch.call(instance.exports.run, ptr)
114
+ batch.stop_if_nonzero(status) # leave the rest out on an error status
115
+ out = batch.read(instance.exports.memory, ptr, 16)
116
+ batch.run()
117
+ out.value # bytes (`.done` says whether the step ran)
118
+ ```
119
+
120
+ A failing step (a trap, an out-of-bounds access) raises from `run()`, after the earlier steps' results are set.
121
+ Only `i32` results can be used in arithmetic (`ptr * 8`, `ptr + 4`).
122
+
123
+ ## Bytes in and out of a JavaScript engine
124
+
125
+ A program with JavaScript of its own (Pyodide, an emulator) needs to hand the engine files and read results back.
126
+ `JSBackend.put_bytes(target, data)` assigns a `Uint8Array` to a JavaScript expression (`__files["a"]`), and
127
+ `JSBackend.get_bytes(expr)` returns the bytes of one. Through hex everywhere; on JSContext through JavaScriptCore's
128
+ C API instead (`ctypes` under `objc_util`), which fills the array in place, with hex as the fallback if that ever
129
+ fails. The self-test reports which was used (`N bytes via C API` or `via hex`).
130
+
131
+ ## Backends
132
+
133
+ | Backend | Where | How it is detected |
134
+ |---|---|---|
135
+ | `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) |
136
+ | `wasmtime` | anywhere with the `wasmtime` package | `import wasmtime` (`pip install wasmtime`) |
137
+ | `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) |
138
+ | `gi-jsc` | Linux | WebKitGTK's JavaScriptCore through PyGObject (`apt install gir1.2-javascriptcoregtk-4.1 python3-gi`) |
139
+ | `node` | anywhere with Node.js | `node` on `PATH` |
140
+
141
+ With nothing configured, the first backend that starts wins, in the order shown. Each backend's constructor is its
142
+ own probe: it fails when its runtime is missing. Choose one with `WASMHOST_BACKEND=<name>`,
143
+ `wasmhost.set_backend("<name>")` or `Module(..., backend="<name>")`; `wasmhost.get_backend().name` says which is in
144
+ use. `wasmhost.close()` closes the backends it started. (In WebAssembly's words the *host* is the embedder, the
145
+ Python side that provides imports; what runs the module is the backend.)
146
+
147
+ Not every backend can do everything (`backend.supports("memory.grow")` and `supports("table.length")` say):
148
+ `wasm3` can't `Memory.grow` from Python (`NotImplementedError`; a module's own `memory.grow` works) and has no
149
+ tables API.
150
+
151
+ ## Try it on a device
152
+
153
+ The package carries a self-test, since nothing else can be run in Pythonista to see whether this works there:
154
+
155
+ ```python
156
+ import wasmhost
157
+
158
+ wasmhost.selftest() # or, from a shell: python -m wasmhost [--backend NAME] [--all]
159
+ ```
160
+
161
+ It prints one line per check (the Objective-C bridge in use, `WebAssembly` and `BigInt` in the engine, calls,
162
+ `i64`, memory, globals, traps, batches, the cost of a call), then `N/M passed`. If something fails, send the whole
163
+ output. On a computer, `python -m wasmhost --all` runs it on every backend that starts.
164
+
165
+ ### Where it has been run
166
+
167
+ | Where | Backend | Result | A call / a batch of 3 |
168
+ |---|---|---|---|
169
+ | Pythonista 3 (StaSh 0.7.5), Python 3.10.4, iPhone17,3 | `jscontext` (`objc_util`) | 23/23 | 53 / 97 us |
170
+ | PythonIDE, Python 3.14.7, `ios-13.0-arm64-iphoneos` | `jscontext` (`objc_util`) | 23/23 | 37 / 76 us |
171
+ | Linux, CPython 3.14t | `gi-jsc` | 23/23 | 34 / 78 us |
172
+ | Linux, CPython 3.14t | `node` | 23/23 | 82 / 116 us |
173
+ | Linux, CPython 3.14t | `wasmtime` | 18/18 | 69 / 233 us |
174
+ | Linux, CPython 3.14t | `wasm3` | 18/18 | 4 / 46 us |
175
+ | Linux, CPython 3.10 and PyPy 3.10 | `node` | 23/23 (and the test suite on 3.10) | |
176
+
177
+ The times are one run of the self-test each, so read them as an order of magnitude. Not run on a device: the
178
+ `rubicon-objc` bridge (both iOS apps above have `objc_util`, so it wasn't needed), and imports (see below).
179
+
180
+ ### A note on wasmtime and `faulthandler`
181
+
182
+ wasmtime installs process-wide signal handlers when its first engine is created, and uses them to catch a trap.
183
+ Python's `faulthandler` (on with `python -X faulthandler`, and in pytest) replaces the handlers when it is enabled
184
+ *after* that, and the first trap then ends the process (`Fatal Python error: Illegal instruction`). Enable it first
185
+ (or not at all), or start the backend later: this repo's `tests/conftest.py` does that.
186
+
187
+ ## Not yet
188
+
189
+ - **Imports.** A module that imports host functions, memories, tables or globals can't be instantiated
190
+ (`NotImplementedError`). Python callbacks need a synchronous bridge: native for `wasmtime` and `wasm3`, a JavaScript function
191
+ made from Python for `gi-jsc`, an Objective-C block for `jscontext`, and for `node` a blocking read of the pipe.
192
+ - **Tables** beyond their length, `v128` and reference types, multi-value results in a batch.
193
+
194
+ ## Test
195
+
196
+ ```bash
197
+ uv run pytest # every backend that starts here
198
+ uv run pytest --wasm-backend node # one backend: it must start, or the run stops with an error
199
+ uv run pytest --wasm-backend wasmtime # or wasm3
200
+ uv run pytest --wasm-backend gi-jsc # needs PyGObject: run it with a system-site-packages venv (see the CI job)
201
+ uv run pyright && uv run ruff check
202
+ ```
203
+
204
+ [sources]: https://github.com/ballistics-lab/py-wasmhost
205
+ [license]: https://img.shields.io/github/license/ballistics-lab/py-wasmhost?style=flat-square
206
+ [MIT]: https://opensource.org/licenses/MIT
207
+ [pypi]: https://img.shields.io/pypi/v/wasmhost?style=flat-square&logo=pypi
208
+ [PyPiUrl]: https://pypi.org/project/wasmhost/
209
+ [py-versions]: https://img.shields.io/pypi/pyversions/wasmhost?style=flat-square
210
+ [Made in Ukraine]: https://img.shields.io/badge/made_in-Ukraine-ffd700.svg?labelColor=0057b7&style=flat-square
211
+ [SWUBadge]: https://stand-with-ukraine.pp.ua
212
+ [WebAssembly]: https://webassembly.org
213
+ [powered by webassembly]: https://img.shields.io/badge/webassembly-%23654FF0?style=flat-square&logo=webassembly&logoColor=white&label=powered%20by