Skip to content

Wasm, WASI, WIT & the q64 component

Every component Qube you deploy to qubepods is a WebAssembly component compiled from q64 source. This page is for the technically curious: what the layers underneath are (Wasm, WASI, WIT, the component model), how the q64 compiler produces a component, and — because nothing demystifies a format like reading it byte by byte — a complete tear-down of the smallest component we could build: one function, a + b, 115 bytes.

WebAssembly is a portable binary instruction format: a small, stack-based virtual machine with linear memory, designed to run at near-native speed inside a sandbox. A .wasm file is a module — functions, a memory, and a list of exports — with no ambient access to anything. It cannot open a file, read a clock, or make a network request unless the host explicitly hands it a function to call. That deny-by-default posture is why qubepods can run untrusted tenant code on shared infrastructure: the sandbox is the format, not an add-on.

A pure module can compute, but real programs need capabilities — storage, clocks, randomness, sockets. WASI (WebAssembly System Interface) is the standardized vocabulary for those: instead of one big POSIX-ish syscall layer, it is a family of small, focused interfaces — wasi:keyvalue, wasi:blobstore, wasi:config, wasi:clocks, and so on. A module doesn’t link these; it imports them, and the host decides — per instance — what implementation (if any) to supply. On qubepods, the platform is that host: when your manifest binds a key-value store, the runtime satisfies the qube’s wasi:keyvalue/store import with a store pinned to your project’s identity. The code never names a namespace, database, or bucket.

WIT (WebAssembly Interface Types) is the IDL of this world: a small, readable language for describing interfaces — functions with named, typed parameters, using real types (string, list<u8>, record, result, s64) rather than raw integers. A world is a WIT document that describes one component’s complete boundary: everything it imports, everything it exports. Here is a real one — the whole contract of the component we dissect below:

package qubepods-examples:adder;
world adder {
// imports — derived capability set
// (none — pure surface)
// exports — public surface
export add: func(a: s64, b: s64) -> s64;
}

You can audit a component from its world alone: this one imports nothing, so it provably cannot touch storage, network, or clocks — the strongest sandbox statement there is, checkable before a single instruction runs.

A core Wasm module speaks only in machine types (i32, i64, f32, f64) and raw memory. The component model wraps one or more core modules into a component: a self-describing unit whose boundary is typed in WIT terms. The wrapper’s job is the canonical ABI — the fixed recipe for lifting core values into interface types (an i64 becomes an s64; a string becomes a pointer + length that the host reads out of the module’s memory) and lowering them back on the way in. Components compose: one component’s typed import can be satisfied by another’s typed export, without either seeing the other’s memory.

q64 targets this stack directly rather than through a C-style toolchain:

  • A qube compiles to a component. qube build --component emits the core module and the component that wraps it, plus the synthesized .wit world. The world is named from the qube’s identity in qube.json5 (wit.package, wit.world), not from a filename.
  • Capabilities are inferred, not declared in code. q64 code reaches the outside world only through envenv.kv, env.blob, env.config, env.out. Each env face lowers to a WASI-family import in the emitted component (env.blobq64:blob/store, env.configwasi:config/store, …). Use no faces and the compiler derives an empty capability set — the “pure surface” you saw in the world above.
  • The surface is the contract. A library qube’s pub fns become the world’s exports, with their q64 names and types carried through (i64 ⇄ WIT s64).

The capitalization is not decoration; it names the two kinds of thing that live in this ecosystem:

  • A qube (lowercase) is a library: type: "library" in its manifest, no main. It exports a surface — a WIT world — for other qubes to link against. The adder below is a qube.
  • A Qube (uppercase) is a deployment artifact: type: "application", has a main, runnable. It’s what you ship with qube deploy and what gets a *.qubepod.app URL on qubepods.
  • qubes (plural, lowercase) is the generic noun for either kind.

So: you publish a qube to the Continuum; you deploy a Qube to qubepods. (The CLI is always lowercase monospace: qube.) Qube names are snake_case dotted segments — qubepods.examples.adder — never hyphenated.

The Continuum — and how qubes bind together into a Qube

Section titled “The Continuum — and how qubes bind together into a Qube”

