Star 历史趋势
数据来源: GitHub API · 生成自 Stargazers.cn
README.md

micropython-wasm

PyPI Tests Changelog License

MicroPython packaged as a WASI WebAssembly module and executed from Python using Wasmtime.

See Running Python code in a sandbox with MicroPython and WASM for background on this project.

This project is an experimental Python package for running small snippets of MicroPython in a fresh WebAssembly sandbox. It is designed around:

  • A custom MicroPython WASI artifact, not the Emscripten browser/Node build.
  • The official wasmtime Python package.
  • A fresh Wasmtime instance for one-shot execution, plus an optional persistent session API backed by a background thread.
  • No host filesystem access unless an explicit read-only directory is preopened.
  • No network capability.
  • Configurable WebAssembly memory, fuel, and wall-clock controls.

Installation

Install from PyPI:

pip install micropython-wasm

For local development, use uv:

git clone https://github.com/simonw/micropython-wasm cd micropython-wasm uv run pytest

Quick Start

You can run MicroPython in WASM from the command line:

micropython-wasm -c "print(1 + 1)" micropython-wasm script.py micropython-wasm

You can also micropython-wasm without installing it first using uvx:

uvx micropython-wasm --help

The no-argument form starts a simple REPL with persistent state between prompts. Use --memory to set the WebAssembly memory limit in bytes and --fuel to set the Wasmtime fuel budget:

micropython-wasm --memory 33554432 --fuel 20000000 -c "print('hello')"

To use it in Python import code from the micropython_wasm package:

from micropython_wasm import run result = run("print(1 + 1)") print(result.stdout)

Output:

2

run() returns a RunResult:

from micropython_wasm import run result = run("print('hello')") print(result.stdout) # "hello\n" print(result.stderr) # "" print(result.fuel_remaining) # integer Wasmtime fuel count

Each call creates a new engine, store, WASI config, module instance, and MicroPython process. Globals and imports do not persist between calls.

For stateful usage with real resident MicroPython state, use MicroPythonSession:

from micropython_wasm import MicroPythonSession with MicroPythonSession() as session: print(session.run("x = 10\nprint(x)").stdout) print(session.run("x += 5\nprint(x)").stdout) print(session.run("print(x * 2)").stdout)

Output:

10 15 30

You can also use the same object without a context manager, which is convenient in an interactive Python REPL:

from micropython_wasm import MicroPythonSession session = MicroPythonSession() session.run("x = 10") session.run("print(x)") session.close()

API

run(code, ...)

Run MicroPython source code using the bundled artifact:

from micropython_wasm import run result = run( "print(sum(range(10)))", memory_bytes=16 * 1024 * 1024, fuel=20_000_000, wall_timeout_seconds=1.0, host_result_bytes=256 * 1024, )

Arguments:

  • code: MicroPython source code passed as micropython -c <code>.
  • wasm_path: optional path to a custom WASI MicroPython artifact. If omitted, micropython_wasm/artifacts/micropython-wasi.wasm is used.
  • memory_bytes: maximum WebAssembly linear memory for the store.
  • fuel: Wasmtime fuel budget. Guest execution traps when it runs out.
  • wall_timeout_seconds: wall-clock timeout. Pass None to disable epoch interruption.
  • readonly_dir: optional host directory to expose inside the guest as /input, with read-only WASI directory and file permissions.
  • host_functions: optional mapping of host function names to Python callables. This enables the low-level host.call(name, payload_json) bridge.
  • host_result_bytes: maximum serialized host callback response size. The default is 256 * 1024.

run_micropython_wasi(code, wasm_path, ...)

Run code against an explicit .wasm artifact:

from micropython_wasm import run_micropython_wasi result = run_micropython_wasi( "print(2 ** 8)", "micropython_wasm/artifacts/micropython-wasi.wasm", )

This is useful when testing a locally rebuilt MicroPython artifact before copying it into the package.

MicroPythonSession(...)

