rontolisp:wit-export
(rontolisp:wit-export "world.wit" :world name)
Declares that the program implements a WIT world. The world's export items
are checked against the program's top-level defuns at compile time and lowered
into the rontolisp:wasm-export directives they
stand for, so the boundary types are never written by hand and the .wit file
and the compiled component cannot drift apart. The WIT is the single source of
truth: the world is the program's export list (a hand-written
rontolisp:wasm-export alongside it is an error), and the emitted component is
byte-identical to the one those hand-written directives would have produced. It
is a compile-time directive, not an ordinary function: on the interpreter
and JVM backends it runs the same contract check and then returns nil, so
the same source runs on every backend. See
Implementing a WIT World
for the full guide.
Because the directive reads a .wit file from disk, the example is shown
statically:
// wit/greeter.wit
package example:greeter;
world greeter {
/// Greet someone by name.
export greet: func(who: string) -> string;
}
;;; greet.lisp -- the directive comes last: on the interpreter it sees only the
;;; functions defined so far.
(defun greet (who)
(concatenate 'string "Hello, " who "!"))
(rontolisp:wit-export "wit/greeter.wit" :world greeter)
rontolisp greet.lisp --component -o greet.wasm
wasmtime run -W gc=y --invoke 'greet("world")' greet.wasm
# "Hello, world!"
Arguments
- The WIT file path, as a string. A relative path resolves against the directory
of the source file that names it, like
load. :world— the world to implement, as a bare symbol (spelled the way WIT spells it) or a string. It may be omitted when the file declares exactly one world; when the file declares several, one must be named.
Everything else comes from the world: rontolisp:wasm-export's :params,
:param-names, :returns and :async are all filled in from it, so the
defuns carry no boundary types at all.
Supported WIT types
| WIT type | Boundary type | Lisp value |
|---|---|---|
s8 s16 s32 | :s8 :s16 :s32 | an integer |
u8 u16 u32 | :u8 :u16 :u32 | an integer |
s64 u64 | :s64 :u64 | an integer; a u64 value of 2^63 or more traps at the boundary |
f64 | :float | a float |
bool | :bool | t or nil |
string | :string | a string |
| (no result) | :void | the function's value is discarded |
The whole fixed-width integer family crosses, so the canonical component-model tutorial world compiles unedited:
// wit/adder.wit
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
Each type carries its own range exactly, or the call traps — a negative returned
through u32 is refused rather than delivered as 4294967295. The component
model has no integer subtyping, so this is not cosmetic: a component that lifted
u32 as s32 would be rejected against its own world by
wasm-tools component targets, by jco, and by any bindgen-based host.
An async func in the world lifts the export with :async t, so blocking is
always legal inside it: I/O inside a sync export usually works too (the
asynchronous built-ins complete without blocking when the host accepts
immediately), but a host that reports BLOCKED would make it trap, and the
async lift removes that residual risk — the WIT states
which exports are async rather than leaving it to be guessed. Every other WIT
type (record, list, option, result, resources, ...) is a compile error at
the export boundary today; the error names the rontolisp representation the type
is settled to have, once marshalling it lands.
What it checks
Every violation is a compile error naming the WIT file and the line of the offending export:
- an export the world declares with no matching
defun—wit/greeter.wit:5: export 'greet' has no matching (defun greet ...) in the program - an arity mismatch —
wit/greeter.wit:5: export 'greet' declares 1 parameter(s), but (defun greet ...) takes 2(an exported function takes required parameters only:&optional/&rest/&keyare rejected) - a WIT type the export boundary does not carry (the whole fixed-width integer
family plus
f64/bool/stringcrosses; arecord,list, ... does not yet) - an
async funcunder--no-gc --component, whose adapter-free reactor has no async machinery - an export name that is not a component-model label (lower-kebab-case words), a
duplicate export, or the reserved name
run(the component'swasi:cli/runentry point) - a world with no exports, a
:worldthe file does not declare, or an omitted:worldwhen the file declares several
Because the world is the export list, so is mixing it with the hand-written
form: a rontolisp:wasm-export in a program that also has a
rontolisp:wit-export, and a rontolisp:http-handler together with a world (a
serve-mode component exports only wasi:http/handler@0.3.0).
Limitations
- Only the world's export side is a contract.
importitems are ignored (a component's WASI imports come from the build, not from the world), and an inlineimport name: func(...)is rejected rather than silently dropped — the functions a program calls are bound from an interface withrontolisp:wit-import, or declared by hand withrontolisp:wasm-import(both Preview 1 only). The component you get therefore has a much larger type than the world you wrote: the 6-line world above compiles to a 149-line component type (tenwasi:*imports plusexport wasi:cli/run), and callingrontolisp:fetchinsidegreetsilently adds five more.--emit-witis how you see it. - A world exports freestanding functions or an interface defined in the same
file:
export add;referencing an in-fileinterface add { ... }, or an inlineexport ops: interface { ... }, is implemented member by member and produces a realdocs:adder/addinstance export (see Exporting an interface). An export naming an interface the file does not define (a barewasi:*reference) is still an error. :s-exprhas no WIT spelling, so an export carrying an arbitrary s-expression still needs a hand-writtenrontolisp:wasm-export— and therefore a program without a world.- On the interpreter the directive is an ordinary form evaluated in order, so
it sees only the functions defined so far: put it at the end of the file.
(The compile path collects every top-level
defunfirst, so there the position does not matter.) - Adding
--emit-witwrites the component's real type back out, and its export lines reproduce the world handed in, parameter names included — but by construction, not by coincidence: the world is what those lines are derived from, so they cannot disagree with it. Emitting is worth it for the import side, not as a check on your program (that iswit-export's own job, on every backend). Two deliberate differences from the input file: the///doc comments are gone (a component's type does not store them), and the emitted world is alwayspackage root:component; world root.