Skip to content

Latest commit

 

History

History
162 lines (123 loc) · 6.85 KB

File metadata and controls

162 lines (123 loc) · 6.85 KB

ClojureWasm

ClojureWasm

A JVM-free Clojure runtime in Zig, with a WebAssembly FFI.

CI Zig 0.16.0 Clojure runtime License: EPL 2.0 GitHub Sponsors

Note

ClojureWasm is stable and released regularly; the current line is 1.x (see Releases). It is built by a very small team, so replies can take a while — but Issues and Pull Requests are open. The single most useful thing you can report is Clojure code that behaves differently here than on the JVM; there is an issue template for exactly that. Questions and general discussion are welcome in Discussions.

What it is

ClojureWasm is a Clojure runtime written from scratch in Zig and Clojure, with no JVM. It builds to a small native binary (arm64 / amd64) that starts in milliseconds. Its main feature is a WebAssembly FFI: from your Clojure code you can load a module compiled from another language — Rust, Go, Zig, C — and call it like an ordinary function. The idea is to stay in the Clojure world and still use what other languages have already built.

Features

  • Small and quick to start — one static binary of about 7.4 MB with the Wasm JIT engine included (for scale: babashka's native binary is ~71 MB ; measured comparison), starting in ~6 ms, which suits short-lived, start-and-stop workloads (CLI tools, serverless, scripts).
  • A lot of everyday Clojure runsclojure.core plus a growing set of standard-library namespaces (clojure.string / set / walk / zip / edn / data.json / data.csv / math / pprint / test / tools.cli …).
  • A CIDER-compatible nREPLcljw nrepl and connect your editor to evaluate real Clojure live.
  • WebAssembly as an FFI(wasm/load "mod.wasm") then (wasm/call m "fn" …): a sandboxed module from any language, called like a namespace. The FFI runs JIT-compiled by default (the embedded zwasm engine), so a hot loop inside a module executes as native code.
  • WebAssembly components as namespaces(:require ["comp.wasm" :as c]) pulls a WIT-typed component in like a library; its exports become ordinary Vars, with arguments and results as plain Clojure data (see below).
  • Single-binary buildscljw build script.clj -o app compiles your program (and the runtime) into one self-contained executable.

Install

Homebrew (macOS arm64 / Linux x86_64):

brew install clojurewasm/tap/cljw

The cljw binary is not code-signed. Homebrew installs it without a Gatekeeper prompt on most setups; if macOS still blocks it as coming from an unidentified developer, clear the quarantine flag once:

xattr -d com.apple.quarantine "$(which cljw)"

Or grab a binary straight from the Releases page. Building from source is in Quickstart below.

Quickstart

Build the optimized, Wasm-enabled binary (needs Zig 0.16 — direnv allow loads it via Nix, or nix develop):

zig build -Dwasm -Doptimize=ReleaseSafe   # → ./zig-out/bin/cljw

Then (the examples assume cljw is on your PATH):

# Call a WebAssembly module compiled from another language, like a function
cljw -e '(wasm/call (wasm/load "docs/examples/wasm/add.wasm") "add" 40 2)'   # => 42

# Evaluate an expression
cljw -e '(->> (range) (filter even?) (take 5))'     # => (0 2 4 6 8)

# Run a file
cljw script.clj

# A REPL — and an nREPL for CIDER / your editor
cljw
cljw nrepl --port 7888

# Compile a program to a single self-contained native binary
cljw build script.clj -o app

Require a Wasm component like a namespace

The headline feature. A WebAssembly component — a .wasm that carries its own WIT type signature — can be :require-d like a Clojure library. Its exports become ordinary Vars, and because the component describes its own types, arguments and return values are plain Clojure data with no hand-written glue.

Build a component from any language that targets the component model (here Rust, cargo build --target wasm32-wasip2), then require the .wasm by name:

(ns my.app
  (:require ["typed_payload.wasm" :as tp]))   ; required like a lib

(tp/process {:xs [3 4 5] :label "data"})
;; => {:xs [3 4 5 12], :label "data!"}

The WIT record arrived as a map, list as a vector, string as a string — and the parameter name survives as metadata, so the export reads like a normal fn:

(:arglists (meta #'tp/process))   ;; => ([input])

JVM Clojure calls Java and ClojureScript calls JavaScript; here the host is a language-neutral .wasm, so a function from Rust, Go, or C is indistinguishable from a Clojure one.

Try it live

  • Playgroundhttps://cw-playground.fly.dev — run Clojure in your browser, evaluated in-process by cljw; call sandboxed Rust / Go WebAssembly modules over the FFI. (source)
  • Bookshelf demohttps://cw-serverless-demo.fly.dev — a small multi-user bookshelf served end-to-end by cljw's own HTTP server, with SQLite and book-cover colours running in-process through the WebAssembly FFI, no JVM. (source)

Documentation

License

Eclipse Public License 2.0 — see LICENSE and NOTICE.


Developed in spare time alongside a day job. Sponsorship via GitHub Sponsors is welcome and helps keep work going.