Rust inside Common Lisp
I started a pet project that combines Rust (for performance and its robust library ecosystem) with Common Lisp (for its interactivity and Condition System), aiming to get the best of both worlds. This setup allows me to build core functionality in Rust and still benefit from the interactive development features and flexibility of Lisp.
Below, you’ll find a brief explanation of how to expose Rust functions and pointers to Common Lisp, and how to set up callbacks from Lisp back to Rust.
Rust Side
To share a Rust function with Common Lisp, we mark it with #[no_mangle]
and specify the C ABI by using extern "C"
. That way, we can safely call it from Lisp. Here’s an example of exposing a Rust function that moves an editor’s cursor up:
#[no_mangle]
pub extern "C" fn move_cursor_up(editor_ptr: *mut Editor) {
let editor: &mut Editor = unsafe { &mut *editor_ptr };
editor.cursor.move_up(&editor.text);
}
In this function, the pointer editor_ptr
is passed in from Lisp. We convert it into a mutable reference (using unsafe { &mut *editor_ptr }
) and then invoke move_up
on the editor’s cursor. The unsafe
block is required because we’re manually dereferencing a raw pointer.
Below is another example that shows how we can run the editor and call back into Lisp when needed. Notice that we have optional callback parameters in the function signature:
fn run_editor(
path: PathBuf,
on_initiate: Option<extern "C" fn(*mut Editor) -> c_void>,
on_key_pressed: Option<extern "C" fn(*const c_char, *const c_char) -> c_void>,
) -> i32 {
// ... some code
let mut editor = Editor::new(text, Some(path), on_key_pressed);
if let Some(callback) = on_initiate {
callback(&mut editor);
}
let result = editor.run();
// ... some code
match result {
Ok(_) => 0,
Err(err) => {
eprintln!("Editor error: {err}");
1
}
}
}
Here, we receive an optional callback function pointer (on_initiate
) which, if present, will be called with a pointer to the Editor
. This mechanism can be useful for setting up or initializing data from Common Lisp before the editor runs. We can also call other callbacks (like on_key_pressed
) from within Rust whenever a key is pressed, passing any necessary data back to Lisp.
Common Lisp Side
For the Lisp side, we can use CFFI to interact with Rust. CFFI allows us to define foreign functions, callbacks, and work directly with pointers. Here’s a minimal example:
;; some code
(defcfun "c_run_editor" :int
(path :string)
(on-initiate :pointer)
(on-key-pressed :pointer))
(defcfun "move_cursor_up" :void
(editor :pointer))
(defcallback on-initiate :void ((editor :pointer))
(setq *editor* editor))
defcfun "c_run_editor"
defines a foreign function namedc_run_editor
that we can call from Lisp. It takes a string (the path) and two pointers (the callbacks).defcfun "move_cursor_up"
defines another foreign function that corresponds to the Rust function we exposed earlier.defcallback on-initiate
creates a callback function in Lisp that matches the signature expected by Rust. Here, we simply capture the editor pointer in a global Lisp variable called*editor*
. This pointer can then be used elsewhere in the Lisp code to interact with the same editor object that Rust is operating on.
By combining these pieces, we can call into Rust for performance-critical tasks or to leverage Rust libraries, while still using Lisp’s REPL-driven workflow and powerful Condition System for interactive development and rapid iteration. This hybrid approach gives us the best of both ecosystems.