Binding qubes into a Qube
The first technology page dissected the smallest
possible component: adder, one pure function, 115 bytes. This page is about
the next step — composition. We build a second library qube that reaches a
real capability (the clock), then bind both into a runnable Qube whose
main() times a call to add() and prints the result:
add(2, 3) = 5elapsed: 675038 nsAll three live in the qubits repo:
adder,
clock,
stopwatch.
The clock qube — a component with an import
Section titled “The clock qube — a component with an import”qubepods.examples.clock is the capability twin of adder. Same one-function
library shape:
pub fn now_ns() -> i64 { env.time.monotonic_ns() }but this time the world is not empty. The source never mentions WASI — it
calls the env.time face, and the compiler derives the import:
package qubepods-examples:clock;
world clock { import wasi:clocks/monotonic-clock@0.2.0; export now-ns: func() -> s64;}monotonic_ns() is the simplest capability there is: nullary, returns a bare
i64, no allocation — it crosses the component boundary in registers alone
(which is why q64’s spec marks @time realtime-safe). Note the export:
q64’s now_ns becomes WIT’s kebab-case now-ns.
The 693 bytes, section by section
Section titled “The 693 bytes, section by section”qube build --component --addr wasm32 emits a 693-byte component. Where the
pure adder needed only embed → instantiate → alias → lift → export, an
import adds instance types and a canon lower — the mirror of the lift:
offset size section what it says────── ──── ─────────────────── ─────────────────────────────────────────────0x00 8 header 00 61 73 6d 0d 00 01 00 — layer 1, component0x08 33 component type an INSTANCE type: { type instant = u64; export now: func() -> instant }0x29 40 component import "wasi:clocks/monotonic-clock@0.2.0" — the name is right there in the bytes at 0x2e0x52 161 core module the embedded 159-byte core (see below)0xf5 8 alias the imported instance's `now` → a comp. func0xff 5 canon LOWER wrap that typed func as a core func the module can call0x106 53 core instances (2) ① a synthetic instance exporting the lowered `now`; ② instantiate the core module `with` ① wired to its clocks import0x13d 31 alias + type reach the instance's memory + declare func() -> s640x152 20 alias core export "cm32p2||now-ns" → in scope0x168 6 canon LIFT lift it as `func() -> s64`0x170 12 component export export "now-ns"0x17c 283 custom sections names + producers — 41% of the file is debug metadata (strippable)The embedded core module (at 0x52) is 159 bytes and worth reading raw:
0x05d 00 61 73 6d 01 00 00 00 a normal core module, version 1 01 05 01 60 00 01 7e type: func() -> i640x066 02 2e 01 26 63 6d 33 32 … import "cm32p2|wasi:clocks/ monotonic-clock@0.2" "now"0x0ad 0e 63 6d 33 32 70 32 7c 7c export "cm32p2||now-ns" …0x0bb 0a 06 01 04 00 10 00 0b code: ONE function — call 0; endThe whole implementation is call 0 — call the import, return its value.
And note the byte-level quirk: the core import module string says @0.2
while the component import says @0.2.0. Stable interface versions are
mangled by their semver-compatible major.minor in core-module linking; the
full pin lives at the component layer.
Read as a sentence, the component says: given an instance that satisfies
wasi:clocks/monotonic-clock, lower its now to a core function, hand it to
the embedded module as its one import, then lift the module’s now-ns export
back out as func() -> s64. Lower on the way in, lift on the way out — the
canonical ABI in both directions, in 693 bytes.
Binding: dependencies → imports → one core module
Section titled “Binding: dependencies → imports → one core module”The stopwatch Qube’s manifest names its two dependencies. Keys are full qube names, which double as the module paths source code imports:
type: "application",entry: "src/main.q",dependencies: { "qubepods.examples.adder": { path: "../adder" }, "qubepods.examples.clock": { path: "../clock" },}import qubepods.examples.adder.{add}import qubepods.examples.clock.{now_ns}
fn main { let t0 = now_ns() let sum = add(2, 3) let t1 = now_ns() let ns = t1 - t0 env.out("add(2, 3) = {sum}") env.out("elapsed: {ns} ns")}Here the deps are local paths (sibling folders in the repo); a published qube
would carry a version range instead, resolved through the
Continuum. Either way qube build resolves each
dependency to its source and hands everything to one q64 emit call.
What linking actually produces is worth seeing. Disassemble the Qube’s core module and the two library qubes are just… functions:
(import "wasi_snapshot_preview1" "fd_write" (func 0))(import "wasi_snapshot_preview1" "clock_time_get" (func 1))(func 2 …) ;; the i64 → decimal-string formatter (for {sum}/{ns})(func 3 ;; _start — main() call 4 ;; t0 = now_ns() ← the clock qube call 5 ;; sum = add(2, 3) ← the adder qube call 4 ;; t1 = now_ns() i64.sub ;; ns = t1 - t0 … call 2 / call 0 ;; format + fd_write, twice)(func 4 … call 1 …) ;; now_ns — qubepods.examples.clock(func 5 …) ;; add — qubepods.examples.adder(data (i32.const 0) "\0aadd(2, 3) = elapsed: ns")That last line is the string constants — interpolation split
"add(2, 3) = {sum}" into pieces around the formatted number. In the
component binary they sit at offset 0xae1:
000ae1 01 00 41 00 0b 19 0a 61 64 64 28 32 2c 20 33 29 |..A....add(2, 3)|000af1 20 3d 20 65 6c 61 70 73 65 64 3a 20 20 6e 73 | = elapsed: ns|The Qube — a command component, mapped
Section titled “The Qube — a command component, mapped”Because main() prints, the Qube is emitted as a wasi:cli/run command:
the core’s fd_write and clock_time_get preview1 syscalls are lifted by
the vendored WASI adapter into typed WASI 0.2 interfaces. Its derived world —
the capability closure over everything linked:
world stopwatch { import wasi:cli/stdout; // main() prints import wasi:clocks/monotonic-clock; // the clock qube export wasi:cli/run; // it's a runnable command}adder appears nowhere: a pure dependency contributes nothing to the
closure. Pull in a qube that reaches the network and the world would say so —
the contract is audit-grade, derived, and checkable before anything runs.
The component is 19,744 bytes and embeds four core modules:
offset size what─────── ────── ─────────────────────────────────────────────────────────0x000a 0x849 component types + imports + aliases — the typed WASI surface (streams, stdout, clocks…) declared up front0x0853 685 module 0 — OUR CODE: main/add/now_ns/formatter, importing preview1 fd_write + clock_time_get (hex below)0x0b03 10,687 module 1 — the WASI preview1→0.2 ADAPTER (54% of the file): implements fd_write over wasi:io/streams, clock_time_get over monotonic-clock.now, etc.0x34c5 361 module 2 — a shim of 13 indirect trampolines (the adapter and main are mutually recursive: main imports the adapter's syscalls, the adapter imports main's memory — the shim's table breaks the cycle)0x3631 204 module 3 — "fixups": patches the shim's table with the real adapter functions once both are instantiated0x36ff ~1.5K the wiring: instances, aliases, canon lifts/lowers that assemble the four modules and export wasi:cli/runAnd our two syscall imports, raw, at 0x887:
000887 02 16 77 61 73 69 5f 73 6e 61 70 73 68 6f 74 5f |..wasi_snapshot_|000897 70 72 65 76 69 65 77 31 08 66 64 5f 77 72 69 74 |preview1.fd_writ|0008a7 65 00 01 16 77 61 73 69 5f 73 6e 61 70 73 68 6f |e...wasi_snapsho|0008b7 74 5f 70 72 65 76 69 65 77 31 0e 63 6c 6f 63 6b |t_preview1.clock|0008c7 5f 74 69 6d 65 5f 67 65 74 00 02 |_time_get..|The proportions are the honest summary: our three qubes compile to 685 bytes; the other ~19K is the standards machinery (adapter, shim, typed world) that makes those 685 bytes runnable, unmodified, on any component host.
Running it
Section titled “Running it”Any component runtime works. Under Node, jco transpile maps the WASI
imports to its shim and the Qube runs as-is:
$ jco transpile qubepods.examples.stopwatch.component.wasm -o out$ node -e "import('./out/….js').then(m => m.run.run())"add(2, 3) = 5elapsed: 675038 nsTwo trailing letters worth keeping apart. The ns in the output is the
unit: env.time.monotonic_ns() is defined in nanoseconds (as is
wasi:clocks/monotonic-clock’s instant), and the Qube prints the label
itself. The n you’ll see on values like 675038n when driving components
from JavaScript is the type: a BigInt literal suffix, because wasm
i64 maps to JS BigInt. The two are connected here: these values are
BigInts precisely because they are nanosecond counts — a plain JS Number
holds integers exactly only up to 2⁵³ − 1, which is a mere ~104 days of
nanoseconds, so a 64-bit clock reading needs BigInt to arrive intact. In
short: nanoseconds, carried as BigInt — the ns says what the number means,
the n says what can hold it.
Honesty note on the number: ~675µs is the host’s clock-call overhead
under the jco JavaScript shim — each now_ns() crosses a JS resource
boundary, and two of those bracket one wasm i64.add. On a native component
host the same Qube reads the clock in tens of nanoseconds. The measurement
is real either way; what it measures is dominated by the host’s capability
implementation — a nicely observable proof that capabilities are host-owned.
One face, three lowerings
Section titled “One face, three lowerings”Every env.time method compiles to a different import depending on the
artifact being built — the source never changes:
| q64 face | Component (library) | Command (printing app) | Local qube run |
|---|---|---|---|
monotonic_ns() |
monotonic-clock.now |
clock_time_get(1, …) |
env.monotonic_ns |
resolution_ns() |
monotonic-clock.resolution |
clock_res_get(1, …) |
env.resolution_ns |
unix_ns() |
wall-clock.now (datetime → epoch-ns) |
clock_time_get(0, …) |
env.unix_ns |
sleep_ns(ns) — blocking |
subscribe-duration → pollable.block → drop |
poll_oneoff (one clock subscription) |
env.sleep_ns |
Same source, three ABIs — the face is the abstraction, the manifest picks
the artifact. The blocking sleep_ns is worth a note: it’s the first rung
of the async ladder. The component-model async ABI lets a synchronously-
lowered caller of an async operation simply block (the host parks the
task), so a sleeping qube needs no language-level futures — those arrive
with q64’s CPS milestone, when sleep gains its suspending
future<()>-returning form.