Create a persistent MicroPython VM running in a background Python thread:

from micropython_wasm import MicroPythonSession session = MicroPythonSession() session.run("x = 10") session.run("x += 5") result = session.run("print(x)") print(result.stdout) session.close()

MicroPythonSession starts lazily on the first run(). A bootstrap loop runs inside MicroPython and repeatedly calls back to the Python host for the next code snippet. Each snippet is executed with exec(..., globals()) in the same MicroPython VM, so variables, imports, functions, classes, and live objects really stay resident between calls.

MicroPythonSession accepts the same resource, filesystem, and host function arguments as run():

session = MicroPythonSession( memory_bytes=16 * 1024 * 1024, fuel=20_000_000, readonly_dir="fixtures", host_functions={"add": lambda a, b: a + b}, host_result_bytes=256 * 1024, )

Methods and properties:

  • session.run(code): run code in the resident VM and return a RunResult.
  • session.register_function(func, name=None): expose a Python function to MicroPython code, optionally under a custom name.
  • session.close(): send a close message to the guest loop and reject further runs.
  • session.closed: True after close().
  • session.host_functions: copy of registered host functions.
  • Context manager support: with MicroPythonSession() as session: ....

Fuel is refreshed for each session.run() request. If a snippet exhausts fuel or otherwise traps the guest, the background VM stops and the session should be discarded.

MicroPythonReplaySession(...)

Create a transcript-backed session object with the same basic run() and close() shape as MicroPythonSession, but without keeping a MicroPython VM running in a background thread.

MicroPythonReplaySession does not run a background thread and does not keep a live MicroPython VM between calls. Each run() call executes a fresh Wasmtime instance and returns when that one command-style execution finishes. To preserve variables, functions, classes, and imports from the caller's point of view, it reconstructs state by replaying previous successful snippets before each new snippet.

from micropython_wasm import MicroPythonReplaySession session = MicroPythonReplaySession() session.run(""" import math def hypotenuse(a, b): return math.sqrt(a * a + b * b) """) result = session.run("print(hypotenuse(3, 4))") print(result.stdout) session.close()

MicroPythonReplaySession accepts the same resource and filesystem arguments as run():

session = MicroPythonReplaySession( memory_bytes=16 * 1024 * 1024, fuel=20_000_000, wall_timeout_seconds=1.0, readonly_dir="fixtures", host_result_bytes=256 * 1024, )

Methods and properties:

  • session.run(code): run code and return a RunResult.
  • session.close(): clear the transcript and reject further runs.
  • session.closed: True after close().
  • session.snippets: tuple of successful snippets currently retained.
  • Context manager support: with MicroPythonReplaySession() as session: ....

Only successful snippets are retained. If a snippet exits nonzero or traps, it is not added to the session transcript:

from micropython_wasm import MicroPythonReplaySession, MicroPythonWasmError session = MicroPythonReplaySession() session.run("x = 1") try: session.run("x = 2\nraise ValueError('boom')") except MicroPythonWasmError: pass print(session.run("print(x)").stdout) # "1\n"

For MicroPythonReplaySession, each session.run() call creates a fresh guest instance, replays previous successful snippets, emits an internal marker, then runs the new snippet and returns only the output after that marker. This preserves ordinary Python state from the caller's point of view, including variables, functions, classes, and imports, but previous snippets are re-executed internally on every call.

That replay behavior matters if previous snippets perform side effects such as writing files, making time-dependent calculations, consuming randomness, or mutating external host state. Use MicroPythonSession when you want true resident in-VM persistence.

For example, this calls the Python record() function again during the second session.run(), because the first snippet is replayed before print(count) is executed:

from micropython_wasm import MicroPythonReplaySession calls = [] def record(value): calls.append(value) return len(calls) session = MicroPythonReplaySession(host_functions={"record": record}) session.run("count = record('once')") session.run("print(count)") session.close() print(calls) # ["once", "once"]

Host Functions