The Continuum is the qube registry. Humans browse it at continuum.q64.dev; the qube CLI talks to its API at qubes.q64.dev. qube publish packs a qube’s archive, computes its content address, and uploads it together with its synthesized WIT world — the contract consumers resolve against.

A Qube is assembled from qubes through the manifest’s dependencies block. Keys are full qube names (which double as module paths in code); values are version ranges resolved through the Continuum, or local paths during development:

dependencies: {
"dev.q64.math": "^0.1", // resolved via the Continuum
"dev.example.llm": { path: "../llm" }, // local, while you develop both
}

In source, the dependency is just a module:

import dev.q64.math.{add}
fn main {
env.out(add(2, 3))
}

Underneath, this is component-model composition: the library qube’s component exports add: func(a: s64, b: s64) -> s64, the consuming Qube imports that same typed interface, and the linker satisfies one with the other — types checked at the WIT boundary, no shared memory between them. Resolved versions are pinned in qube.lock.

Two properties make this more than a package manager:

  • Capabilities compose visibly. Every published qube carries its compiler-derived capability set (a capabilities field the toolchain rewrites on publish, backed by a Wasm custom section — hand-edits are corrected, and the registry cross-checks uploads). A Qube’s effective capability set is the closure over everything it links: pull in a qube that reaches the network and your Qube’s contract says so. adder’s contribution to any closure is exactly nothing.
  • Remote qubes bind the same way. A dependency can also resolve to a running qube’s wRPC address instead of linked code — the call site looks the same, but crossing the wire is tracked as an effect (@wire), so even distribution is visible in the contract.

The example is adder in the qubits repo — a library qube whose entire source is:

pub fn add(a: i64, b: i64) -> i64 { a + b }

and a manifest that names it and asks for a component:

{
name: "qubepods.examples.adder",
type: "library",
entry: "src/lib.q",
component: { emit: true },
wit: { package: "qubepods-examples:adder", world: "adder", path: "synthesized" },
}

We compiled it with the prebuilt q64/qube binaries from the q64 GitHub release (v0.0.3, CLI 0.0.1 pre-alpha), checksums verified against the release’s SHA256SUMS — you never build the compiler from source just to build a qube. One command:

Terminal window
qube build --component
# → target/debug/wasm64/qubepods.examples.adder.wasm (core module)
# → target/debug/wasm64/qubepods.examples.adder.component.wasm (component)
# → target/debug/wasm64/qubepods.examples.adder.wit (synthesized world)

The artifact sizes:

Artifact Size
Core module 56 bytes
Component 115 bytes
WIT world (text) 236 bytes

Yes — the human-readable contract is bigger than either binary.

A Wasm binary is a fixed 8-byte header followed by numbered sections, each id, size, payload, with all integers in LEB128 (one byte while the value fits in 7 bits — everything here does). The complete file:

offset bytes meaning
────── ───────────────────── ────────────────────────────────────────────
0x00 00 61 73 6d magic: "\0asm"
0x04 01 00 00 00 version 1 — a core wasm module
── Type section ──
0x08 01 07 section id 1, size 7
0x0A 01 1 type
0x0B 60 function type
0x0C 02 7e 7e 2 params: i64, i64 (0x7e = i64)
0x0F 01 7e 1 result: i64 → (i64,i64)→i64
── Function section ──
0x11 03 02 section id 3, size 2
0x13 01 00 1 function; func 0 has type 0
── Memory section ──
0x15 05 04 section id 5, size 4
0x17 01 1 memory
0x18 05 01 01 flags: has-max | memory64; min=1, max=1 page
── Export section ──
0x1B 07 10 section id 7, size 16
0x1D 02 2 exports
0x1E 06 6d 65 6d 6f 72 79 name "memory" (len 6)
0x25 02 00 kind 2 = memory, index 0
0x27 03 61 64 64 name "add" (len 3)
0x2B 00 00 kind 0 = function, index 0
── Code section ──
0x2D 0a 09 section id 10, size 9
0x2F 01 07 1 body, 7 bytes
0x31 00 0 local declarations
0x32 20 00 local.get 0 (a)
0x34 20 01 local.get 1 (b)
0x36 7c i64.add ← the entire program
0x37 0b end

