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.
- wasmhost-0.0.1a1/.github/dependabot.yml +24 -0
- wasmhost-0.0.1a1/.github/workflows/pre-commit.yml +83 -0
- wasmhost-0.0.1a1/.github/workflows/publish.yml +110 -0
- wasmhost-0.0.1a1/.github/workflows/tests.yml +105 -0
- wasmhost-0.0.1a1/.gitignore +17 -0
- wasmhost-0.0.1a1/.pre-commit-config.yaml +56 -0
- wasmhost-0.0.1a1/LICENSE +21 -0
- wasmhost-0.0.1a1/PKG-INFO +139 -0
- wasmhost-0.0.1a1/README.md +109 -0
- wasmhost-0.0.1a1/examples/basic.py +38 -0
- wasmhost-0.0.1a1/pyproject.toml +80 -0
- wasmhost-0.0.1a1/setup.cfg +4 -0
- wasmhost-0.0.1a1/src/wasmhost/__init__.py +70 -0
- wasmhost-0.0.1a1/src/wasmhost/__main__.py +5 -0
- wasmhost-0.0.1a1/src/wasmhost/_api.py +462 -0
- wasmhost-0.0.1a1/src/wasmhost/_backend.py +163 -0
- wasmhost-0.0.1a1/src/wasmhost/_binary.py +169 -0
- wasmhost-0.0.1a1/src/wasmhost/_errors.py +21 -0
- wasmhost-0.0.1a1/src/wasmhost/_js.py +382 -0
- wasmhost-0.0.1a1/src/wasmhost/_native.py +195 -0
- wasmhost-0.0.1a1/src/wasmhost/_registry.py +60 -0
- wasmhost-0.0.1a1/src/wasmhost/_selftest.py +290 -0
- wasmhost-0.0.1a1/src/wasmhost/_version.py +24 -0
- wasmhost-0.0.1a1/src/wasmhost/py.typed +0 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/PKG-INFO +139 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/SOURCES.txt +37 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/dependency_links.txt +1 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/requires.txt +3 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/scm_file_list.json +33 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/scm_version.json +8 -0
- wasmhost-0.0.1a1/src/wasmhost.egg-info/top_level.txt +1 -0
- wasmhost-0.0.1a1/tests/conftest.py +69 -0
- wasmhost-0.0.1a1/tests/fake_objc.py +98 -0
- wasmhost-0.0.1a1/tests/test_api.py +227 -0
- wasmhost-0.0.1a1/tests/test_binary.py +32 -0
- wasmhost-0.0.1a1/tests/test_jscontext.py +67 -0
- wasmhost-0.0.1a1/tests/test_selftest.py +37 -0
- wasmhost-0.0.1a1/tests/wasm_builder.py +112 -0
- 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)$'
|
wasmhost-0.0.1a1/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,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
|
+
```
|