MicroPythonSession and MicroPythonReplaySession can expose regular Python functions to MicroPython code. Register a function, then call it by name inside the guest:

from micropython_wasm import MicroPythonSession def add(a, b): return a + b session = MicroPythonSession() session.register_function(add) result = session.run("print(add(2, 3))") print(result.stdout)

Output:

5

If the Python callable already has the name you want to expose, pass it directly:

def shout(value): return value.upper() + "!" session = MicroPythonSession() session.register_function(shout) print(session.run("print(shout('hello'))").stdout)

To expose a function under a different MicroPython name, pass name=:

def add(a, b): return a + b session = MicroPythonSession() session.register_function(add, name="plus") print(session.run("print(plus(2, 3))").stdout)

You can also provide functions when constructing the session:

def format_name(first, last, uppercase=False): result = f"{first} {last}" if uppercase: result = result.upper() return result session = MicroPythonSession( host_functions={"format_name": format_name}, ) print(session.run("print(format_name('Ada', last='Lovelace', uppercase=True))").stdout)

Arguments and return values cross the WebAssembly boundary as JSON. Supported values are therefore JSON-compatible values: None, booleans, numbers, strings, lists, and dictionaries with string keys.

Python-side exceptions are returned to the MicroPython wrapper and raised as RuntimeError, so guest code can catch them:

def fail(): raise ValueError("bad host value") session = MicroPythonSession(host_functions={"fail": fail}) result = session.run(""" try: fail() except RuntimeError as ex: print(str(ex)) """) print(result.stdout)

Output:

ValueError: bad host value

Under the hood the bundled MicroPython artifact includes a tiny built-in module named host. That module imports micropython_wasm.host_call from Wasmtime and exposes a low-level host.call(name, payload_json) function. The session API builds friendly MicroPython wrappers on top of that low-level bridge.

One-shot run() and run_micropython_wasi() also accept a host_functions mapping, but they do not automatically define friendly wrappers. They expose the low-level host module:

from micropython_wasm import run def add(a, b): return a + b result = run( """ import host print(host.call("add", '{"args": [2, 3], "kwargs": {}}')) """, host_functions={"add": add}, ) print(result.stdout)

default_wasm_path()

Return the package's expected artifact path:

from micropython_wasm import default_wasm_path print(default_wasm_path())

Exceptions

The package raises:

  • MicroPythonWasmArtifactNotFound if the configured artifact does not exist.
  • MicroPythonSessionClosed if session.run() is called after session.close().
  • MicroPythonWasmError for guest traps, nonzero guest exits, invalid artifacts, missing Wasmtime support, or invalid preopened directories.
  • ValueError for invalid host-side resource limits.

For example:

from micropython_wasm import MicroPythonWasmError, run try: run('raise ValueError("boom")') except MicroPythonWasmError as ex: print(ex)

Filesystem Access

By default, the guest gets no preopened host directories:

from micropython_wasm import run run("print('no files by default')")

To expose input files, place them in a directory and pass readonly_dir:

from pathlib import Path from micropython_wasm import run fixtures = Path("fixtures") fixtures.mkdir(exist_ok=True) (fixtures / "example.txt").write_text("hello from the host\n") result = run( "print(open('/input/example.txt').read())", readonly_dir=fixtures, ) print(result.stdout)

The directory is mounted at /input in the WASI guest. The package asks Wasmtime for read-only directory and file permissions. Attempts to write inside /input should fail.

Do not preopen your project root, home directory, /, or a shared temporary directory when running untrusted code.

Resource Controls

The host configures these Wasmtime controls for each execution:

  • Store.set_limits(memory_size=...) limits WebAssembly linear memory.
  • Store.set_fuel(...) limits CPU-like instruction progress.
  • Epoch interruption is enabled when wall_timeout_seconds is not None.
  • Config.max_wasm_stack is set to 512 * 1024.

Example:

