Skip to content

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) = 5
elapsed: 675038 ns

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

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, component
0x08 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 0x2e
0x52 161 core module the embedded 159-byte core (see below)
0xf5 8 alias the imported instance's `now` → a comp. func
0xff 5 canon LOWER wrap that typed func as a core func the
module can call
0x106 53 core instances (2) ① a synthetic instance exporting the lowered
`now`; ② instantiate the core module `with`
① wired to its clocks import
0x13d 31 alias + type reach the instance's memory + declare
func() -> s64
0x152 20 alias core export "cm32p2||now-ns" → in scope
0x168 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() -> i64
0x066 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; end

The 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:

stopwatch/qube.json5
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|

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 front
0x0853 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 instantiated
0x36ff ~1.5K the wiring: instances, aliases, canon lifts/lowers that
assemble the four modules and export wasi:cli/run

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

Any component runtime works. Under Node, jco transpile maps the WASI imports to its shim and the Qube runs as-is:

Terminal window
$ jco transpile qubepods.examples.stopwatch.component.wasm -o out
$ node -e "import('./out/….js').then(m => m.run.run())"
add(2, 3) = 5
elapsed: 675038 ns

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

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