Build universal Python wheels from C/C++ libraries via WebAssembly.
wit-wheel takes a WIT interface definition and a C/C++ library and produces a pip-installable Python wheel — either a universal WASM wheel (py3-none-any) that runs anywhere via wasmtime, or a native wheel for the current platform.
pip install wit-wheelFor WASM wheels:
- WASI SDK — set
WASI_SDK_PATHor place at~/wasi-sdk - wit-bindgen —
cargo install wit-bindgen-cli
For native wheels:
- A C/C++ compiler (clang or gcc)
You need two files to wrap a C++ library:
1. A WIT file describing the interface:
// physics.wit
package my:physics;
interface types {
record vec3 { x: f32, y: f32, z: f32 }
resource simulator {
constructor();
set-gravity: func(g: vec3);
update: func(dt-ms: s32);
add-sphere: func(mass: f32, radius: f32) -> solid;
}
resource solid {
set-position: func(pos: vec3);
get-position: func() -> vec3;
}
}
world my-physics {
export types;
}2. A bindings.toml mapping WIT types to your C++ library:
[includes]
headers = ["<my_physics.h>"]
[records.vec3]
cpp-type = "::my::vec3"
to-cpp = "::my::vec3({x}, {y}, {z})"
from-cpp = "{_expr_}.x, {_expr_}.y, {_expr_}.z"
[resources.simulator]
cpp-type = "::my::simulator"
constructor = "default"
[resources.simulator.methods]
set-gravity = "impl_.set_gravity({g})"
update = "impl_.update({dt_ms})"
[resources.simulator.methods.add-sphere]
body = """
auto s = impl_.create_sphere({mass}, {radius});
return Solid::Owned(new Solid(s));
"""
[resources.solid]
cpp-type = "::my::solid_ptr"
constructor = "Solid(::my::solid_ptr s) : impl_(std::move(s)) {}"
[resources.solid.methods]
set-position = "impl_->set_position({pos})"
get-position = "impl_->get_position()"Build:
# Universal WASM wheel (runs on any OS/arch)
wit-wheel build --wit physics.wit --bindings bindings.toml \
-I /path/to/library/include --pkg-name my-physics --cflags "-std=c++17"
# Native wheel (current platform, no wasmtime dependency)
wit-wheel build --wit physics.wit --bindings bindings.toml \
-I /path/to/library/include --pkg-name my-physics --cflags "-std=c++17" --native
# Both
wit-wheel build ... --bothUse from Python:
from my_physics import Simulator, Vec3
sim = Simulator()
sim.set_gravity(Vec3(0.0, 0.0, -9.81))
ball = sim.add_sphere(1.0, 0.5)
ball.set_position(Vec3(0.0, 0.0, 10.0))
for _ in range(100):
sim.update(10)
print(ball.get_position()) # Vec3(x=0.0, y=0.0, z=5.09)The Python API is identical whether using the WASM or native wheel.
wit-wheel build [OPTIONS]
Options:
--wit PATH WIT interface file (required)
--bindings PATH bindings.toml for C++ resource mapping
--source PATH Individual C/C++ source files (repeatable)
--source-dir PATH Source directories (repeatable)
-I PATH Include directories (repeatable)
--pkg-name NAME Python package name (default: from world name)
--pkg-version VER Package version (default: 0.1.0)
--out-dir PATH Output directory (default: dist)
--world NAME World name (auto-detected if single)
--wasi-sdk PATH WASI SDK path (default: $WASI_SDK_PATH)
--cflags FLAGS Extra compiler flags
--ldflags FLAGS Extra linker flags (native only)
--native Build native wheel only
--both Build both WASM and native wheels
Generate starter files from a WIT definition:
# Generate skeleton C++ headers to fill in
wit-wheel init --wit physics.wit --out-dir src/
# Generate a starter bindings.toml with TODO placeholders
wit-wheel init --wit physics.wit --out-dir . --bindingsWASM build path:
wit-bindgen cppgenerates C++ guest stubs from the WIT filebindings.tomlgenerates resource headers that replace the wit-bindgen skeletons- WASI SDK compiles everything to a
.wasmcomponent - wit-wheel generates a Python wrapper using
wasmtimeand packages it as apy3-none-anywheel
Native build path:
- wit-wheel generates a C bridge (flattened C ABI) and a Python ctypes wrapper
bindings.tomlgenerates the same resource headers- The system compiler builds a shared library (
.dylib/.so) - wit-wheel packages it as a platform-specific wheel
Both paths produce the same Python API — the wheel is a drop-in replacement.
WIT imports let C++ call back into Python:
world my-physics {
import on-collision: func(point: vec3, normal: vec3);
export types;
}from my_physics import Simulator, set_on_collision
def handle_collision(point, normal):
print(f"Hit at {point}")
set_on_collision(handle_collision)Wire the callback in bindings.toml via extra-members:
[resources.simulator]
extra-members = """
struct Listener : public ::my::collision_listener {
void on_collision(const ::my::collision& c) override {
OnCollision(Vec3{c.point.x, c.point.y, c.point.z},
Vec3{c.normal.x, c.normal.y, c.normal.z});
}
} listener_;
"""Map WIT records to C++ types with bidirectional conversion:
[records.vec3]
cpp-type = "::lib::vec3<float>"
to-cpp = "::lib::vec3<float>({x}, {y}, {z})" # WIT fields → C++
from-cpp = "{_expr_}.x, {_expr_}.y, {_expr_}.z" # C++ → WIT fieldsMap WIT resources to C++ classes:
[resources.simulator]
cpp-type = "::lib::simulator"
constructor = "default" # or a custom constructor signature
extra-members = "int counter_ = 0;" # extra class membersExpression form — one-liner, wit-wheel handles the return:
[resources.simulator.methods]
get-gravity = "impl_.get_gravity()"
set-gravity = "impl_.set_gravity({g})"Body form — full control with multi-line C++:
[resources.simulator.methods.add-sphere]
body = """
auto s = impl_.create({mass}, {radius});
return Solid::Owned(new Solid(s));
"""Parameters are substituted as {param_name}. Record parameters are automatically converted using to-cpp.
Minimal example — a single exported function with no resources:
examples/adder/
├── adder.wit
├── adder.cpp
└── test_adder.py
wit-wheel build --wit examples/adder/adder.wit \
--source examples/adder/adder.cpp --pkg-name adderFull example wrapping a header-only C++17 physics library with resources, records, and import callbacks:
examples/hop/
├── hop.wit # WIT interface (simulator, solid, vec3, on-collision)
├── bindings.toml # C++ binding configuration
├── test_hop.py # Integration test
├── demo_3d.py # Pygame 3D bounce room demo
└── hop/ # Git submodule (github.com/alanfischer/hop)
git submodule update --init
wit-wheel build --wit examples/hop/hop.wit \
--bindings examples/hop/bindings.toml \
--source-dir examples/hop \
-I examples/hop/hop/include \
--pkg-name hop-physics --cflags "-std=c++17" --bothgit clone <repo> && cd wit-wheel
git submodule update --init
pip install -e ".[test]"
# Run tests (set WASI_SDK_PATH for WASM integration tests)
WASI_SDK_PATH=~/wasi-sdk pytest tests/ -v