from micropython_wasm import MicroPythonWasmError, run try: run( "while True:\n pass", fuel=50_000, wall_timeout_seconds=None, ) except MicroPythonWasmError as ex: print("stopped:", ex)

memory_bytes limits guest linear memory, not total host process RSS. Wasmtime runtime memory, compiled code, Python process memory, and host callbacks are outside that limit. For high-risk multi-tenant workloads, run each execution in a separate worker process with OS-level CPU, memory, and wall-clock limits too.

Network Access

This package does not expose network imports or host socket functions. The current MicroPython WASI artifact also has socket and SSL support disabled in the WASI variant configuration.

If network access is ever added, prefer a narrow host-mediated API such as http_get(url) with explicit allowlists, timeouts, redirect limits, and maximum response sizes. Do not expose raw sockets to untrusted code.

Supported Python Behavior

MicroPython is not CPython. It implements a substantial subset of Python, but there are differences in syntax support, standard-library coverage, object behavior, and platform details.

The test suite verifies useful behavior including:

  • Arithmetic and big integers.
  • Strings and bytes.
  • Lists, tuples, dictionaries, and sets.
  • List, dict, set, and generator comprehensions.
  • Functions, default arguments, keyword-only arguments, lambdas, closures, and recursion.
  • Classes, inheritance, property, isinstance.
  • try/except/finally and context managers.
  • math, json, re, binascii, sys, and os.listdir('/input').
  • Fresh execution state between calls.
  • Transcript-backed session state across MicroPythonReplaySession.run() calls.
  • True resident VM state across MicroPythonSession.run() calls.
  • Host function callbacks through session register_function() methods.
  • Read-only file preopens.
  • Fuel exhaustion.

Known observations from this artifact:

  • sys.platform reports linux.
  • sys.argv is ['-c'] inside the guest.
  • hashlib.sha256 is not available in the bundled artifact.
  • zlib is not available in the bundled artifact.

Listing Available Modules

To see the MicroPython import path and the modules available to the bundled artifact, run:

from micropython_wasm import run result = run( """ import sys print("sys.path:") for path in sys.path: print(" ", path) print() print("modules:") help("modules") """ ) print(result.stdout)

help("modules") is the most useful MicroPython-native listing because it includes built-in and frozen modules as well as modules available on the filesystem. A plain os.listdir() scan of sys.path will miss frozen modules in this artifact.

Rebuilding the WASI Artifact

The build helper is:

scripts/build_micropython_wasi.py

It:

  1. Clones MicroPython into /tmp/micropython-wasm-build/micropython.
  2. Checks out the requested ref, including GitHub PR refs such as pull/13676/head.
  3. Builds mpy-cross.
  4. Runs make submodules for ports/unix.
  5. Builds ports/unix VARIANT=wasi.
  6. Includes the bundled host user C module by default.
  7. Finds the best wasm artifact.
  8. Copies it to micropython_wasm/artifacts/micropython-wasi.wasm.

macOS ARM64 Setup

Install Binaryen:

brew install binaryen

Download wasi-sdk 25.0 to /tmp:

curl -L -o /tmp/wasi-sdk-25.0-arm64-macos.tar.gz \ https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz tar -xzf /tmp/wasi-sdk-25.0-arm64-macos.tar.gz -C /tmp

Build the artifact:

uv run python scripts/build_micropython_wasi.py \ --ref pull/13676/head \ --wasi-sdk /tmp/wasi-sdk-25.0-arm64-macos

Use --clean to discard the existing /tmp checkout:

uv run python scripts/build_micropython_wasi.py \ --clean \ --ref pull/13676/head \ --wasi-sdk /tmp/wasi-sdk-25.0-arm64-macos

Use --skip-build to recopy an already-built artifact from the checkout:

uv run python scripts/build_micropython_wasi.py \ --skip-build \ --ref pull/13676/head \ --wasi-sdk /tmp/wasi-sdk-25.0-arm64-macos

Useful Build Options

  • --repo-url: alternate MicroPython repository.
  • --ref: git ref to build. PR refs like pull/<number>/head are supported.
  • --work-dir: alternate build checkout directory.
  • --output: alternate destination for the copied artifact.
  • --variant: alternate Unix variant, defaults to wasi.
  • --wasi-sdk: path to a wasi-sdk directory.
  • --user-c-modules: path to a MicroPython USER_C_MODULES directory.
  • --jobs: parallel build jobs.
  • --extra-make-arg: additional argument forwarded to the Unix make command.
  • --clean: remove the checkout before cloning.
  • --skip-build: find and copy an existing artifact without rebuilding.

On macOS, the script passes CFLAGS_EXTRA=-Wno-error=gnu-folding-constant when building mpy-cross, because the experimental branch currently trips this Apple Clang warning while using -Werror.

Artifact Selection

The script prefers artifacts in this order:

  1. build-wasi/micropython.spilled.exnref
  2. build-wasi/micropython.exnref
  3. build-wasi/micropython.wasm
  4. build-wasi/micropython

The current local build produced:

/tmp/micropython-wasm-build/micropython/ports/unix/build-wasi/micropython /tmp/micropython-wasm-build/micropython/ports/unix/build-wasi/micropython.exnref micropython_wasm/artifacts/micropython-wasi.wasm

The copied package artifact is the micropython.exnref output.

Testing

Run the full test suite:

uv run pytest

The suite includes package tests and runtime integration tests against the bundled wasm artifact. If the artifact is missing, runtime integration tests are skipped, but package/build-script tests still run.

To test a custom artifact manually:

uv run python - <<'PY' from micropython_wasm import run_micropython_wasi result = run_micropython_wasi( "print(1 + 1)", "/path/to/micropython-wasi.wasm", ) print(result.stdout) PY

Security Notes

This package is a Wasmtime embedding for a MicroPython WASI command module. It is a useful sandboxing layer, but it is not a complete security boundary by itself for high-risk production use.

Reasonable defaults in this package:

  • Fresh instance for each run.
  • No inherited host environment.
  • No preopened host directories by default.
  • Optional read-only preopened directory only.
  • No network host functions.
  • Fuel and memory limits.
  • Optional wall-clock timeout.

Additional protections to consider:

  • Run executions in separate worker processes.
  • Apply OS-level memory and CPU limits.
  • Put workers in containers or another isolation boundary.
  • Bound stdout/stderr capture size if running untrusted code at scale.
  • Avoid host callbacks that expose ambient authority.
  • Keep Wasmtime and the MicroPython artifact pinned and regularly tested.

Implementation Status and Caveats

The repository currently includes a working bundled artifact at:

micropython_wasm/artifacts/micropython-wasi.wasm

That artifact was built from MicroPython PR #13676, using the PR ref pull/13676/head. MicroPython's WASI Unix variant is still experimental upstream, so this package should also be treated as experimental.

The test suite verifies the bundled artifact against arithmetic, strings, bytes, collections, comprehensions, functions, closures, recursion, classes, exceptions, context managers, a small standard-library subset, fresh instance isolation, read-only file access, and fuel interruption. It also verifies both stateful session APIs: the transcript-backed MicroPythonReplaySession and the persistent background-thread MicroPythonSession.

One important build caveat: the PR's full post-link Binaryen pipeline currently fails here at wasm-opt --spill-pointers with Binaryen 130. The artifact in this repository uses the successful wasm-opt --translate-to-exnref postprocess instead. Simple and moderately broad Python execution works under Wasmtime, but this should be stress-tested before relying on it for hostile or long-running code.

License

Apache-2.0

关于 About

Python library for running a MicroPython sandbox using WebAssembly

语言 Languages

Python96.1%
C3.7%
Makefile0.2%

提交活跃度 Commit Activity

代码提交热力图
过去 52 周的开发活跃度
23
Total Commits
峰值: 19次/周
Less
More

核心贡献者 Contributors