Geri Dön

PyWasm — Comprehensive Techincal Documentation


Version documented: 2.2.3
Specification target: WebAssembly Core Specification 2.0 (Draft 2025-04-25) · WASI Preview 1
Python requirement: ≥ 3.12
License: MIT


Table of Contents

  1. Executive Summary
  2. Project Overview
  3. Directory Structure
  4. Architecture
  5. Dependency Analysis
  6. File-by-File Documentation
  7. Execution Flow
  8. API Documentation
  9. Configuration Documentation
  10. Build & Deployment
  11. Security Analysis
  12. Performance Analysis
  13. Developer Guide
  14. Improvement Recommendations

1. Executive Summary

pywasm is a complete WebAssembly interpreter written entirely in standard Python (no third-party libraries). It parses .wasm binary files, instantiates modules according to the WebAssembly abstract machine semantics, and executes bytecode instruction by instruction inside a Python-level evaluation loop.

What problem it solves

WebAssembly runtimes (Wasmtime, V8, Wasmer) are fast but require native code and are difficult to embed into pure-Python environments or to instrument without binary patching. pywasm fills the niche of a 100 % portable, introspectable, embeddable WebAssembly interpreter: it runs wherever CPython or PyPy runs, and can be extended with host functions written in ordinary Python.

Target users

Audience Use case
Researchers / security auditors Trace WebAssembly execution, hook every instruction
Educators Study the WebAssembly abstract machine via readable Python
Tool builders Embed Wasm execution in Python scripts without native deps
CI/CD pipelines Validate Wasm modules on any platform

Key features

  • Full WebAssembly 2.0 instruction set (integer, float, SIMD, reference types, bulk-memory, table operations)
  • WASI Preview 1 host implementation (file I/O, sockets, clocks, randomness, environment, signals, polling)
  • Host-function injection API — call Python callables from inside Wasm
  • CLI entry-point (python -m pywasm)
  • Optional debug-level tracing of every decoded section and every executed instruction
  • PyPy-friendly design (≈10× speedup over CPython on PyPy)
  • Comprehensive spec-test suite and WASI test suite

High-level architecture

┌─────────────────────────────────────────────────────────────┐
│                        Host / User                          │
│   pywasm.Runtime  ◄──► pywasm.wasi.Preview1                │
└─────────────────┬───────────────────────────────────────────┘
                  │  instance_from_file / invocate
┌─────────────────▼───────────────────────────────────────────┐
│                   pywasm.core.Machine                       │
│  ┌───────────┐  ┌────────────────────────────────────────┐  │
│  │   Store   │  │              Stack                     │  │
│  │ func[]    │  │  value[] (ValInst)                     │  │
│  │ tabl[]    │  │  label[] (Label / control flow)        │  │
│  │ mems[]    │  │  frame[] (Frame / activation records)  │  │
│  │ glob[]    │  └────────────────────────────────────────┘  │
│  │ elem[]    │                                              │
│  │ data[]    │  ← allocate() / instance() / invocate()      │
│  └───────────┘                                              │
└─────────────────┬───────────────────────────────────────────┘
                  │  byte stream
┌─────────────────▼───────────────────────────────────────────┐
│               pywasm.core.ModuleDesc (binary parser)        │
│  Section loop: type / import / func / table / mem /        │
│                global / export / start / elem / code / data │
└─────────────────────────────────────────────────────────────┘
         helpers: pywasm.leb128 · pywasm.arith · pywasm.opcode

2. Project Overview

Project goals

  1. Correctness first. Pass the official WebAssembly spec test suite (core + SIMD) and the WASI preview-1 test suite.
  2. Readability. Every class and method mirrors the prose of the WebAssembly specification. Someone reading the spec should be able to map terminology directly to code.
  3. Embeddability. The public API is intentionally small: Runtime, instance_from_file, invocate, allocate_func_host.
  4. Zero dependencies. Only Python's standard library (io, struct, ctypes, math, typing, platform, os, socket, …).

Core concepts

Concept Description
ValType One of i32, i64, f32, f64, v128, ref.func, ref.extern
ValInst A 16-byte tagged union holding any WebAssembly value
ModuleDesc Static, decoded description of a .wasm binary (no runtime state)
ModuleInst Runtime instance: index tables pointing into the shared Store
Store Shared heap of function, table, memory, global, element, data instances
Machine Owns one Store and one Stack; drives the evaluation loop
Label Control-flow record for block / loop / if; tracks current instruction pointer
Frame Activation record for a function call; holds locals and module context
Runtime Convenience wrapper around Machine; manages import registration

Design philosophy

  • Specification-faithful naming. Classes, fields, and methods use the vocabulary of the WebAssembly spec wherever possible (e.g. ModuleInst, FuncInst, Store, allocate_func_wasm).
  • Everything is an index. ModuleInst stores integer indices into Store lists, not direct references. This allows the store to grow while modules hold stable handles.
  • 16-byte value representation. ValInst.data is always a 16-byte bytearray, supporting v128 (SIMD) uniformly alongside scalars. Scalars occupy the low bytes; the high bytes are zero-padded.
  • Iterative evaluation loop. Instead of recursive function calls, execution uses an iterative loop over stack.label. Frame and Label entries are pushed/popped by call and control-flow instructions. This keeps Python's call stack shallow and makes the design JIT-friendly.

Technology stack

Layer Technology
Language Python ≥ 3.12 (uses match/case, typing.Self)
Binary encoding struct, custom LEB128 reader
Arithmetic precision ctypes.c_float, struct.pack/unpack for IEEE 754
WASI I/O os, fcntl, socket, select, stat, io (Linux/macOS only)
Wasm source examples Rust → wasm32-unknown-unknown / wasm32-wasip1
Test infrastructure Official WebAssembly spec tests, official WASI test suite, wast2json from WABT
Packaging hatchling (pyproject.toml)
CI GitHub Actions (macOS, Ubuntu, Windows, Python 3.14)

3. Directory Structure

pywasm/                          ← repository root
├── .github/
│   └── workflows/
│       └── develop.yaml         ← CI: test on 3 OS × Python 3.14
├── .gitignore
├── LICENSE                      ← MIT
├── MANIFEST.in                  ← includes LICENSE in sdist
├── README.md
├── pyproject.toml               ← hatchling build; version 2.2.3
│
├── pywasm/                      ← THE LIBRARY (Python package)
│   ├── __init__.py              ← re-exports, version, platform guard
│   ├── __main__.py              ← CLI entry point
│   ├── arith.py                 ← fixed-width integer & IEEE 754 arithmetic
│   ├── core.py                  ← 4193-line interpreter heart
│   ├── leb128.py                ← LEB128 encode/decode helpers
│   ├── log.py                   ← lightweight debug logger
│   ├── opcode.py                ← opcode constants + name lookup table
│   └── wasi.py                  ← WASI Preview 1 implementation (Linux/macOS)
│
├── example/                     ← runnable Python driver scripts
│   ├── blake2b.py               ← Blake2b via Wasm
│   ├── blake2b_direct.py
│   ├── blake2b_iter.py
│   ├── blake2b_simd.py
│   ├── fibonacci.py
│   ├── fibonacci_env.py         ← host-function injection demo
│   ├── pi.py
│   ├── wasi_httpbin.py
│   ├── wasi_ll.py
│   ├── wasi_stdout.py           ← stdout capture demo
│   ├── wasi_zen.py
│   └── <name>/                  ← Rust crate for each example
│       ├── Cargo.toml
│       ├── src/lib.rs  (or main.rs)
│       └── bin/<name>.wasm      ← pre-compiled binary (committed)
│
├── res/
│   └── pywasm.jpg               ← project logo
│
├── script/
│   ├── build_example.py         ← compiles all Rust examples → .wasm
│   ├── build_spec.py            ← clones/updates spec test suite, runs wast2json
│   ├── build_wabt.py            ← downloads platform-appropriate WABT release
│   └── build_wasi.py            ← downloads wasi-testsuite
│
└── test/
    ├── example.py               ← runs all example/ scripts
    ├── main.py                  ← CLI smoke test (pi, wasi_ll)
    ├── spec.py                  ← WebAssembly spec conformance test runner
    └── wasi.py                  ← WASI test runner

Directory roles

Directory Role
pywasm/ The distributable Python package. Everything a downstream user installs.
example/ Demonstrates all public API surfaces; Rust source is the ground truth; .wasm binaries are committed so the repo is usable without a Rust toolchain.
script/ Developer-only automation: downloads tooling, compiles examples, prepares test fixtures. Not shipped in the sdist.
test/ Test suite. Requires external fixtures built by script/.
res/ Static assets (logo).
.github/ CI configuration.

4. Architecture

4.1 System architecture

pywasm is a single-process, single-threaded interpreter. There are no network calls, background threads, or foreign-function interfaces at runtime. The only external processes are spawned at development time (by scripts) — cargo build, wast2json, git.

4.2 Component architecture

┌──────────────────────────────────────────────────┐
│                  Public API Layer                │
│   Runtime (core.py)                              │
│   ├── allocate_func_host()                       │
│   ├── allocate_table()                           │
│   ├── allocate_memory()                          │
│   ├── allocate_global()                          │
│   ├── instance() / instance_from_file()          │
│   ├── invocate()                                 │
│   ├── exported_memory()                          │
│   └── exported_global()                          │
└──────────────────┬───────────────────────────────┘
                   │
┌──────────────────▼───────────────────────────────┐
│               Machine (core.py)                  │
│  ├── Store ─► FuncInst | FuncHost                │
│  │            TableInst                          │
│  │            MemInst (bytearray, pages)         │
│  │            GlobalInst                         │
│  │            ElemInst                           │
│  │            DataInst                           │
│  ├── Stack ─► value: list[ValInst]               │
│  │            label: list[Label]                 │
│  │            frame: list[Frame]                 │
│  │                                               │
│  ├── allocate()   — fills Store, returns ModInst │
│  ├── instance()   — validates imports, runs init │
│  ├── invocate()   — public function call entry   │
│  └── evaluate()   — the bytecode dispatch loop   │
└──────────────────┬───────────────────────────────┘
                   │
┌──────────────────▼───────────────────────────────┐
│             Binary Decoder (core.py)             │
│   ModuleDesc.from_reader()                       │
│   ├── Section 0x01 — type   (FuncType)           │
│   ├── Section 0x02 — import (Import)             │
│   ├── Section 0x03 — func   (FuncDesc type ref)  │
│   ├── Section 0x04 — table  (TableType)          │
│   ├── Section 0x05 — memory (MemType)            │
│   ├── Section 0x06 — global (GlobalDesc+Expr)    │
│   ├── Section 0x07 — export (ExportDesc)         │
│   ├── Section 0x08 — start  (function index)     │
│   ├── Section 0x09 — elem   (ElemDesc)           │
│   ├── Section 0x0a — code   (LocalsDesc+Expr)    │
│   ├── Section 0x0b — data   (DataDesc)           │
│   └── Section 0x0c — datacount                   │
└──────────────────────────────────────────────────┘
         ↑ uses
┌─────────────────────────────────────────────────┐
│          Support Modules                        │
│   leb128.py  — unsigned/signed LEB128 I/O       │
│   arith.py   — fixed-width arithmetic helpers   │
│   opcode.py  — opcode constants + name table    │
│   log.py     — timestamped debug output         │
└─────────────────────────────────────────────────┘

4.3 Runtime architecture (abstract machine)

The WebAssembly specification defines execution in terms of a Store and a Stack. pywasm implements both faithfully.

Store

All runtime objects live in one global Store per Machine:

Store.func  : list[FuncInst | FuncHost]   # function instances
Store.tabl  : list[TableInst]             # table instances
Store.mems  : list[MemInst]              # linear memory instances
Store.glob  : list[GlobalInst]           # global variable instances
Store.elem  : list[ElemInst]             # element segment instances
Store.data  : list[DataInst]             # data segment instances

Indices into these lists serve as addresses. A ModuleInst holds list[int] for each category, mapping module-local indices to store addresses.

Stack

Stack.value : list[ValInst]    # operand stack
Stack.label : list[Label]      # control-flow stack
Stack.frame : list[Frame]      # call-frame stack

All three are Python lists used as stacks (append / pop). The spec mixes all three kinds on a single conceptual stack; pywasm separates them into three parallel lists indexed by frame/label depth.

Label

class Label:
    arity : int        # number of return values
    frame : int        # frame depth at label entry
    value : int        # value-stack depth at label entry
    carry : int        # 0x00 = loop-back, 0x01 = pop label, 0x03 = pop frame too
    instr : list[Inst] # instruction sequence to execute
    index : int        # current instruction pointer within instr

carry is a bit field. Bit 0 controls branch semantics (loop vs block). Bit 1 indicates the label is associated with a Frame (i.e. a function activation), so popping the label also pops the frame.

Frame

class Frame:
    module : ModuleInst    # module context for resolving indices
    locals : LocalsInst    # local variables (including arguments)
    arity  : int           # number of return values expected
    label  : int           # label-stack depth at frame entry
    value  : int           # value-stack depth at frame entry

4.4 Opcode dispatch

The Machine.evaluate() method is a tight iterative loop:

for _ in range(1 << 32):     # effectively infinite; exception exits
    label = self.stack.label[-1]
    frame = self.stack.frame[-1]
    if label.index == len(label.instr):
        self.stack.label.pop()
        if label.carry & 2:  # frame-carrying label
            self.stack.frame.pop()
        continue
    instr = label.instr[label.index]
    label.index += 1
    match instr.opcode:
        case opcode.i32_add: ...
        case opcode.call:    ...
        # ... 400+ cases

Each iteration:

  1. Fetches the current label (top of label stack)
  2. If the instruction list is exhausted, pops the label (and optionally the frame)
  3. Otherwise, fetches the next instruction and dispatches via match

This design is allocation-free per iteration for most arithmetic instructions (just list indexing and ValInst construction).

4.5 Control-flow mechanics

Instruction Effect
block Push a new Label(carry=1) with the block's instruction list
loop Push a new Label(carry=0)br resets index to 0 (loops back)
if Pop condition; push Label with either then or else instruction list
br n evaluate_br(n): pop n labels from label stack, collect arity values, restore value stack
br_if Conditional br
br_table Indexed br
return Pop all labels up to and including the frame label
call Push a new Frame and a new Label(carry=3) containing the callee's instruction list

4.6 Instantiation lifecycle

sequenceDiagram
    participant User
    participant Runtime
    participant Machine
    participant Store

    User->>Runtime: instance_from_file(path)
    Runtime->>Runtime: open(path) → ModuleDesc.from_reader()
    Runtime->>Machine: instance(module, extern_list)
    Machine->>Machine: validate imports
    Machine->>Machine: push auxiliary Frame
    Machine->>Machine: evaluate global init exprs
    Machine->>Machine: evaluate elem init exprs
    Machine->>Store: allocate(module, extern, globin, elemin)
    Store-->>Machine: ModuleInst
    Machine->>Machine: push newmod Frame
    Machine->>Machine: run active elem segments (table_init)
    Machine->>Machine: run active data segments (memory_init)
    Machine->>Machine: call start function (if any)
    Machine->>Machine: pop newmod Frame
    Machine-->>Runtime: ModuleInst
    Runtime-->>User: ModuleInst

4.7 WASI integration architecture

┌──────────────────────┐
│  Preview1 (wasi.py)  │   bind(runtime)  →  registers 45 host funcs
│                      │                     under "wasi_snapshot_preview1"
│  fd: list[File]      │
│  args, dirs, envs    │
│  return_on_exit      │
└──────────┬───────────┘
           │  host functions called from Wasm via call_indirect/call
           ▼
┌──────────────────────┐
│  Machine (evaluate)  │   FuncHost.hostcode(machine, args) → rets
│                      │
│  reads/writes linear │
│  memory for iovec,   │
│  stat structs, etc.  │
└──────────────────────┘
           │
           ▼
   OS: open/read/write/socket/...

WASI functions receive a Machine reference and a list[int] of already-unwrapped argument values. They access linear memory through machine.store.mems[...].get_u32(ptr) etc. Results are written back into memory and an error code is returned as a list[int].


5. Dependency Analysis

pywasm has zero runtime dependencies outside the Python standard library.

Module Used by Purpose
ctypes arith.py, core.py c_float for IEEE 754 single-precision rounding; struct for byte layout
struct arith.py, core.py Pack/unpack little-endian integers and floats
math arith.py, core.py isnan, isinf, copysign, ceil, floor, trunc, sqrt for float instructions
io leb128.py, core.py io.BytesIO as a seekable byte reader for section-level parsing
typing core.py, leb128.py typing.Self, typing.Any, typing.Callable for type hints
platform __init__.py, wasi.py Detect OS to conditionally import wasi and handle Darwin vs Linux errno differences
os wasi.py All POSIX file operations (open, read, write, stat, lseek, pread, pwrite, openat-style dir_fd=)
fcntl wasi.py fcntl.F_SETFL for non-blocking / append flag changes
socket wasi.py TCP/UDP socket operations for sock_accept, sock_recv, sock_send, sock_shutdown
select wasi.py select.select() for poll_oneoff subscription polling
stat wasi.py stat.S_ISREG, stat.S_ISDIR for WASI file-type mapping
random wasi.py random.randbytes() for random_get
time wasi.py time.time_ns(), time.monotonic_ns(), time.process_time_ns() for WASI clocks
dataclasses wasi.py @dataclasses.dataclass for the Preview1.File descriptor
sys wasi.py, __main__.py sys.stdin/stdout/stderr file descriptors; sys.exit()
argparse __main__.py CLI argument parsing

Build-time (developer only)

Tool Purpose
hatchling PEP 517 build backend (declared in pyproject.toml)
wabt (wast2json) Converts .wast spec tests to .json + .wasm
Rust + cargo Compiles example Wasm binaries (wasm32-unknown-unknown, wasm32-wasip1)
git Clones the official WebAssembly spec and WASI test suite repos

6. File-by-File Documentation

6.1 pywasm/__init__.py

File purpose

Package entry point. Re-exports the public surface of core, conditionally imports wasi (POSIX only), and declares the library version.

Key components

version = '2.2.3'

The version string is duplicated in pyproject.toml. There is no automated synchronization between the two.

Platform guard

if platform.system().lower() in ['darwin', 'linux']:
    from . import wasi

wasi.py uses fcntl, select, os.pread, and POSIX-specific dir_fd= arguments which are not available on Windows. The guard prevents an ImportError on Windows, where WASI programs cannot currently be run through pywasm.

Relationships

  • Imports from: arith, core, leb128, log, opcode, conditionally wasi
  • from .core import * makes all public symbols from core.py available at the pywasm namespace level

6.2 pywasm/log.py

File purpose

Minimal timestamped logger used throughout the interpreter for debug tracing.

Key components

lvl = 0  # global debug level; set to 1 to enable tracing

def debugln(*args):
    if lvl > 0:
        println(*args)

def println(*args):
    pre = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
    print(pre, 'pywasm:', *args)

Internal logic

debugln is gated by lvl. Setting pywasm.log.lvl = 1 enables verbose output that includes:

  • Every section header as the binary is decoded
  • Every instruction executed during evaluate()
  • Global and element initializer evaluations

Notes

  • There is no log level above 1 (any non-zero value enables debug output).
  • There is no file-based logging or structured logging. All output goes to stdout.
  • Enabling tracing incurs significant overhead because string formatting happens for every instruction.

6.3 pywasm/leb128.py

File purpose

Implementation of LEB128 (Little-Endian Base-128) variable-length integer encoding, as specified by the WebAssembly binary format.

Key components

class _U — Unsigned LEB128
Method Signature Description
encode (i: int) → bytearray Encode a non-negative integer
decode (b: bytearray) → int Decode a pre-read bytearray
decode_reader (r: io.IOBase) → tuple[int, int] Read bytes from a stream until the MSB is 0; return (value, bytes_read)
class _I — Signed LEB128

Same interface as _U, but handles two's-complement sign extension in decode:

if e & 0x40 != 0:               # if the sign bit of the last byte is set
    r |= - (1 << (i * 7) + 7)  # sign-extend
Module-level singletons
u = _U()   # unsigned LEB128 codec
i = _I()   # signed LEB128 codec

These are used throughout core.py as pywasm.leb128.u.decode_reader(r).

Internal logic

The encoding loop accumulates 7 bits per byte with the high bit (0x80) set as a continuation flag. The decoder reverses this, collecting bytes until one lacks the continuation flag.

Notes

  • The decode_reader returns a (value, num_bytes_read) tuple, but in almost all call sites only [0] (the value) is used. The byte count is available for callers that need to advance a manual position counter.
  • There is no maximum byte length validation. Malformed inputs with an infinite continuation-bit stream would loop forever.

6.4 pywasm/arith.py

File purpose

Fixed-width arithmetic helper classes that correctly implement WebAssembly integer and floating-point semantics, which differ from Python's arbitrary-precision defaults.

Key components

class I(blen) — Signed integer of blen bytes
Attribute Value (for I(4))
bits_length 32
byte_length 4
mask 0xFFFFFFFF
mask_sign 0x80000000
max 2147483647
min -2147483648

Key methods:

  • fit(n) — Wraps n into the signed range using two's-complement masking:
    n = n & self.mask                        # truncate to bit width
    n = n - ((n & self.mask_sign) << 1)     # sign-extend
    
  • div(a, b) — Implements truncation toward zero (WebAssembly requirement):
    return a // b if a * b > 0 else (a + (-a % b)) // b
    
    Python's native // truncates toward negative infinity; this correction is critical for correctness.
  • rem(a, b) — Matching signed remainder (always has sign of dividend).
  • shl/shr — Shift amount is masked to bits_length - 1 (WebAssembly requirement).
class U(blen) — Unsigned integer of blen bytes

Same structure but fit is just n & mask. Adds rotation and bit-counting operations:

  • rotl(a, b) / rotr(a, b) — Circular bit rotation
  • clz(a) — Count leading zeros (bit loop, O(bits_length))
  • ctz(a) — Count trailing zeros
  • popcnt(a) — Population count (count set bits)
class F32 and class F64 — IEEE 754 float wrappers
  • F32.fit(n) uses ctypes.c_float(n).value to round to 32-bit precision — essential for correctness because Python's float is always 64-bit.
  • F64.div(a, b) handles division by zero per WebAssembly: returns ±∞ or NaN with correct sign.
  • min/max propagate NaN on either operand (WebAssembly NaN-propagation rules differ from Python's min/max).
Module-level singletons
i8, u8, i16, u16, i32, i64, u32, u64 = I(1), U(1), I(2), U(2), I(4), I(8), U(4), U(8)
f32, f64 = F32(), F64()

These are imported by core.py as pywasm.arith.i32.add(a, b) etc.

Notes

  • clz, ctz, popcnt are O(N) bit-by-bit loops. For large N=64 (i64), this is 64 iterations — acceptable for an interpreter but not optimal. Native int.bit_count() (Python 3.10+) could replace popcnt.
  • I.div special-case: assert a != self.min or b != -1 guards against integer overflow on INT_MIN / -1.

6.5 pywasm/opcode.py

File purpose

Flat module of opcode constants and a name dictionary for debug display. Contains every WebAssembly opcode from the 2.0 specification including SIMD (0xfd prefix), saturating truncation (0xfc prefix), and reference types.

Key components

# Basic opcodes (single byte)
unreachable = 0x00
nop         = 0x01
block       = 0x02
...
# Extended opcodes (two-byte with 0xfc prefix, encoded as combined int)
i32_trunc_sat_f32_s = 0xfc00
memory_init         = 0xfc08
...
# SIMD opcodes (0xfd prefix)
v128_load   = 0xfd00
i8x16_shuffle = 0xfd0d
...
i16x8_abs = 0xfd8001   # multi-byte SIMD: 0xfd, LEB128(0x0180) = 0x80 0x03 (note encoding)

Opcode encoding note

The Inst.from_reader method reads multi-byte opcodes by:

  1. Reading the first byte b
  2. If b >= 0xfc, reading the remainder as unsigned LEB128 and combining

The combined value (e.g. 0xfd8001) is used as the dictionary key in opcode.name and in the match statement.

The name dict maps every opcode integer to its human-readable string form (e.g. name[0x6a] = 'i32.add'), used solely for debug output.

Notes

  • The module contains 437 opcode definitions and 437 corresponding name entries.
  • It is purely declarative — no logic, no imports. This makes it very cheap to import.

6.6 pywasm/core.py

File purpose

The central 4193-line module. Contains:

  • All type-descriptor and runtime-instance classes (mirroring the WebAssembly spec)
  • The binary module decoder (ModuleDesc.from_reader)
  • The abstract machine (Machine)
  • The public convenience API (Runtime)

6.6.1 Value system

class ValType

Seven WebAssembly value types encoded as single bytes:

Constant Value Meaning
0x7f i32 32-bit integer
0x7e i64 64-bit integer
0x7d f32 32-bit float
0x7c f64 64-bit float
0x7b v128 128-bit SIMD vector
0x70 ref.func Function reference
0x6f ref.extern External reference

Factory class methods: ValType.i32(), ValType.f32(), etc.

class ValInst

A 16-byte tagged value. The data field is always exactly 16 bytes.

Layout for i32 value 42:
  bytes  0-3:  42 in little-endian (0x2a 0x00 0x00 0x00)
  bytes  4-15: zeros

Layout for ref (func index 7, non-null):
  bytes  0-3:  7 in signed 32-bit little-endian
  byte   4:    0x01  (non-null flag)
  bytes  5-15: zeros

Layout for v128:
  bytes  0-15: raw 16 bytes of the vector

Construction methods (class methods): from_i32, from_i64, from_u32, from_u64, from_f32, from_f64, from_v128, from_v128_i8from_v128_f64, from_ref, from_all.

Extraction methods (instance methods): into_i32, into_i64, into_u32, into_u64, into_f32, into_f64, into_v128, into_v128_i8into_v128_f64, into_ref, into_all.

The from_all(type, n) and into_all() dispatch on type.data to call the appropriate typed constructor/extractor.

6.6.2 Type descriptors (static / decoded from binary)

Class Purpose
Bype Block type — empty (0x40), single value type, or function type index
Inst A single decoded instruction: opcode + args list
Expr A list of Inst terminated by end
LocalsDesc n × type pair for local variable declaration
LocalsInst Runtime mutable list of ValInst locals
FuncType args: list[ValType], rets: list[ValType]
FuncDesc Type index + locals + body Expr
Limits n (min), m (max, 0 = unlimited)
MemType Wraps Limits
TableType ValType + Limits
GlobalType ValType + mutability flag (0=const, 1=mut)
GlobalDesc GlobalType + initializer Expr
Import Module name, item name, kind, descriptor
Extern Runtime external value: kind + store address
ExportDesc Name, kind (func/table/mem/global), descriptor index
ElemDesc Element segment (8 kinds per spec, parsed accordingly)
DataDesc Data segment (3 kinds per spec)
Custom Custom section (ignored by semantics)
ModuleDesc All decoded sections aggregated

6.6.3 Inst.from_reader — instruction decoder

This is a 350-line match statement that reads one instruction at a time from a BytesIO reader. For structured control instructions (block, loop, if), it recursively reads until end (or else for if), building a tree of nested instruction lists.

For memory instructions it reads alignment+offset immediate pairs and asserts the alignment is within spec bounds.

For SIMD instructions it handles lane indices, immediate bytes (shuffle), and vector constants.

f32/f64 constant encoding note:

case opcode.f32_const:
    # Python misinterprets 0x7fa00000 as 0x7fe00000 when cast to float.
    o.args.append(struct.unpack('<i', r.read(4))[0])  # store as raw int

Float constants are stored as their raw bit-pattern (signed int32/int64) to avoid Python's NaN-canonicalization. They are reinterpreted at execution time via ValInst.from_f32_u32.

6.6.4 ModuleDesc.from_reader — binary module decoder

Reads the Wasm binary format:

  1. Checks magic \x00asm and version \x01\x00\x00\x00
  2. Loops up to 32 sections, reading each as a BytesIO sub-reader
  3. Dispatches on section ID (0–12)
  4. The code section (0x0a) is special: it is a count of function bodies, each sized with its own LEB128 length. The code body for func[i] patches locals and expr into the already-created FuncDesc from the function section (0x03).

6.6.5 Runtime instances

Class Description
ModuleInst Per-module index tables (type, func, tabl, mems, glob, elem, data, exps)
FuncInst Wasm-defined function: type, module context, code
FuncHost Python-hosted function: type, callable
MemInst Linear memory: bytearray data + page count; grow(n) adds 64KiB pages
TableInst Table: list[ValInst] of references; grow(n, v) appends n copies of value v
GlobalInst Global: data: ValInst + mutability flag
ElemInst Element segment: type + list[ValInst] references
DataInst Data segment: bytearray

MemInst memory limits:
The memory_grow instruction is additionally capped to 1024 pages (64 MiB):

cnda = size + incr <= 1024   # hard cap
cndb = mems.type.limits.m == 0 or size + incr <= mems.type.limits.m

This is a deliberate embedder policy, not a spec requirement.

6.6.6 Machine.allocate — resource allocation

Maps ModuleDescModuleInst by allocating all resources into the Store:

  1. External values (from imports) are appended to module index tables
  2. store.allocate_func_wasm(inst, func) → creates FuncInst, returns store address
  3. store.allocate_table, allocate_memory, allocate_global, allocate_elem, allocate_data similarly
  4. Export descriptors are resolved to ExportInst entries

6.6.7 Machine.instance — module instantiation

The full spec-defined instantiation procedure:

  1. Validate that import list length and kinds match declared imports
  2. Type-check each import against the declared descriptor
  3. Push an auxiliary Frame to allow global/element init expressions to run (they can reference imports)
  4. Evaluate each global initializer expression
  5. Evaluate each element initializer expression
  6. Call allocate() to build the real ModuleInst
  7. Re-push frame with the real module
  8. Run active element segments (table_init + elem_drop)
  9. Run declarative element segments (elem_drop)
  10. Run active data segments (memory_init + data_drop)
  11. Call start function if declared
  12. Pop frame and return ModuleInst

6.6.8 Machine.invocate — function invocation

Public entry point for calling an exported function:

  1. Validate argument types
  2. Push a sentinel Frame (to detect over-unwinding)
  3. Build locals from args + zero-initialized local variables
  4. Push the real Frame and an initial Label(carry=3) with the function body
  5. Call evaluate()
  6. Pop return values from value stack
  7. Validate return types
  8. Pop sentinel frame; assert stacks are empty

6.6.9 Machine.evaluate — the dispatch loop

The heart of the interpreter. Approximately 2300 lines of a single match instr.opcode: statement inside a for loop. Notable implementation patterns:

Memory instructions:

def evaluate_mem_load(self, offset: int, size: int) -> bytearray:
    mems = self.store.mems[self.stack.frame[-1].module.mems[0]]
    addr = self.stack.value.pop().into_u32() + offset
    assert addr + size <= len(mems.data)    # trap on OOB
    return mems.data[addr:addr+size]

All loads share this helper. Stores have evaluate_mem_save.

Branch handling (evaluate_br):

def evaluate_br(self, l: int) -> None:
    label = self.stack.label[-1 - l]       # target label (depth l)
    rets = [self.stack.value.pop() for _ in range(label.arity)][::-1]
    self.stack.frame = self.stack.frame[:label.frame]  # unwind frames
    self.stack.label = self.stack.label[:len(self.stack.label)-l]
    match label.carry & 1:
        case 0x00: label.index = 0    # loop: reset to start
        case 0x01: self.stack.label.pop()  # block: exit label
    self.stack.value = self.stack.value[:label.value]
    self.stack.value.extend(rets)

SIMD instructions — all follow the same pattern: pop v128 operand(s) as typed lists, apply element-wise operation, push result:

case opcode.i8x16_add:
    b = self.stack.value.pop().into_v128_i8()
    a = self.stack.value.pop().into_v128_i8()
    c = [pywasm.arith.i8.add(a[i], b[i]) for i in range(16)]
    self.stack.value.append(ValInst.from_v128_i8(c))

Float abs/neg are implemented directly via bit manipulation to correctly handle NaN sign bits:

case opcode.f32_abs:
    a = self.stack.value.pop()
    b = ValInst(ValType.f32(), a.data.copy())
    b.data[3] = b.data[3] & 0x7f    # clear sign bit
    self.stack.value.append(b)

6.6.10 class Runtime

The public API facade. Owns one Machine and an imports dict:

class Runtime:
    machine: Machine
    imports: dict[str, dict[str, Extern]]   # module_name → {item_name → Extern}
Method Description
allocate_func_host(type, func) Register a Python callable as a host function
allocate_table(type) Pre-allocate a table for import
allocate_memory(type) Pre-allocate a memory for import
allocate_global(type, data) Pre-allocate a global for import
exported_memory(module, name) Look up an exported MemInst
exported_global(module, name) Look up an exported GlobalInst
instance(module) Instantiate from a ModuleDesc; resolves imports from self.imports
instance_from_file(path) Open file → ModuleDesc.from_readerinstance()
invocate(module, func, args) Call an exported function by name; auto-converts Python values to ValInst

Host function calling convention:

# Signature required for all host functions:
def my_func(machine: pywasm.core.Machine, args: list[int|float|...]) -> list[int|float|...]:
    ...

The Machine reference gives host functions full access to memory, globals, and the call stack.


6.7 pywasm/__main__.py

File purpose

CLI entry point, invoked as python -m pywasm.

Key components

Arguments:
  file              wasm file path (required)
  --version, -v     print version and exit
  --func            exported function name to call (default: _start)
  --func-args       repeated; argument values as strings
  --wasi            'preview1' to enable WASI
  --wasi-envs       'KEY=VAL' pairs
  --wasi-dirs       'wasm_path:host_path' directory mappings
  --wasi-args       argv for the WASI program

Internal logic

Two code paths:

  1. main_wasi()--wasi preview1 is present:

    • Creates a Runtime and a Preview1 object
    • Calls wasi.bind(runtime) to register host functions
    • Calls runtime.instance_from_file(file)
    • Calls wasi.main(runtime, inst) to call _start
    • sys.exit(code) with the process exit code
  2. main_wasm() — plain Wasm:

    • Creates Runtime, instantiates the module
    • Resolves the named export
    • Parses --func-args strings into typed Python values (int, float, or hex bytearray for v128)
    • Calls runtime.invocate(...) and prints the result list

Notes

  • The script runs main_wasm() unconditionally after the WASI check — if --wasi is set, wasi.main() eventually calls sys.exit() (or raises an exception that propagates), so main_wasm() is never reached in practice when --wasi is active. This is technically a control-flow quirk — it relies on side-effects from main_wasi() to stop execution.

6.8 pywasm/wasi.py

File purpose

Complete implementation of the WASI Preview 1 ABI as a Python class. Provides 45 syscall-like host functions that Wasm programs compiled against wasm32-wasip1 will call.

Important: Platform restriction

wasi.py uses fcntl (POSIX) and dir_fd= arguments to os functions. This module is only imported on Linux and macOS. On Windows, WASI support is not available.

Key components

class Preview1.File (dataclass)

Represents an open file descriptor with both host-side and WASI-side metadata:

Field Description
host_fd OS file descriptor integer
host_flag OS flags (O_RDONLY, O_APPEND, etc.)
host_name Absolute path on the host filesystem
host_status FILE_STATUS_OPENED or FILE_STATUS_CLOSED
pipe Optional io.BytesIO for redirecting reads/writes (e.g. stdout capture)
wasm_fd WASI file descriptor number (index into self.fd)
wasm_flag WASI fdflags
wasm_name Path as seen by the Wasm program
wasm_rights_base Bitmask of allowed operations
wasm_rights_root Bitmask of inheritable rights
wasm_status WASI fd status
wasm_type WASI file type (regular, directory, socket, etc.)
Preview1.__init__

Pre-populates self.fd with:

  • Index 0: stdin (FD_STDIN)
  • Index 1: stdout (FD_STDOUT)
  • Index 2: stderr (FD_STDERR)
  • Indices 3+: pre-opened directories from the dirs mapping

The pipe field on stdout/stderr allows capturing output into a BytesIO object (see wasi_stdout.py example).

Preview1.bind(runtime)

Registers all 45 WASI functions as host functions in runtime.imports['wasi_snapshot_preview1']. Each function gets an explicit FuncType descriptor with the exact WASI-specified parameter and return types.

WASI function categories
Category Functions
Args/Env args_get, args_sizes_get, environ_get, environ_sizes_get
Clock clock_res_get, clock_time_get
File descriptor fd_advise, fd_allocate, fd_close, fd_datasync, fd_fdstat_get, fd_fdstat_set_flags, fd_fdstat_set_rights, fd_filestat_get, fd_filestat_set_size, fd_filestat_set_times, fd_pread, fd_prestat_get, fd_prestat_dir_name, fd_pwrite, fd_read, fd_readdir, fd_renumber, fd_seek, fd_sync, fd_tell, fd_write
Path path_create_directory, path_filestat_get, path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, path_rename, path_symlink, path_unlink_file
Poll poll_oneoff
Process proc_exit, proc_raise
Random random_get
Scheduler sched_yield
Socket sock_accept, sock_recv, sock_send, sock_shutdown
I/O implementation pattern

WASI read/write functions use the iovec (scatter/gather I/O) model:

Memory layout of an iovec array:
  +0:  ptr  (u32) → address of data buffer
  +4:  len  (u32) → length of data buffer
  (repeat for each vector element)

fd_write and fd_read iterate over these, calling os.write(host_fd, data) or os.read(host_fd, n). If file.pipe is set, I/O goes to/from the BytesIO object instead of the OS.

proc_exit behavior
def proc_exit(self, _, args):
    if self.return_on_exit:
        raise Exception(SystemExit, args[0])   # caught by main()
    sys.exit(args[0])

return_on_exit = 1 by default — this allows the Python program to continue after the Wasm program exits (e.g. to inspect captured stdout). Setting it to 0 causes the Python process to terminate.

Path sandboxing

Every path_* function calls help_escp(wasm_name, user_path) to detect path traversal attempts (e.g. ../../etc/passwd). If the normalized path escapes the pre-opened directory, ERRNO_PERM is returned.

Helper methods
Helper Purpose
help_badf(fd) Returns True if fd is invalid or closed
help_idir(fd) Returns True if fd is a directory (disallowed for file ops)
help_sock(fd) Returns True if fd is NOT a socket (disallowed for socket ops)
help_perm(fd, rights) Returns True if wasm_rights_base & rights == 0
help_escp(root, name) Returns True if name escapes root
help_time(clockid) Maps WASI clock IDs to nanosecond timestamps
help_wasm_type(stat) Maps os.stat_result to WASI file type constants
help_host_open_flag(oflags) Maps WASI open flags to OS O_* flags

6.9 test/spec.py

File purpose

Runs the official WebAssembly specification test suite. Tests are in JSON format (generated from .wast by wast2json), specifying module loads, function invocations, expected return values, and expected traps.

Key components

  • valj(j) — Converts a JSON value descriptor to a ValInst. Handles all types including nan:canonical, nan:arithmetic, and all SIMD lane types.
  • vale(a, b) — Value equality check with NaN propagation and floating-point tolerance (rel_tol=1e-6 via math.isclose). v128 equality also checks float lanes.
  • host(runtime) — Registers the spectest pseudo-module with standard globals, table, memory, and print functions required by spec tests.

Test command types

Type Action
module Load a .wasm file
register Make a module's exports available as imports under an alias
assert_return Call a function and compare result
assert_trap Expect an exception (any Exception)
assert_exhaustion Expect a stack depth limit exception
assert_invalid Skipped (pywasm has no static validator)
assert_malformed Skipped
assert_uninstantiable Expect instantiation failure
assert_unlinkable Expect import resolution failure
action Invoke with no result check

6.10 test/main.py

File purpose

Smoke test that exercises the CLI (python -m pywasm) for a pure-Wasm invocation and a WASI invocation.

call('python -m pywasm --func pi --func-args 7 example/pi/bin/pi.wasm')
call('python -m pywasm --wasi preview1 --wasi-dirs pywasm:pywasm example/wasi_ll/bin/wasi_ll.wasm')

6.11 test/example.py and test/wasi.py

test/example.py runs all Python example scripts. test/wasi.py runs the downloaded WASI test suite binaries through pywasm.


6.12 script/build_wabt.py

Downloads the platform-appropriate WABT release tarball from GitHub (v1.0.41) and extracts it to res/wabt/. Supports macOS, Linux, and Windows. Required because wast2json is needed to compile spec tests.

6.13 script/build_spec.py

Clones the official WebAssembly spec repository (if not already present), checks out a fixed commit (fffc6e12f), and runs wast2json on every .wast file in test/core and test/core/simd, producing JSON + .wasm files for test/spec.py.

6.14 script/build_wasi.py

Downloads the WASI test suite (similar pattern to build_spec.py).

6.15 script/build_example.py

Compiles every Rust crate in example/*/ that doesn't yet have a compiled bin/<name>.wasm. Uses cargo build --release and copies the result to bin/. Also runs wasm2wat to produce a human-readable .wat alongside the binary.


6.16 Example files (example/)

Each example demonstrates a distinct pywasm use case:

File What it demonstrates
fibonacci.py Basic API: Runtime, instance_from_file, invocate
fibonacci_env.py Host function injection: allocate_func_host, runtime.imports['env']
blake2b.py Non-trivial Wasm (crypto hash); output via memory pointer
blake2b_direct.py Return value vs. output pointer patterns
blake2b_iter.py Benchmarking; repeated invocation
blake2b_simd.py SIMD-accelerated variant
pi.py Float-heavy Wasm (f64)
wasi_stdout.py Capturing WASI stdout via pipe = io.BytesIO()
wasi_zen.py Running a WASI binary that prints to stdout
wasi_ll.py WASI with pre-opened directories
wasi_httpbin.py WASI with socket access (HTTP GET)

The Rust source for each example is in example/<name>/src/. Examples without a main.rs are cdylib (library) crates targeting wasm32-unknown-unknown; WASI examples are bin crates targeting wasm32-wasip1.


6.17 pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "pywasm"
version = "2.2.3"
authors = [{ name="Mohanson", email="mohanson@outlook.com" }]
description = "WebAssembly Interpreter by pure Python"
readme = "README.md"
license = { file = "LICENSE" }

Note that python_requires is not set. Any Python 3 version would nominally be accepted by the package metadata, but the code uses match/case (Python 3.10+) and typing.Self (Python 3.11+), so Python ≥ 3.12 is the actual minimum.

6.18 .github/workflows/develop.yaml

Triggered on every push and on pull-request open. Runs on all three platforms (macOS, Ubuntu, Windows) with Python 3.14:

  1. Download WABT
  2. Install pywasm in editable mode
  3. Build spec tests
  4. Build WASI tests
  5. Run test/example.py, test/main.py, test/spec.py, test/wasi.py

7. Execution Flow

7.1 Library usage startup sequence

import pywasm
│
├── arith.py module executed   → creates i8, u8, …, f32, f64 singletons
├── leb128.py module executed  → creates u, i singletons
├── log.py module executed     → lvl = 0
├── opcode.py module executed  → 437 constants + name dict
├── core.py module executed    → class definitions only
└── wasi.py imported (Linux/macOS only)

runtime = pywasm.core.Runtime()
│
└── Machine()
    └── Store()
    └── Stack()
    runtime.imports = {}

runtime.instance_from_file('module.wasm')
│
├── open('module.wasm', 'rb')
├── ModuleDesc.from_reader(f)
│   ├── Check magic + version
│   └── Loop over sections (type, import, func, table, mem, global, export, start, elem, code, data)
│       Each section → BytesIO sub-reader → section-specific parsing
│
└── Runtime.instance(module)
    └── Machine.instance(module, extern_list)
        ├── Validate imports (count, kind, type)
        ├── Push aux Frame (for init exprs)
        ├── Evaluate global init exprs  → globin list
        ├── Evaluate elem init exprs    → elemin list
        ├── Pop aux Frame
        ├── Machine.allocate(module, extern, globin, elemin)
        │   ├── store.allocate_func_wasm × N
        │   ├── store.allocate_table × N
        │   ├── store.allocate_memory × N
        │   ├── store.allocate_global × N
        │   ├── store.allocate_elem × N
        │   ├── store.allocate_data × N
        │   └── Build ExportInst entries
        ├── Push newmod Frame
        ├── Run active elem segments
        ├── Run declarative elem segments
        ├── Run active data segments
        ├── Run start function
        └── Pop newmod Frame
        → return ModuleInst

7.2 Function call sequence

runtime.invocate(module, 'fibonacci', [10])
│
├── Look up 'fibonacci' in module.exps → func_addr
├── Get func_inst = store.func[func_addr]
├── Build func_args = [ValInst.from_i32(10)]
└── Machine.invocate(func_addr, func_args)
    ├── Push sentinel Frame
    ├── Build LocalsInst([ValInst.from_i32(10)] + [zeros for locals])
    ├── Push real Frame(module=func.module, locals=..., arity=1)
    ├── Push Label(carry=3, instr=func.code.expr.data)
    └── Machine.evaluate()
        Loop until label stack empty:
            fetch instr from label.instr[label.index++]
            match instr.opcode:
                ... dispatch ...
                case call: push new Frame + Label → recursive wasm call
                case return: unwind frames
                case i32_add: pop 2, push 1
                ...
        ← label stack empty
    Pop return values from value stack
    Pop sentinel Frame
    → return [55]  (fibonacci(10))

7.3 Sequence diagram: WASI fd_write call path

sequenceDiagram
    participant WasmProg as Wasm Program
    participant Eval as evaluate()
    participant FuncHost as FuncHost(fd_write)
    participant Preview1 as Preview1.fd_write()
    participant OS as OS (write syscall)

    WasmProg->>Eval: call fd_write (via import table)
    Eval->>Eval: pop args from value stack
    Eval->>FuncHost: hostcode(machine, [fd, iovs_ptr, iovs_len, nwritten_ptr])
    FuncHost->>Preview1: fd_write(machine, args)
    Preview1->>Preview1: validate fd, permissions
    Preview1->>Preview1: read iovec from machine.store.mems[0].data
    Preview1->>OS: os.write(host_fd, data) or pipe.write(data)
    OS-->>Preview1: bytes_written
    Preview1->>Preview1: put_u32(nwritten_ptr, bytes_written)
    Preview1-->>FuncHost: [ERRNO_SUCCESS]
    FuncHost->>Eval: push ValInst.from_i32(0)
    Eval-->>WasmProg: i32 result on stack

8. API Documentation

8.1 Runtime (primary public interface)

Runtime()

Create a new, isolated WebAssembly runtime.

runtime = pywasm.core.Runtime()

runtime.imports

runtime.imports: dict[str, dict[str, Extern]]

Import registry. Populate before instantiation:

runtime.imports['env'] = {
    'my_func': runtime.allocate_func_host(type, callable),
}

runtime.allocate_func_host(type, func) → Extern

Register a Python callable as a WebAssembly host function.

  • type: FuncType — parameter and return types
  • func: Callable[[Machine, list], list] — implementation
def my_print(machine, args):
    print(f"Wasm says: {args[0]}")
    return []

extern = runtime.allocate_func_host(
    pywasm.core.FuncType([pywasm.core.ValType.i32()], []),
    my_print
)

runtime.allocate_memory(type) → Extern

Pre-allocate a linear memory for export to a Wasm module.

extern = runtime.allocate_memory(
    pywasm.core.MemType(pywasm.core.Limits(1, 4))  # min=1 page, max=4 pages
)

runtime.allocate_global(type, data) → Extern

Pre-allocate a mutable or immutable global.

extern = runtime.allocate_global(
    pywasm.core.GlobalType(pywasm.core.ValType.i32(), 0x01),  # mutable i32
    pywasm.core.ValInst.from_i32(42)
)

runtime.instance_from_file(path) → ModuleInst

Load a .wasm binary and instantiate it. Resolves all imports from runtime.imports.

Raises: AssertionError if magic/version mismatch, if imports are unsatisfied, or if type validation fails.

runtime.invocate(module, func, args) → list

Call an exported function by name.

  • module: ModuleInst — from instance_from_file
  • func: str — exported function name
  • args: list[int|float|bytearray] — argument values (auto-typed via function signature)

Returns: list[int|float|bytearray] of return values.

result = runtime.invocate(m, 'fibonacci', [10])
# result == [55]

runtime.exported_memory(module, name) → MemInst

Get direct access to an exported linear memory.

mem = runtime.exported_memory(m, 'memory')
data = mem.get(ptr, length)      # read bytes
mem.put(ptr, bytearray(...))     # write bytes

runtime.exported_global(module, name) → GlobalInst

Get access to an exported global's current value.

g = runtime.exported_global(m, 'my_global')
print(g.data.into_i32())
g.data = pywasm.core.ValInst.from_i32(99)   # modify mutable global

8.2 Core types used in the API

Type Construction Description
ValType.i32() Class method 32-bit integer type
ValType.i64() Class method 64-bit integer type
ValType.f32() Class method 32-bit float type
ValType.f64() Class method 64-bit float type
ValType.v128() Class method 128-bit SIMD type
FuncType(args, rets) Constructor Function signature
Limits(n, m) Constructor Min/max bounds (m=0 means unlimited)
MemType(limits) Constructor Memory type
TableType(valtype, limits) Constructor Table type
GlobalType(valtype, mut) Constructor Global type (mut: 0=const, 1=mut)
ValInst.from_i32(n) Class method Create i32 value
ValInst.from_f64(n) Class method Create f64 value
ValInst.zero(type) Class method Create zero value of given type

9. Configuration Documentation

9.1 Debug logging

import pywasm
pywasm.log.lvl = 1   # enable verbose debug output

Setting lvl to any non-zero integer enables per-section and per-instruction tracing to stdout. There is no way to redirect this output to a file without monkey-patching pywasm.log.println.

9.2 Memory limit

Machine.evaluate hard-caps memory_grow to 1024 pages (64 MiB). This is not configurable at runtime without modifying core.py:

# core.py:2008
cnda = size + incr <= 1024   # hard cap — change this to increase limit

9.3 Call stack depth

Machine.evaluate_call enforces a 1024-frame limit:

assert len(self.stack.frame) < 1024

This prevents infinite recursion from consuming unbounded memory. Not configurable at runtime.

9.4 WASI return_on_exit

wasi = pywasm.wasi.Preview1(args, dirs, envs)
wasi.return_on_exit = 0   # set to 0 to call sys.exit() instead of raising

Default is 1 (raise an exception). Set to 0 if the Wasm program must cleanly terminate the Python process.

9.5 WASI stdout/stdin capture

import io
wasi.fd[1].pipe = io.BytesIO(bytearray())   # stdout → buffer
wasi.fd[0].pipe = io.BytesIO(b'input data') # stdin ← buffer

9.6 Environment variables and directory mappings (WASI)

wasi = pywasm.wasi.Preview1(
    args=['program.wasm', '--flag'],
    dirs={'/sandbox': '/host/path/to/dir'},  # wasm_path: host_path
    envs={'HOME': '/sandbox', 'LANG': 'C'}
)

10. Build & Deployment

10.1 Installing from PyPI

pip install pywasm

10.2 Installing from source (editable)

git clone https://github.com/libraries/pywasm
cd pywasm
pip install --editable .

10.3 Building a distribution package

pip install build
python -m build          # creates dist/pywasm-2.2.3.tar.gz and dist/pywasm-2.2.3-py3-none-any.whl

The build uses hatchling. The wheel is universal (py3-none-any) because the package is pure Python.

10.4 Compiling Wasm examples (developer only)

Requires Rust with wasm32-unknown-unknown and wasm32-wasip1 targets:

rustup target add wasm32-unknown-unknown wasm32-wasip1
python script/build_wabt.py     # download wabt tools
python script/build_example.py  # compile all Rust examples

10.5 Running tests

python script/build_wabt.py   # one-time
python script/build_spec.py   # clone + compile spec tests
python script/build_wasi.py   # clone WASI test suite

python test/example.py        # run all examples
python test/main.py           # CLI smoke tests
python test/spec.py           # spec conformance (slow)
python test/wasi.py           # WASI conformance

10.6 CI/CD pipeline

GitHub Actions (.github/workflows/develop.yaml):

  • Triggers on every push and PR open
  • Matrix: macOS, Ubuntu, Windows × Python 3.14
  • Steps: checkout → setup-python → build_wabt → install pywasm → build tests → run all tests
  • No deployment step (PyPI publishing is manual)

10.7 Versioning strategy

Version is set in two places:

  1. pyproject.toml: version = "2.2.3"
  2. pywasm/__init__.py: version = '2.2.3'

There is no automated version bumping. A developer must update both files manually before releasing.


11. Security Analysis

11.1 Trust boundaries

Untrusted: .wasm file content
Trusted:   Python host code, WASI pre-opened directory paths

11.2 Sandboxing properties

Memory access: All MemInst.data accesses use Python slice indexing with explicit bounds assertions. Out-of-bounds accesses raise AssertionError (trapped), not segfaults. The interpreter cannot access host memory outside the bytearray.

File system: WASI path_* functions call help_escp to prevent directory traversal. Pre-opened directories define the sandbox boundary. A Wasm program cannot access paths outside pre-opened directories.

Network: Socket operations require an existing socket to be passed in via the fd table. The Wasm program cannot initiate new connections independently; it can only operate on sockets the host has pre-opened.

Process isolation: The Wasm program cannot spawn processes, load shared libraries, or access the Python interpreter's objects directly. The only interaction is through the host-function API.

11.3 Potential vulnerabilities

Risk Severity Notes
assert for bounds checking Medium assert statements are disabled with python -O. Running pywasm optimized disables all safety checks. This would cause incorrect behavior, not necessarily a sandbox escape, but is undesirable.
LEB128 infinite loop Low A malformed .wasm with a missing stop byte in LEB128 data would loop forever. No maximum byte count guard.
Memory limit hard-coded Low 64 MiB cap may be bypassed if running modules that request exactly 1024 pages then rely on unguarded growth.
WASI help_escp reliability Medium The path-escape check uses os.path.normpath and string prefix matching. On case-insensitive filesystems (macOS default, Windows) with mixed-case paths, escapes might be possible.
random.randbytes Low Uses Python's random module (Mersenne Twister), not a cryptographically secure RNG. WASI's random_get is documented to provide "high-quality randomness" — this implementation does not meet that guarantee.
No resource limits Medium A runaway Wasm program can exhaust CPU time and Python heap. No timeout or memory-use limits exist beyond the page cap.

11.4 Authentication / authorization

pywasm has no authentication mechanism. It is a library; access control is the responsibility of the embedding application.


12. Performance Analysis

12.1 Baseline characteristics

pywasm is an interpreter with no JIT compiler. Every WebAssembly instruction is a Python function call (dictionary lookup + match case). Rough order-of-magnitude:

  • Native Wasmtime: ~1 ns/instruction
  • pywasm on CPython: ~1 µs/instruction (1000× slower)
  • pywasm on PyPy: 100 ns/instruction (10× faster than CPython)

12.2 Primary bottlenecks

Bottleneck Explanation
match instr.opcode dispatch Python's structural pattern matching is not as fast as a switch table. Each instruction requires traversing the match cases.
ValInst allocation Most instructions create a new ValInst (16-byte bytearray + type). This generates significant GC pressure.
SIMD list comprehensions Each SIMD instruction converts the 16-byte bytearray to a Python list, operates element-wise, and re-packs. This is 16–32 Python object operations per SIMD instruction.
arith.clz/ctz/popcnt Pure Python bit loops for O(64) iterations.
Debug mode overhead pywasm.log.lvl = 1 calls f-string formatting and print() for every instruction.

12.3 Scalability

pywasm is single-threaded. There is no support for threading or asyncio. Multiple modules can share a single Store (as demonstrated by the spec test runner which loads many modules into one Runtime), but execution is sequential.

12.4 Optimization opportunities

Opportunity Expected gain
Use int.bit_count() for popcnt Replaces O(N) loop with native method
Cache label.instr[label.index] in a local Reduces attribute lookups per iteration
Pre-compute instruction list as a flat array of tuples Reduce Inst object overhead
Compile frequently-called functions to Python bytecode (JIT) The "JIT-friendly" design comment suggests this is intended
Pool ValInst objects (flyweight for common values) Reduce allocation/GC pressure
Use memoryview for memory slices Avoid bytearray copy in evaluate_mem_load

13. Developer Guide

13.1 Prerequisites

Tool Version Purpose
Python ≥ 3.12 Runtime and development
Rust + cargo stable Compile example .wasm binaries
wasm32 targets rustup target add wasm32-unknown-unknown wasm32-wasip1
WABT (auto-downloaded) 1.0.41 wast2json for spec tests
Git any Clone spec and WASI test repos

13.2 First-time setup

# 1. Clone
git clone <repo>
cd pywasm

# 2. Install (editable)
pip install --editable .

# 3. Download dev tooling
python script/build_wabt.py

# 4. Optional: compile Rust examples (requires Rust)
python script/build_example.py

# 5. Run basic tests (no spec tests needed)
python test/example.py
python test/main.py

13.3 Running a quick smoke test

import pywasm
runtime = pywasm.core.Runtime()
m = runtime.instance_from_file('example/fibonacci/bin/fibonacci.wasm')
r = runtime.invocate(m, 'fibonacci', [10])
assert r == [55], r
print("OK")

13.4 Enabling debug tracing

import pywasm
pywasm.log.lvl = 1
runtime = pywasm.core.Runtime()
m = runtime.instance_from_file('example/fibonacci/bin/fibonacci.wasm')
r = runtime.invocate(m, 'fibonacci', [3])
# Output:
# 2026/06/07 12:00:00 pywasm: section type [i32] -> [i32]
# 2026/06/07 12:00:00 pywasm: invocate 0 [i32 3]
# 2026/06/07 12:00:00 pywasm:      local.get 0
# 2026/06/07 12:00:00 pywasm:      i32.const 1
# ...

13.5 Injecting a host function

import pywasm

def my_log(machine: pywasm.core.Machine, args: list) -> list:
    print(f"[WASM LOG] value={args[0]}")
    return []

runtime = pywasm.core.Runtime()
runtime.imports['env'] = {
    'log': runtime.allocate_func_host(
        pywasm.core.FuncType([pywasm.core.ValType.i32()], []),
        my_log
    )
}
m = runtime.instance_from_file('my_module.wasm')
runtime.invocate(m, 'run', [])

The corresponding Rust code would declare the import:

extern "C" { fn log(value: i32); }

13.6 Reading / writing linear memory

mem = runtime.exported_memory(m, 'memory')

# Read a null-terminated C string at address 1000
ptr = 1000
end = mem.data.index(0, ptr)
s = mem.data[ptr:end].decode('utf-8')

# Write 4 bytes at address 2000
mem.put(2000, bytearray([0x01, 0x02, 0x03, 0x04]))

13.7 Adding a new opcode

  1. Add the constant to opcode.py:

    my_new_op = 0xXX
    name[my_new_op] = 'my_new.op'
    
  2. Add decode logic in Inst.from_reader in core.py (if it has immediates):

    case pywasm.opcode.my_new_op:
        o.args.append(pywasm.leb128.u.decode_reader(r)[0])  # if it has an arg
    
  3. Add execution logic in Machine.evaluate in core.py:

    case pywasm.opcode.my_new_op:
        a = self.stack.value.pop().into_i32()
        b = ValInst.from_i32(my_operation(a))
        self.stack.value.append(b)
    

13.8 Adding a new WASI syscall

  1. Add constant(s) to Preview1 class (errno, flags, etc.)
  2. Write the implementation method:
    def my_syscall(self, m: pywasm.core.Machine, args: list[int]) -> list[int]:
        if self.help_badf(args[0]):
            return [self.ERRNO_BADF]
        # ... implementation ...
        return [self.ERRNO_SUCCESS]
    
  3. Register in Preview1.bind():
    ['my_syscall', [i32, i32], [i32], self.my_syscall],
    

13.9 Running spec tests

python script/build_spec.py   # one-time
python test/spec.py

Individual test files can be isolated by modifying all_test in test/spec.py.

13.10 Debugging a failed spec test

  1. Set pywasm.log.lvl = 1 in test/spec.py (line 8)
  2. Filter all_test to a single file: all_test = ['res/spec/test/core/i32.json']
  3. Filter the JSON commands to a specific test case index

14. Improvement Recommendations

14.1 Architectural improvements

Recommendation Rationale
Separate decoder from core ModuleDesc and all *Desc classes live in core.py alongside the machine. Splitting into a decoder.py would improve navigability.
Introduce a proper exception hierarchy All errors currently use AssertionError (for traps) or bare Exception. A WasmTrap, WasmLinkError, WasiError hierarchy would allow callers to distinguish error types.
Make memory limit configurable The 1024-page hard cap and 1024-frame depth limit should be parameters of Runtime, not magic numbers in core.py.
Thread-safe multi-module execution A threading.Lock around the store's allocation lists would allow concurrent instantiation in multi-threaded embedder scenarios.

14.2 Code quality improvements

Recommendation Rationale
Add python_requires = ">=3.12" to pyproject.toml Prevents confusing failures on older Python.
Synchronize version from one source Use importlib.metadata.version('pywasm') in __init__.py or a version file, to avoid version drift between pyproject.toml and __init__.py.
Replace assert with explicit checks assert is disabled with python -O. Safety-critical bounds checks should be if ... raise ....
Add type hints to wasi.py methods The args: list[int] hint is accurate for most syscalls, but should be `list[int
Separate SIMD opcode handling The 1200+ SIMD cases in evaluate() could be extracted into a evaluate_simd(instr) helper to reduce the size of the main loop.

14.3 Maintainability improvements

Recommendation Rationale
Add docstrings The code relies entirely on inline comments. Docstrings on Runtime, Machine, Store, Preview1 would improve IDE integration.
Introduce a changelog There is no CHANGELOG.md. Contributors cannot see what changed between versions.
Pin WABT version in CI build_wabt.py downloads a hardcoded v1.0.41. This should match the version expected by the spec checkout (fffc6e12f).

14.4 Performance improvements

Recommendation Priority Expected gain
Replace arith.popcnt with int.bit_count() Low 64× speedup for popcount
Pool common ValInst objects (zero, small integers) Medium Reduce GC pressure by ~30%
Represent instructions as (opcode, *args) tuples Medium Smaller objects, fewer attribute lookups
Add optional Cython extension for evaluate() High 5–50× speedup for CPU-bound code
Implement a basic JIT via compile() / exec() Very High Could match PyPy performance on CPython

14.5 Security improvements

Recommendation Rationale
Replace assert with raise for memory OOB python -O disables assert; OOB access would corrupt host memory.
Use os.urandom for random_get Replace random.randbytes with os.urandom to meet the "high-quality randomness" WASI spec requirement.
Add path traversal tests for WASI help_escp logic should be tested against edge cases: symlinks, .. components, Windows paths, Unicode normalization.
Add LEB128 max-length guard Cap LEB128 reads at ceil(64/7) = 10 bytes to prevent infinite loops on malformed input.

Documentation generated by code analysis of pywasm v2.2.3. All line references are based on the repository state at the time of analysis.

Tüm Yazılar