Rust Interop
clojurust lets Clojure code call Rust functions with full type safety and GC integration. The interop layer has two modes that work together:
- Interpreter mode — the Rust crate is compiled to a shared library
(
.so/.dylib/.dll) and loaded bycljrs run/cljrs replat startup viacljrs build-native. - AOT mode —
cljrs compilestatically links the Rust crate into the generated binary; the native init function is called before any Clojure code runs.
Both modes use the same API: a Registry object that maps Clojure-visible
names to Rust functions.
When to use Rust interop
- Wrapping an existing Rust library (e.g. a database driver, image codec, or systems API) for use from Clojure.
- Hot paths where Clojure performance is insufficient.
- Exposing mutable or OS-level state (file descriptors, sockets, GPU buffers)
as opaque
NativeObjectvalues that participate in protocol dispatch.
Chapter overview
| Page | Contents |
|---|---|
| Project setup | cljrs.edn config, Cargo setup, crate layout |
| Registry API | Registry, wrap_fn*, type marshalling, NativeObject |
The #[export] macro | Zero-boilerplate function registration |
| Interpreter mode | cljrs build-native, auto-loading, hot-reload workflow |
| AOT mode | How cljrs compile wires native init into the binary |
Quick example
cljrs.edn:
{:paths ["src"]
:rust {:crate "."
:init "my_project::cljrs_init"}}
Cargo.toml (user crate):
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cljrs-interop = { path = "/path/to/cljrs/crates/cljrs-interop" }
src/lib.rs:
#![allow(unused)]
fn main() {
use cljrs_interop::{Registry, wrap_fn2};
#[no_mangle]
pub extern "C" fn cljrs_init(registry: *mut Registry) {
let r = unsafe { &mut *registry };
r.define("my.project/add",
wrap_fn2("add", |a: i64, b: i64| Ok::<i64, String>(a + b)));
}
}
src/main.cljrs:
(ns my.project.core
(:require [my.project :as native]))
(println (native/add 3 4)) ; => 7
Workflow:
cljrs build-native # compile lib → target/debug/libmy_project.so
cljrs run src/main.cljrs # auto-loads the .so, then runs Clojure