The #[export] Macro
The #[export] attribute (from cljrs_interop) is the zero-boilerplate way to
expose Rust functions to Clojure. Annotating a function with it causes the
function to be registered automatically when a Registry is created — no
explicit call required.
Basic usage
#![allow(unused)]
fn main() {
use cljrs_interop::export;
#[export(ns = "math")]
pub fn add(a: i64, b: i64) -> Result<i64, String> {
Ok(a + b)
}
}
add is now visible in Clojure as math/add as soon as the shared library is
loaded or the AOT binary starts. No init function is required unless you have
other setup to perform (see When an init function is still needed).
Attribute options
| Key | Required | Description |
|---|---|---|
ns | yes | Clojure namespace, e.g. "math" or "my.project". |
name | no | Override the Clojure symbol name. Default: Rust name with _ replaced by -. |
variadic_min | no | Minimum arity for variadic functions (default 0). Only valid when the function takes a single &[Value] parameter. |
Name mapping
Rust function names are converted to Clojure-style kebab-case: every _ becomes
-. Use name = "..." to override:
#![allow(unused)]
fn main() {
#[export(ns = "str.util")]
fn to_upper_case(s: String) -> String { // → str.util/to-upper-case
s.to_uppercase()
}
#[export(ns = "str.util", name = "upper")] // → str.util/upper
fn to_upper_case_v2(s: String) -> String {
s.to_uppercase()
}
}
Supported signatures
Fixed arity
Each parameter must implement FromValue. The parameter count sets the arity
enforced by the runtime. There is no upper limit.
Plain return value — any type implementing IntoValue; wrapped in Ok
automatically:
#![allow(unused)]
fn main() {
#[export(ns = "math")]
pub fn pi() -> f64 {
std::f64::consts::PI
}
}
Result<T, E> return — Err becomes a Clojure exception via E::to_string():
#![allow(unused)]
fn main() {
#[export(ns = "math")]
pub fn safe_sqrt(x: f64) -> Result<f64, String> {
if x < 0.0 {
Err(format!("cannot take sqrt of {x}"))
} else {
Ok(x.sqrt())
}
}
}
No return value — maps to nil:
#![allow(unused)]
fn main() {
#[export(ns = "log")]
pub fn log_info(msg: String) {
eprintln!("[info] {msg}");
}
}
Four or more parameters work identically to two or three:
#![allow(unused)]
fn main() {
#[export(ns = "geom")]
pub fn rect_contains(rx: f64, ry: f64, rw: f64, rh: f64, px: f64, py: f64) -> bool {
px >= rx && px <= rx + rw && py >= ry && py <= ry + rh
}
}
Variadic
For functions that take a variable number of arguments, use a single &[Value]
parameter. Set variadic_min to enforce a minimum argument count:
#![allow(unused)]
fn main() {
use cljrs_interop::{FromValue, IntoValue, export};
use cljrs_value::Value;
#[export(ns = "math", variadic_min = 1)]
pub fn sum(args: &[Value]) -> Result<Value, String> {
let total: i64 = args
.iter()
.map(|v| i64::from_value(v).map_err(|e| e.to_string()))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.sum();
Ok(total.into_value())
}
}
(math/sum 1 2 3 4 5) ; => 15
When an init function is still needed
#[export] handles function registration. An init function is still required when
you need to:
- Call
mark_loadedsorequiretreats a namespace as built-in rather than looking for a file on the source path. - Set namespace aliases or refer bindings via
Registry::env(). - Perform any other startup work beyond defining Clojure-visible functions.
#![allow(unused)]
fn main() {
use cljrs_interop::Registry;
#[no_mangle]
pub extern "C" fn cljrs_init_my_project(registry: *mut Registry) {
let r = unsafe { &mut *registry };
// #[export] functions are already registered — Registry::new ran first.
r.env().mark_loaded("math");
r.env().mark_loaded("log");
}
}
Note: The
*mut Registrypassed to the init function is the sameRegistrycreated by the runtime before calling your function. All#[export]entries are already interned when the init function is invoked.
Mixing #[export] with manual define
Both styles can coexist freely. Use #[export] for straightforward functions
and r.define / wrap_fn* for cases that capture runtime state or need custom
arity logic:
#![allow(unused)]
fn main() {
use cljrs_interop::{Registry, wrap_fn1, export};
use std::sync::{Arc, Mutex};
// Simple stateless function — use #[export].
#[export(ns = "counter")]
pub fn increment(n: i64) -> i64 {
n + 1
}
#[no_mangle]
pub extern "C" fn cljrs_init_my_project(registry: *mut Registry) {
let r = unsafe { &mut *registry };
// Stateful closure that captures a value created at init time.
let total: Arc<Mutex<i64>> = Arc::new(Mutex::new(0));
let t = total.clone();
r.define(
"counter/running-total",
wrap_fn1("running-total", move |n: i64| {
let mut guard = t.lock().unwrap();
*guard += n;
Ok::<i64, String>(*guard)
}),
);
}
}
How it works
#[export] is a proc-macro (in cljrs-export-macro) that leaves the original
function intact and emits an inventory submission:
#![allow(unused)]
fn main() {
// What #[export(ns = "math")] generates alongside your function:
::cljrs_interop::inventory::submit!(::cljrs_interop::ExportEntry {
qualified: "math/add",
make_fn: || {
::cljrs_interop::NativeFn::with_closure(
"math/add",
::cljrs_interop::Arity::Fixed(2),
move |args| {
let __a0 = <i64 as ::cljrs_interop::FromValue>::from_value(&args[0])?;
let __a1 = <i64 as ::cljrs_interop::FromValue>::from_value(&args[1])?;
add(__a0, __a1)
.map(<i64 as ::cljrs_interop::IntoValue>::into_value)
.map_err(|e| ::cljrs_interop::ValueError::Other(e.to_string()))
},
)
},
});
}
inventory uses linker constructors to collect all submissions across the
binary at link time. Registry::new then iterates them and calls
Registry::define for each one.
If you need to register exports into a Registry you constructed outside the
normal runtime flow (e.g. in tests), call register_exports directly:
#![allow(unused)]
fn main() {
use cljrs_interop::{register_exports, Registry};
let r = Registry::new(env.clone()); // already auto-registered
// …or, if you have a registry from elsewhere:
register_exports(&r);
}