Skip to content

Repository files navigation

rulisp

Write Rust, call it from Common Lisp. rulisp is the missing PyO3/rustler-style bridge for CL: annotate plain Rust with a couple of macros, build a cdylib, and load it from the REPL as an idiomatic Lisp package — CLOS handles, typed conditions, GC finalizers, restarts, and live reload included.

use rulisp::prelude::*;
use std::sync::Mutex;

#[rulisp::handle]
pub struct WordBag { words: Mutex<Vec<String>> }

#[rulisp::export]
impl WordBag {
    #[rulisp(constructor)]
    pub fn new() -> WordBag { WordBag { words: Mutex::new(Vec::new()) } }

    pub fn add(&self, word: &str) -> Result<(), Error> {
        if word.is_empty() { return Err(Error::msg("empty word not allowed")); }
        self.words.lock().unwrap().push(word.to_owned());
        Ok(())
    }

    pub fn len(&self) -> u64 { self.words.lock().unwrap().len() as u64 }
}

#[rulisp::export]
pub fn greet(name: &str) -> String { format!("Hello, {name}!") }

rulisp::module! {
    name: "wordbag",
    handles: [WordBag],
    fns: [greet, WordBag::new, WordBag::add, WordBag::len],
}
CL-USER> (asdf:load-system :rulisp)
CL-USER> (rulisp:use-crate #p"~/src/wordbag/")   ; cargo build + load
#<RULISP:CRATE "wordbag" gen 1 abi 1 :: 4 fns, 1 handle, package WORDBAG>

CL-USER> (wordbag:greet "리스퍼")
"Hello, 리스퍼!"

CL-USER> (defvar *bag* (wordbag:make-word-bag))
#<WORDBAG:WORD-BAG live gen 1 {100A3C2E13}>

CL-USER> (wordbag:word-bag-add *bag* "hello")
CL-USER> (wordbag:word-bag-len *bag*)
1

;; edit Rust, rebuild, reload — without restarting the image:
CL-USER> (rulisp:use-crate #p"~/src/wordbag/")
#<RULISP:CRATE "wordbag" gen 2 ...>

How it works

The macros generate extern "C" shims (panic-catching, status codes, (ptr,len) UTF-8 strings, opaque handles) plus an s-expression manifest embedded in the cdylib. The CL side dlopens a unique copy of the library, reads the manifest, and generates wrappers at load time — defuns, CLOS handle classes, typed conditions from your Rust error types, and trivial-garbage finalizers. No C headers, no hand-written FFI on either side, and the bindings are derived by construction from the exact library that was just loaded.

The hard problems are handled structurally, not by convention:

  • Panics become rulisp:rust-panic conditions; panic = "abort" builds fail to compile.
  • Handles are gated by a state machine (in-flight counting + deferred free): a second free is refused (idempotent, returns NIL), use-after-free and stale handles after reload or image restore signal named conditions, and a free racing an in-flight call is deferred until that call returns — memory is never corrupted.
  • Callbacks are borrowed, same-thread, lifetime-pinned (!Send): storing one is a compile error. A Lisp condition tunnels through Rust (destructors run) and re-signals as the same object.
  • Live reload never calls dlclose and always dlopens a fresh copy — the reload cannot lie.
  • Image dump/restore (save-lisp-and-die) invalidates every pre-dump handle via a session counter and regenerates all bindings on startup.

Start here: docs/installation.md (Linux/macOS/Windows setup, dependencies), then docs/quickstart.md — wrapping the real regex crate (the finished example is examples/rx/). docs/usage.md explains the two ways to consume rulisp — running a prebuilt glue library (no Rust toolchain needed) vs building your own. For something bigger, examples/wasm/ gives Common Lisp a WebAssembly runtime in under 250 lines of glue: load .wat/.wasm modules from the REPL, call their exports under a fuel-metered CPU budget (runaway guest code traps as a condition instead of hanging the image), move byte buffers in and out of the guest's linear memory (bounds-checked; out-of-bounds is a condition), wire host functions so GUEST code calls straight into your Lisp closures (stored callbacks; a condition in the closure becomes a guest trap), and watch wasm traps arrive as Lisp conditions (built on the signal-handler-free wasmi interpreter — see BOUNDARY.md §7 for why that matters). examples/fetch/ is an async HTTPS client on tokio and rustls: the pattern for a crate that owns threads (capped waits, no signal handlers, a declared dump hook), with its own suite in CI. The full contract is in BOUNDARY.md; architecture and rationale in DESIGN.md (Korean). Measured boundary costs, with the method: docs/benchmarks.md. What is stable, what 1.0 will promise, and how hosts are supported: docs/stability.md. Release history: CHANGELOG.md. Reporting a vulnerability and the threat model: SECURITY.md.

Status

0.5.0. A host is supported exactly when it is a required CI job (docs/stability.md §5); this table is that matrix, and nothing else is claimed:

Host Linux x86-64 macOS arm64 Windows x86-64 Linux aarch64
SBCL 2.1.11+ required required required best-effort
Clozure CL 1.13 required
ECL 21.2.1 required

Every required job runs the full gate (races, nested callbacks, reload, the 10k-op fuzzers, and dump/restore where the host dumps images); SBCL and CCL on Linux also run the async HTTPS example, and the ECL job builds and runs a program-op executable. ECL notes: a C toolchain is required for callbacks (rulisp natively compiles trampolines to dodge an upstream GC bug we root-caused — docs/upstream/ecl-dynamic-callback-gc.md), and foreign-thread stored callbacks are unsupported there.

Versions up to 0.2.1 have a soundness hole (issue #1: an explicit 'static in an export signature could retain a Lisp-owned buffer past the call) and are yanked — depend on 0.3.0 or later.

Requirements: a Rust toolchain (cargo), CFFI-capable Lisp, Quicklisp (deps: cffi, babel, trivial-garbage, bordeaux-threads).

make test-m4    # full gate: cargo tests + every suite but fetch, on SBCL
make test-fetch # the async HTTPS example (hermetic)
make test-ccl   # the m4 suites on Clozure CL

What crosses the boundary

Scalars, bool, &str/String, &[u8]/Vec<u8> byte buffers, Option<T> (NIL ↔ None; Option<bool> is refused), &[scalar]/ Vec<scalar> vectors, opaque handles (&self methods, constructors), borrowed same-thread callbacks and stored any-thread callbacks (StoredCallback + rulisp:callback tokens — fail-safe lifetime, verified cross-thread on SBCL and CCL), Result errors → typed conditions, prebuilt-blob loading (load-blob-crate, no Rust toolchain needed), live reload, image dump/restore on SBCL and CCL (ECL has no image dump — ship a program-op executable, docs/distribution.md), use-value/retry-build restarts. The closed type vocabulary, token by token, is BOUNDARY.md §11; every claim on this page has a citation in docs/claims.md, the claims register.

Out — see ROADMAP.md: Vec<String>/nested containers, multiple return values.

rulisp is for writing glue crates — it does not auto-bind arbitrary existing crates, by design (neither does PyO3).

License

MIT

About

Write Rust, call it from Common Lisp — the missing PyO3/rustler-style bridge

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages