Skip to main content

The three linkage modes

Caps ship as separate wasm components and are dynamically loaded at runtime by default. Two other modes exist for when you want to embed caps into a custom pylon.wasm, or when you're targeting a runtime without component-model support.

Pylon's design center. Caps ride the plan JSON as content-addressed digests; composectl fetches them from the blob store on first use and stitches them into the running graph.

The C ext calls linker.resolve_by_id("<name>") at PyInit_<module>, gets an instance handle, and routes every call through instance.invoke(method, payload). method is a short ASCII string; payload is opaque bytes (typically CBOR-encoded for non-trivial signatures — see the native-package conventions).

The host trust-gates the resolve and instantiates the provider in its own Component-Model Store. Adding a new provider does not require rebuilding pylon.wasm; swap the entry in the plan JSON, land the blob in the store, restart.

Reference: ADR-001: dynlink-first architecture. Example: _crc32c_dynlink_cap.

Embedded via wac plug (custom pylon builds)

wac plug python.wasm <cap>.wasm ... > pylon.wasm

The cap is baked into pylon.wasm at compose-time. First-instantiation cost is amortized — opening a SQLite database is a normal function call, no blob fetch.

The stock pylon.wasm distribution ships with several caps pre-embedded for convenience: users don't have to stage blobs to get a working demo. But those choices are not architectural — they're a packaging convenience. If you want a leaner pylon.wasm (drop caps you don't need) or a fatter one (embed a cap you use everywhere), scripts/compose-python-component.sh accepts WITH_*=0 env flags per cap, and you can produce your own custom pylon.

Use wac plug embedding for: hot-path caps where invoke overhead dominates (large math workloads, tight compression inner loops), and for caps that hold C pointer / handle types across calls (SQLite prepared statements, arrow arrays) because those cross the WIT boundary as resource types with nominal-per-component identity that today's dynlink invoke path can't preserve.

Static-C in-cap (MicroPython static profile only)

The cap's C source is folded directly into the CPython/MP compile line — no WIT, no component, no wac plug. Ships as a flat wasi-p1 .wasm, suitable for wamr / iwasm runtimes that do not implement the component model.

Use for: MicroPython static builds targeting wamr.

Choosing between them

QuestionRuntime dynlinkEmbedded via wac plugStatic-C in-cap
Swap providers without rebuilding pylon?YesNoNo
Hot path — invoke every microsecond?AvoidYesYes
Holds C handles across calls (prepared stmt, etc.)?No*YesYes
Vendor library > ~1 MB?AvoidYesAvoid
Target runtime supports the component model?RequiredRequiredN/A

* Handles are possible over dynlink but require app-level session tokens in the CBOR envelope. Ergonomics are much worse than nominal resource types on the WIT boundary.