The whole function is four instructions — 20 00 20 01 7c 0b — and the return is implicit (a Wasm function returns whatever is on the stack at end). Note the limits flag 0x05 at offset 0x18: the memory64 bit is set, because this is a wasm64 build (--addr wasm64, the q64 default).

The component has the same \0asm magic but version bytes 0d 00 01 00 — version 13, layer 1 (layer 0 means core module). It then embeds the entire 56-byte core module verbatim and wraps it in 59 bytes of envelope:

offset bytes meaning
────── ───────────────────── ────────────────────────────────────────────
0x00 00 61 73 6d magic
0x04 0d 00 01 00 version 13, layer 1 = component
── Core module section ──
0x08 01 38 section id 1, size 0x38 = 56
0x0A…0x41 the 56-byte core module above, byte for byte
── Core instance section ──
0x42 02 04 section id 2, size 4
0x44 01 00 00 00 1 instance: instantiate module 0, 0 imports
── Alias section ──
0x48 06 09 section id 6, size 9
0x4A 01 1 alias
0x4B 00 00 sort: core function
0x4D 01 00 target: core-instance 0's export…
0x4F 03 61 64 64 …named "add"
── Type section ──
0x53 07 0b section id 7, size 11
0x55 01 40 1 type: function
0x57 02 2 named params
0x58 01 61 78 "a": s64 (0x78 = s64)
0x5B 01 62 78 "b": s64
0x5E 00 78 result: s64 → func(a,b) -> s64
── Canon section ──
0x60 08 06 section id 8, size 6
0x62 01 00 00 1 definition: canon lift
0x65 00 00 00 core func 0, 0 options, type 0
── Export section ──
0x68 0b 09 section id 11, size 9
0x6A 01 1 export
0x6B 00 03 61 64 64 name "add"
0x70 01 00 00 sort function, index 0, no type ascription

Read as a sentence: embed the core module, instantiate it, alias its add export, lift that through the canonical ABI as func(a: s64, b: s64) -> s64, and export the result as add. The canon lift has zero options — scalars cross the boundary in registers, so no memory or realloc needs to be named. Every arithmetic byte in the file is still the single 7c (i64.add) at offset 0x36 inside the embedded module. And this tiny wrapper is the same machinery that scales up: a real qube’s component differs only in having more aliases, imports to satisfy (wasi:keyvalue, …), and richer types to lift.

Almost. add never touches linear memory, yet the module declares one page and exports it — hand-writing the true minimum gives a 41-byte module that instantiates and computes identically:

Structure Compiler Minimum Delta
Header + type + function + code 32 B 32 B 0
Memory section (1 page, min = max) 6 B +6
Export section 18 B 9 B +9 (the "memory" entry)
Total 56 B 41 B +15

Everything else is at the floor: no name/custom sections, every LEB128 length a single byte, canonical section order, and no shorter encoding of a + b exists. The 15 bytes are a deliberate uniformity trade-off, not waste: the canonical ABI requires a memory (plus a realloc) the moment anything non-scalar — a string, a list, a record — crosses the boundary. A pure-scalar surface is the special case, so q64 emits every module with its memory, capped at min = max = 1 page. Cost: 15 bytes in the file and one 64 KiB page at instantiation.

The core module runs in any Wasm runtime — in Node, i64 crosses the JS boundary as BigInt:

import { readFileSync } from "node:fs";
const bytes = readFileSync("target/debug/wasm64/qubepods.examples.adder.wasm");
const { instance } = await WebAssembly.instantiate(bytes, {});
// The `n` suffix is a JS BigInt literal — a type marker, not a unit.
// Wasm i64 maps to BigInt at the JS boundary, because a plain Number
// (an IEEE-754 double) only holds integers exactly up to 2^53 - 1;
// passing a bare `2` here would throw a TypeError.
instance.exports.add(2n, 3n); // 5n
instance.exports.add(1000000000000n, 2345n); // 1000000002345n

No import object — {} — because the world imports nothing. That is the whole q64 + Wasm story in one line: the contract said this code needs no capabilities, and the empty braces prove it.

Next: Binding qubes into a Qube — a second qube that imports the monotonic clock, both bound into a runnable Qube that times an add() call, and the emitted command component mapped byte by byte.