(rontolisp) docs
← Reference

WASM host functions (rontolisp.wasm)

rontolisp.wasm declares what crosses between a compiled WebAssembly module and its host: defimport binds a var to a function the host provides, and export -- or a defn's {:wasm/export ...} metadata -- hands a var to the host. Both lower to the directives of the WASM host boundary guide (rontolisp:wasm-import, rontolisp:wasm-export), so a Clojure module and a Common Lisp module making the same declarations compile to the same bytes. Require it like clojure.string.

NameExampleResult
rontolisp.wasm/defimport(defimport add {:from "host" :params [:int :int] :returns :int})add calls the host's add
rontolisp.wasm/export(export add10 {:params [:int] :returns :int})the module exports add10
:wasm/export metadata(defn add10 {:wasm/export {:params [:int] :returns :int}} [n] ...)the module exports add10
$ cat main.clj
(ns main (:require [rontolisp.wasm :as wasm]))

(wasm/defimport add {:from "host" :params [:int :int] :returns :int})

(defn add10 {:wasm/export {:params [:int] :returns :int}} [n]
  (add n 10))
$ rontolisp main.clj -o main.wasm --no-wasi
$ rontolisp host.clj -o host.wasm --no-wasi     # a module exporting "add"
$ wasmtime run --preload host=host.wasm --invoke add10 main.wasm 32
42

On the interpreter and the JVM an export is an ordinary function and an import has no host to call:

42
add is a host function (rontolisp.wasm/defimport): only a compiled WASM module can call it

Types

The type keywords are the boundary's: :int, :long, :s8 ... :u64, :float, :bool, :string, :s-expr, :bytes, :octets and :extern (the last two imports only) and, for a result, :void -- what a declaration without :returns means. A value Clojure spells differently from the boundary is converted on the way across:

TypeTo the hostFrom the host
:boolfalse and nil cross as falsefalse arrives as false, not nil
:s-exprthe text pr-str printsthe value read-string reads
:bytesa byte array's octets; anything else throws ClassCastExceptiona byte array
:octetsas :bytesa byte array

So vectors, maps, keywords and false round-trip through :s-expr, which crosses as the same UTF-8 text a :string does on every host. A declaration with none of these types lowers to exactly the directive a Common Lisp source would write. :async is refused: the future a suspending crossing answers is no Clojure future yet.

:bytes transfers raw octets, so ff crosses as ff where a :string would be decoded as UTF-8. As a result it is the caller's buffer: an import answering :bytes takes, after its declared parameters, the byte array the host fills, and answers the full length of the value -- a length past the array's size means the array was too small. An export answering :bytes answers a byte array, which its host reads into the buffer it passes:

$ cat bin.clj
(ns bin (:require [rontolisp.wasm :as wasm]))

(wasm/defimport read-chunk {:from "host" :as "readChunk" :params [:int] :returns :bytes})

(defn checksum {:wasm/export {:params [:bytes] :returns :int}} [data]
  (reduce + (map #(if (neg? %) (+ % 256) %) data)))

(defn first-chunk {:wasm/export {:as "firstChunk" :params [:int] :returns :string}} [id]
  (let [buf (byte-array 4096)
        n (read-chunk id buf)]
    (String. buf 0 (min n 4096) "UTF-8")))
$ rontolisp bin.clj -o bin.wasm --no-wasi --emit-js-glue

Like the Common Lisp :bytes type, it crosses only a WASM core module of the GC backend; on the interpreter and the JVM an import answering :bytes is a stub taking the byte array too.

:octets is the same octets as a value: an import answering :octets answers a byte array holding exactly the octets the host returned, with no buffer to pass. It is what rontolisp.wit declares a WIT list<u8> as on a WASM core module.

Where it runs

Targetdefimportexport
WASM core module (-o out.wasm, --no-wasi)an import of the modulean export of the module
--componentrefused by rontolisp:wasm-import: a component calls a WIT interface (rontolisp.wit)a typed component export
interpreter, JVMa function throwing UnsupportedOperationExceptionnone; the function stays callable

--emit-js-glue writes the JavaScript half of a --no-wasi module's boundary as it does for a Common Lisp module. A complete pair, with a node driver over the generated file, is examples/clojure/host-boundary/.