Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 cljrs_init is required unless you have other setup to perform (see When cljrs_init is still needed).

Attribute options

KeyRequiredDescription
nsyesClojure namespace, e.g. "math" or "my.project".
namenoOverride the Clojure symbol name. Default: Rust name with _ replaced by -.
variadic_minnoMinimum 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> returnErr 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 cljrs_init is still needed

#[export] handles function registration. A cljrs_init is still required when you need to:

  • Call mark_loaded so require treats 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(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 Registry passed to cljrs_init is the same Registry created by the runtime before calling your function. All #[export] entries are already interned when cljrs_init 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(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);
}