Skip to content

snmsts/cl-wasm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

cl-wasm

Pure Common Lisp WebAssembly runtime. Zero external dependencies.

Passes 100% of the WebAssembly MVP spec tests (24,877/24,877 non-skipped).

Installation

Requires SBCL (tested with 2.6.1). Symlink or copy to your local-projects:

ln -s /path/to/cl-wasm ~/.roswell/lisp/quicklisp/local-projects/cl-wasm
(asdf:load-system :cl-wasm)

Quick Start

(asdf:load-system :cl-wasm)

;; Load and instantiate a .wasm file
(let* ((module (cl-wasm:load-module #P"add.wasm"))
       (instance (cl-wasm:instantiate module)))
  ;; Call an exported function
  (cl-wasm:call-export instance "add" 1 2))
;; => 3

Loading Modules

From a file:

(defvar *module* (cl-wasm:load-module #P"path/to/module.wasm"))

From an in-memory byte vector:

(defvar *module* (cl-wasm:load-module-from-octets bytes))

Both return a wasm-module struct. You can inspect it before instantiation:

(cl-wasm:wasm-module-exports *module*)
(cl-wasm:wasm-module-imports *module*)

Instantiation and Calling Exports

(defvar *instance* (cl-wasm:instantiate *module*))

;; Call an exported function by name
(cl-wasm:call-export *instance* "factorial" 10)
;; => 3628800

;; Arguments are auto-tagged based on the function signature.
;; Return values are auto-untagged.
;; Multiple return values come back as a list.

Get any export (function, memory, table, global):

(cl-wasm:get-export *instance* "memory")
;; => (:memory . #<WASM-MEMORY-INSTANCE ...>)

Memory Access

Read/write linear memory from the CL side:

(let ((mem (cl-wasm:get-instance-memory *instance*)))
  ;; Write bytes
  (cl-wasm:wasm-memory-write-bytes mem #(72 101 108 108 111) 0)

  ;; Read bytes
  (cl-wasm:wasm-memory-read-bytes mem 0 5)
  ;; => #(72 101 108 108 111)

  ;; Write/read strings (UTF-8)
  (cl-wasm:wasm-memory-write-string mem "Hello, Wasm!" 100)
  (cl-wasm:wasm-memory-read-string mem 100 12)
  ;; => "Hello, Wasm!"
  )

WASI Support

Run WASI modules (compiled with wasi_snapshot_preview1):

(multiple-value-bind (instance ctx)
    (cl-wasm:instantiate-with-wasi
     (cl-wasm:load-module #P"hello.wasm")
     :args '("hello" "world")
     :env '(("HOME" . "/home/user")))
  ;; Call _start (WASI entry point)
  (handler-case
      (cl-wasm:call-export instance "_start")
    (cl-wasm:wasi-exit (e)
      (format t "Exit code: ~D~%" (cl-wasm:wasi-exit-code e)))))

Implemented WASI functions: proc_exit, fd_write, fd_read, fd_close, fd_seek, environ_sizes_get, environ_get, args_sizes_get, args_get, fd_prestat_get, fd_prestat_dir_name, clock_time_get, random_get.

ASDF Integration

Use .wasm files directly as ASDF components:

(defsystem :my-app
  :defsystem-depends-on (:cl-wasm)
  :depends-on (:cl-wasm)
  :components
  ((:wasm-module-file "crypto"
    :package :my-crypto
    :wasi t)
   (:file "main")))

This loads crypto.wasm, instantiates it with WASI support, and creates the :my-crypto package with all exports on asdf:load-system.

define-wasm-module Macro

Generate a CL package from a Wasm module's exports:

(cl-wasm:define-wasm-module :my-math #P"math.wasm")

;; Exported Wasm functions become CL functions in the package.
;; "add_two" -> MY-MATH:ADD-TWO
(my-math:add-two 10 20) ;; => 30

Error Handling

All conditions inherit from cl-wasm:wasm-error:

Condition When
wasm-decode-error Binary format parsing failure
wasm-validation-error Module validation failure
wasm-link-error Import resolution / instantiation failure
wasm-trap Runtime trap (unreachable, div-by-zero, OOB, etc.)
wasi-exit proc_exit called (not an error, a condition)
(handler-case
    (cl-wasm:call-export instance "risky_func")
  (cl-wasm:wasm-trap (e)
    (format t "Trap: ~A~%" e)))

Building .wasm Files

From C:

clang --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry \
  -Wl,--export-all -o add.wasm add.c

From C with WASI:

clang --target=wasm32-wasi --sysroot=/usr -o hello.wasm hello.c

From WAT (text format):

wat2wasm add.wat -o add.wasm

Spec Compliance

WebAssembly MVP: 24,877 pass, 0 fail (15 skipped assert_exhaustion tests).

Tested against the official WebAssembly spec test suite using wast2json + JSON runner.

The 15 skipped tests are all assert_exhaustion (stack overflow detection). The interpreter uses CL's native call stack, so infinite Wasm recursion produces a CL-level stack overflow rather than a clean wasm-trap.

Performance

CoreMark 1.0 benchmark (aarch64 Linux, SBCL 2.6.1):

Runtime CoreMark vs Native
Native C (GCC -O2) 35,350 1.00x
wasmtime (JIT) 29,447 0.83x
cl-wasm (compiler) 1,147 0.032x
cl-wasm (interpreter) 72 0.0020x

The compiler is enabled by default. Set cl-wasm:*enable-compiler* to nil before instantiation to use the interpreter. Functions that fail to compile fall back to the interpreter automatically.

Note: Each compiled function is a separate CL closure produced by (compile nil ...). Instantiating many large modules in a single image can consume significant heap. If memory is a concern, disable the compiler for bulk loading.

Architecture

  • Wasm-to-CL compiler -- compiles Wasm functions to native CL closures via (compile nil ...) at instantiation time. Uses a simple-vector stack with fixnum stack pointer, SBCL type declarations for unboxed i32 arithmetic, and CL block/tagbody for Wasm control flow.
  • Tree-walking interpreter -- fallback for functions the compiler cannot handle. Instructions are decoded into struct trees at load time.
  • Full validation -- binary reader performs complete type validation (stack typing, control flow, index bounds) per the spec.
  • No external dependencies -- pure ANSI CL with SBCL-specific workarounds for IEEE 754 edge cases only.

Limitations

  • No call stack depth limit -- deep Wasm recursion exhausts the CL stack instead of trapping cleanly.
  • No streaming/incremental loading -- the entire .wasm file must be in memory before decoding begins.
  • No WAT text format reader -- load-module only accepts binary .wasm. Use wat2wasm (wabt) to convert.

Post-MVP Proposals

None of these are currently implemented: fixed-width SIMD, memory64, threads/atomics, exception handling, tail calls, multi-memory, GC, component model.

Potential Improvements

  • Register allocation -- reduce stack manipulation overhead with virtual registers
  • Inlining of small Wasm functions -- eliminate call overhead for leaf functions
  • Direct memory access -- SAP-based memory operations to replace byte-at-a-time access

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors