Skip to content

Build universal Python wheels from C/C++ via WebAssembly — define once with WIT, get both native and WASM wheels with identical Python APIs

Notifications You must be signed in to change notification settings

alanfischer/wit-wheel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wit-wheel

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.

Install

pip install wit-wheel

Prerequisites

For WASM wheels:

  • WASI SDK — set WASI_SDK_PATH or place at ~/wasi-sdk
  • wit-bindgencargo install wit-bindgen-cli

For native wheels:

  • A C/C++ compiler (clang or gcc)

Quick start

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 ... --both

Use 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.

CLI reference

wit-wheel build

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

wit-wheel init

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 . --bindings

How it works

WASM build path:

  1. wit-bindgen cpp generates C++ guest stubs from the WIT file
  2. bindings.toml generates resource headers that replace the wit-bindgen skeletons
  3. WASI SDK compiles everything to a .wasm component
  4. wit-wheel generates a Python wrapper using wasmtime and packages it as a py3-none-any wheel

Native build path:

  1. wit-wheel generates a C bridge (flattened C ABI) and a Python ctypes wrapper
  2. bindings.toml generates the same resource headers
  3. The system compiler builds a shared library (.dylib / .so)
  4. wit-wheel packages it as a platform-specific wheel

Both paths produce the same Python API — the wheel is a drop-in replacement.

Import functions (host callbacks)

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_;
"""

bindings.toml reference

Records

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 fields

Resources

Map 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 members

Methods

Expression 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.

Examples

Adder (simple functions)

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 adder

Hop physics (resources + callbacks)

Full 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" --both

Development

git 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

About

Build universal Python wheels from C/C++ via WebAssembly — define once with WIT, get both native and WASM wheels with identical Python APIs

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published