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.
Runtime dynlink via compose:dynlink/linker (default)
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
| Question | Runtime dynlink | Embedded via wac plug | Static-C in-cap |
|---|---|---|---|
| Swap providers without rebuilding pylon? | Yes | No | No |
| Hot path — invoke every microsecond? | Avoid | Yes | Yes |
| Holds C handles across calls (prepared stmt, etc.)? | No* | Yes | Yes |
| Vendor library > ~1 MB? | Avoid | Yes | Avoid |
| Target runtime supports the component model? | Required | Required | N/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.