Overview
emacs-module-rs provides high-level Rust binding and tools to write Emacs’s dynamic modules. It is easy to use if you know either Rust or Emacs.
It currently supports:
- Stable Rust (latest stable recommended; 1.87 minimum).
- Emacs 25 or above, built with module support.
- Linux, macOS, FreeBSD, Windows.
Setting up
- Make sure your Emacs was compiled with module support. Check that
module-file-suffixis notnil, and the functionmodule-loadis defined.- On macOS, the recommended installation method is MacPorts (
emacs-appandemacs-mac-app). - On Windows, use either an official GNU build, or MSYS2’s package:
pacman -S mingw-w64-ucrt-x86_64-emacs.
- On macOS, the recommended installation method is MacPorts (
- Install the Rust toolchain with rustup.
- On Windows, use the
stable-gnutoolchain. Build in the MSYS2 shell that matches the C runtime (CRT) of your Emacs:- MINGW64 (
mingw-w64-x86_64-toolchain) for official GNU builds, which use MSVCRT. - UCRT64 (
mingw-w64-ucrt-x86_64-toolchain) for official-UCRT64builds (Emacs 31+) and MSYS2’s package, which use UCRT.
- MINGW64 (
- On Windows, use the
Notes
- When the optional feature
bindgenis enabled, the raw binding will be generated fromemacs-module.hat build time. You will also need to installclang. (This is recommended only for troubleshooting.) For example, on Windows:# In Powershell scoop install llvm $env:LIBCLANG_PATH = "$(scoop prefix llvm)\bin" cargo build --all
Known Issues
There is a bug (see issue #1) with Emacs 26 on Linux that prevents it from loading any dynamic modules (even those written in C), if:
- Emacs is built without thread support.
- The OS is Ubuntu 16.04 (Xenial).
Hello, Emacs!
Create a new project:
cargo new --lib greeting
cd greeting
cargo add emacs
Add to Cargo.toml:
[lib]
crate-type = ["cdylib"]
Write code in src/lib.rs:
use emacs::{defun, Env, Result, Value};
// Emacs won't load the module without this.
emacs::plugin_is_GPL_compatible!();
// Register the initialization hook that Emacs will call when it loads the module.
#[emacs::module]
fn init(env: &Env) -> Result<Value<'_>> {
env.message("Done loading!")
}
// Define a function callable by Lisp code.
#[defun]
fn say_hello(env: &Env, name: String) -> Result<Value<'_>> {
env.message(format!("Hello, {}!", name))
}
Build the module and create a symlink with .so extension so that Emacs can recognize it:
cargo build
cd target/debug
# If you are on Linux
ln -s libgreeting.so greeting.so
# If you are on macOS
ln -s libgreeting.dylib greeting.so
Add target/debug to your Emacs’s load-path, then load the module:
(add-to-list 'load-path "/path/to/target/debug")
(require 'greeting)
(greeting-say-hello "Emacs")
The minibuffer should display the message Hello, Emacs!.
Declaring a Module
Each dynamic module must have an initialization function, marked by the attribute macro #[emacs::module]. The function’s type must be fn(&Env) -> Result<()>.
In addition, in order to be loadable by Emacs, the module must be declared GPL-compatible.
emacs::plugin_is_GPL_compatible!();
#[emacs::module]
fn init(env: &Env) -> Result<()> {
// This is run when Emacs loads the module.
// More concretely, it is run after all the functions it defines are exported,
// but before `(provide 'feature-name)` is (automatically) called.
Ok(())
}
Options
-
name: By default, the name of the feature provided by the module is the crate’s name (with_replaced by-). There is no need to explicitly callprovideinside the initialization function. This option allows the function’s name, or a string, to be used instead.// Putting `rs` in crate's name is discouraged so we use the function's name // instead. The feature will be `rs-module-helper`. #[emacs::module(name(fn))] fn rs_module_helper(_: &Env) -> Result<()> { Ok(()) } -
defun_prefixandseparator: Function names in Emacs are conventionally prefixed with the feature name followed by-. These 2 options allow a different prefix and separator to be used.// Use `/` as the separator that goes after feature name, like some other packages. #[emacs::module(separator = "/")] fn init(_: &Env) -> Result<()> { Ok(()) }// The whole package contains other Lisp files, so the module is named // `tree-sitter-dyn`. But we want functions to be `tree-sitter-something`, // not `tree-sitter-dyn-something`. #[emacs::module(name = "tree-sitter-dyn", defun_prefix = "tree-sitter")] fn init(_: &Env) -> Result<()> { Ok(()) } -
mod_in_name: Whether to use Rust’smodpath to construct function names. Default totrue. For example, supposed that the crate is namedparser, a#[defun]namednext_childinsidemod cursorwill have the Lisp name ofparser-cursor-next-child. This can also be overridden for each individual function, by an option of the same name on#[defun].
Note: Often time, there’s no initialization logic needed. A future version of this crate will support putting #![emacs::module] on the crate, without having to define a no-op function. See Rust’s issue #54726.
Writing Functions
You can use the attribute macro #[defun] to export Rust functions to the Lisp runtime, so that Lisp code can call them. The exporting process happens when the module is loaded, even if the definitions are inside another function that is never called, or inside a private mod.
Input Parameters
Each parameter must be one of the following:
- An owned value of a type that implements
FromLisp. This is for simple data types that have an equivalent in Lisp./// This docstring will appear in Lisp too! #[defun] fn inc(x: i64) -> Result<i64> { Ok(x + 1) } - A shared/mutable reference. This gives access to data structures that other module functions have created and embedded in the Lisp runtime (through
user-ptrobjects).#[defun] fn stash_pop(repo: &mut git2::Repository) -> Result<()> { repo.stash_pop(0, None)?; Ok(()) } - A Lisp
Value, or one of its “sub-types” (e.g.Vector). This allows holding off the conversion to Rust data structures until necessary, or working with values that don’t have a meaningful representation in Rust, like Lisp lambdas.#[defun] fn maybe_call(lambda: Value) -> Result<()> { if some_hidden_native_logic() { lambda.call([])?; } Ok(()) } #[defun(user_ptr)] fn to_rust_vec_string(input: Vector) -> Result<Vec<String>> { let mut vec = vec![]; for e in input { vec.push(e.into_rust()?); } Ok(vec) } - An
&Env. This enables interaction with the Lisp runtime. It does not appear in the function’s Lisp signature. This is unnecessary if there is already another parameter with typeValue, which allows accessing the runtime throughValue.env.// Note that the function takes an owned `String`, not a reference, which would // have been understood as a `user-ptr` object containing a Rust string. #[defun] fn hello(env: &Env, name: String) -> Result<Value<'_>> { env.message(format!("Hello, {}!", name)) }
Return Value
The return type must be Result<T>, where T is one of the following:
- A type that implements
IntoLisp. This is for simple data types that have an equivalent in Lisp./// Return the path to the .git dir. /// Return `nil' if the given path is not in a repo, /// or if the .git path is not valid utf-8. #[defun] fn dot_git_path(path: String) -> Result<Option<String>> { Ok(git2::Repository::discover(&path).ok().and_then(|repo| { repo.path().to_str().map(|s| s.to_owned()) })) } - An arbitrary type. This allows embedding a native data structure in a
user-ptrobject, for read-write use cases. It requiresuser_ptroption to be specified. If the data is to be shared with background Rust threads,user_ptr(rwlock)oruser_ptr(mutex)must be used instead.#[defun(user_ptr)] fn repo(path: String) -> Result<git2::Repository> { Ok(git2::Repository::discover(&path)?) } - A type that implements
Transfer. This allows embedding a native data structure in auser-ptrobject, for read-only use cases. It requiresuser_ptr(direct)option to be specified. Value, or one of its “sub-types” (e.g.Vector). This is mostly useful for returning an input parameter unchanged.
See Custom Types for more details on embedding Rust data structures in Lisp’s user-ptr objects.
Naming
By default, the function’s Lisp name has the form <feature-prefix>[mod-prefix]<base-name>.
feature-prefixis the feature name followed by-. This can be customized by thename,defun_prefix, andseparatoroptions on#[emacs::module].mod-prefixis constructed from the function’s Rustmodpath (with_and::replaced by-). This can be turned off crate-wide, or for individual function, using the optionmod_in_name.base-nameis the function’s Rust name (with_replaced by-). This can be overridden with the optionname.
Examples:
// Assuming crate's name is `native_parallelism`.
#[emacs::module(separator = "/")]
fn init(_: &Env) -> Result<()> { Ok(()) }
mod shared_state {
mod thread {
use emacs::{defun, Result};
// Ignore the nested mod's.
// (native-parallelism/make-thread "name")
#[defun(mod_in_name = false)]
fn make_thread(name: String) -> Result<String> {
Ok(name)
}
}
mod process {
use emacs::{defun, Result};
// (native-parallelism/shared-state-process-launch "bckgrnd")
#[defun]
fn launch(name: String) -> Result<String> {
Ok(name)
}
// Specify a name explicitly, since Rust identifier cannot contain `:`.
// (native-parallelism/process:pool "http-client" 2 8)
#[defun(mod_in_name = false, name = "process:pool")]
fn pool(name: String, min: i64, max: i64) -> Result<String> {
Ok(format!("{name}: {min}..{max}"))
}
}
}
Documentation
#[defun] converts Rust’s docstring into Lisp’s docstring. It also automatically constructs and appends the function’s signature to the end of the docstring, so that help modes can correctly display it.
// `(fn X Y)` is automatically appended, so you don't have to manually do so.
// In help modes, the signature will be (add X Y).
/// Add 2 numbers.
#[defun]
fn add(x: usize, y: usize) -> Result<usize> {
Ok(x + y)
}
Calling Lisp Functions
Frequently-used Lisp functions are exposed as methods on env:
env.intern("defun")?;
env.message("Hello")?;
env.type_of(5.into_lisp(env)?)?;
env.provide("my-module")?;
env.list((1, "str", true))?;
To call arbitrary Lisp functions, use env.call(func, args).
funccan be:- A string identifying a named function in Lisp.
- Any Lisp-callable
Value(a symbol with a function assigned, a lambda, a subr). This can also be written asfunc.call(args).
argscan be:- An array, or a slice of
Value. - A tuple of different types, each satisfying the
IntoLisptrait.
- An array, or a slice of
// (list "str" 2)
env.call("list", ("str", 2))?;
let list = env.intern("list")?;
// (symbol-function 'list)
let subr = env.call("symbol-function", [list])?;
// (funcall 'list "str" 2)
env.call(list, ("str", 2))?;
// (funcall (symbol-function 'list) "str" 2)
env.call(subr, ("str", 2))?;
subr.call(("str", 2))?; // Like the above, but shorter.
// (add-hook 'text-mode-hook 'variable-pitch-mode)
env.call("add-hook", [
env.intern("text-mode-hook")?,
env.intern("variable-pitch-mode")?,
])?;
#[defun]
fn listify_vec(vector: Vector) -> Result<Value> {
let mut args = vec![];
for e in vector {
args.push(e)
}
vector.value().env.call("list", &args)
}
Caching Symbols and Functions
Every call to env.intern and every symbol-lookup in env.call("name", ...) does a hash-table lookup inside Emacs. For hot paths, cache the result with use_symbols! or use_functions!.
use_symbols!
use_symbols! declares static variables of type &OnceGlobalRef that hold interned symbol values. The variables are initialized once when the module is loaded.
use emacs::{defun, use_symbols, Result, Value};
use_symbols! {
left right center
}
#[defun(mod_in_name = false)]
fn classify(pos: Value<'_>) -> Result<String> {
if pos == *left {
Ok("left".to_owned())
} else if pos == *right {
Ok("right".to_owned())
} else if pos == *center {
Ok("center".to_owned())
} else {
Ok("unknown".to_owned())
}
}
The Lisp name for each symbol is derived by replacing _ with -. Use => "lisp-name" to override:
use_symbols! {
nil t
buffer_read_only => "buffer-read-only"
}
If the symbol is bound to a function, you can call it via env.call(symbol_var, args). This goes through symbol lookup on each call. Use use_functions! to avoid that indirection.
use_functions!
use_functions! is like use_symbols!, but stores the function object directly (via indirect-function). Calls through these variables skip symbol lookup entirely.
use emacs::{defun, use_functions, Env, Result};
use_functions! {
message
string_to_number => "string-to-number"
}
#[defun]
fn greet_parsed(env: &Env, s: String) -> Result<()> {
let n: i64 = env.call(string_to_number, (s,))?.into_rust()?;
env.call(message, (format!("Got {}", n),))?;
Ok(())
}
Trade-off: use_functions! is faster than use_symbols! for repeated calls because it skips symbol lookup. However, if the symbol is later rebound to a different function, the cached reference still points to the original function. Use use_symbols! when you need to respect runtime rebinding; use use_functions! for built-in and primitive functions where rebinding is not expected.
Both macros can be used only once per Rust mod. To cover multiple mods, place one invocation in each.
Type Conversions
The type Value represents Lisp values:
- They can be copied around, but cannot outlive the
Envthey come from. - They are “proxy values”: only useful when converted to Rust values, or used as arguments when calling Lisp functions.
Converting a Lisp Value to Rust
This is enabled for types that implement FromLisp. Most built-in types are supported. Note that conversion may fail, so the return type is Result<T>.
let i: i64 = value.into_rust()?; // error if Lisp value is not an integer
let f: f64 = value.into_rust()?; // error if Lisp value is nil
let s = value.into_rust::<String>()?;
let s: Option<String> = value.into_rust()?; // None if Lisp value is nil
let b: Vec<u8> = value.into_rust()?; // raw bytes of a Lisp string
It’s better to declare input types for #[defun] than calling .into_rust(), unless delayed conversion is needed.
Converting a Rust Value to Lisp
This is enabled for types that implement IntoLisp. Most built-in types are supported. Note that conversion may fail, so the return type is Result<Value<'_>>.
"abc".into_lisp(env)?;
"a\0bc".into_lisp(env)?;
b"\xff\0".into_lisp(env)?; // unibyte string, needs feature emacs-28
5.into_lisp(env)?;
65.3.into_lisp(env)?;
().into_lisp(env)?; // nil
true.into_lisp(env)?; // t
false.into_lisp(env)?; // nil
It’s better to declare return type for #[defun] than calling .into_lisp(env), whenever possible.
Integers
Integer conversion is lossless by default. Rust code signals rust-integer-out-of-range (RustError::IntegerOutOfRange) when a value does not fit the target type, in cases such as:
- A
#[defun]expectingu8gets passed-1. - A
#[defun]returningu64returns a value larger thani64::max_value().
On Emacs 25 and 26, which have no bignums, the module layer itself can reject a Rust integer that does not fit a fixnum. This signals rust-module-integer-out-of-range (ModuleError::IntegerOutOfRange) instead.
The reverse also happens. On Emacs 27+, which support bignums, a Lisp integer outside the i64 range signals rust-module-integer-out-of-range (ModuleError::IntegerOutOfRange), for every target type, for example i8 or u64. This happens even with lossy-integer-conversion: extracting the i64 itself fails, before any Rust-side narrowing runs.
To disable Rust-side narrowing checks, use the lossy-integer-conversion feature:
[dependencies.emacs]
features = ["lossy-integer-conversion"]
Support for Rust’s NonZero integer types is disabled by default. To enable it, use the nonzero-integer-conversion feature:
[dependencies.emacs]
features = ["nonzero-integer-conversion"]
Strings
Use String for text. Use Vec<u8> for binary data, or for text in an encoding other than UTF-8.
// (xor-bytes "\377\0" 1) returns "\376\1". Returning `Vec<u8>` needs feature emacs-28.
#[defun]
fn xor_bytes(data: Vec<u8>, key: u8) -> Result<Vec<u8>> {
Ok(data.iter().map(|b| b ^ key).collect())
}
Lisp to Rust conversion copies the bytes of a unibyte string, or the UTF-8 encoding of a multibyte string. String then validates them as UTF-8. Vec<u8> does not.
| Lisp value | String | Vec<u8> |
|---|---|---|
"abc" (unibyte) | "abc" | b"abc" |
"\377" (unibyte, not UTF-8) | rust-invalid-utf-8 | b"\xff" |
"é" (multibyte) | "é" | b"\xc3\xa9" |
(string-to-multibyte "\377") (raw byte, no UTF-8 encoding) | rust-module-non-unicode-string † | rust-module-non-unicode-string † |
5 | rust-module-wrong-type | rust-module-wrong-type |
† Emacs 25 and 26 do not check this. String signals rust-invalid-utf-8. Vec<u8> gives b"\xff".
Rust to Lisp conversion:
&strandStringgive a multibyte string, even for ASCII text.&[u8]andVec<u8>give a unibyte string. They need theemacs-28feature, because the module API before Emacs 28 cannot make a unibyte string.
Supported byte types: Vec<u8> and Box<[u8]> from Lisp; &[u8], &Vec<u8>, Vec<u8>, and Box<[u8]> to Lisp. They convert to a Lisp string, not to a vector of integers.
To squeeze out some performance:
- You can avoid allocating memory for
Stringstructs, by usingvalue.copy_string_contents(buffer)with a large enough buffer. - If you know the string is valid UTF-8, you can skip the Rust-side check with
String::from_utf8_unchecked(value.clone_string_contents()?).
Equality
Value implements PartialEq, which maps to Lisp’s eq (identity/pointer equality, not equal).
// Two references to the same interned symbol are eq.
let a = env.intern("hello")?;
let b = env.intern("hello")?;
assert!(a == b);
// Two separately allocated strings with the same content are not eq.
let s1 = "hi".into_lisp(env)?;
let s2 = "hi".into_lisp(env)?;
assert!(s1 != s2);
GlobalRef and OnceGlobalRef implement PartialEq<Value> (and vice versa), so you can compare a cached global against an incoming argument without rebinding:
use emacs::use_symbols;
use_symbols! { nil }
#[defun]
fn is_nil(v: Value<'_>) -> Result<bool> {
Ok(v == *nil)
}
The old value.eq(other) method is deprecated since 0.20.0. Use == instead.
Vectors
Lisp vectors are represented by the type Vector, which can be considered a “sub-type” of Value.
To construct Lisp vectors, use env.make_vector and env.vector, which are efficient wrappers of Emacs’s built-in subroutines make-vector and vector.
env.make_vector(5, ())?;
env.vector((1, "x", true))?;
Embedding Rust Values in Lisp
Speeding up Emacs is one of the goals of dynamic modules. Too many back-and-forth conversions between Rust’s data structures and Lisp’s can defeat the purpose. The solution to this is embedding Rust data structures in opaque user-ptr Lisp objects.
If a type implements Transfer, its heap-allocated (Box-wrapped) values can be moved into the Lisp runtime, where the GC will become its owner.
Lisp code sees these as opaque “embedded user pointers”, whose printed representation is something like #<user-ptr ptr=0x102e10b60 finalizer=0x103c9c390>. For these values to be useful, a Rust module needs to export additional functions to manipulate them.
Since these values are owned by the GC, Rust code can only safely access them through immutable references. Therefore, interior mutability is usually needed. As a result, Transfer is implemented for the smart pointer types RefCell, Mutex, RwLock, Rc, and Arc.
To return an embedded value, a function needs to be exported with a user_ptr option:
user_ptr: Embedding through aRefCell. This is suitable for common use cases, where module functions can borrow the underlying data back for read/write. It is safe because Lisp threads are subjected to the GIL.BorrowError/BorrowMutErrormay be signaled at runtime, depending on how module functions call back into the Lisp runtime.user_ptr(rwlock),user_ptr(mutex): Embedding through aRwLock/Mutex. This is suitable for sharing data between module functions (on Lisp threads, withEnvaccess) and pure Rust code (on background threads, without access to anEnv).user_ptr(direct): Embedding aTransfervalue directly. This is suitable for immutable data that will only be read back (not written to) by module functions (writing requiresunsafeaccess, and is discouraged).
As an example, a module that allows Emacs to use Rust’s HashMap may look like this:
use std::collections::HashMap;
use emacs::{defun, Env, Result};
#[emacs::module(name = "rs-hash-map", separator = "/")]
fn init(env: &Env) -> Result<()> {
type Map = HashMap<String, String>;
#[defun(user_ptr)]
fn make() -> Result<Map> {
Ok(Map::new())
}
#[defun]
fn get(map: &Map, key: String) -> Result<Option<&String>> {
Ok(map.get(&key))
}
#[defun]
fn set(map: &mut Map, key: String, value: String) -> Result<Option<String>> {
Ok(map.insert(key, value))
}
Ok(())
}
(let ((m (rs-hash-map/make)))
(rs-hash-map/get m "a") ; -> nil
(rs-hash-map/set m "a" "1") ; -> nil
(rs-hash-map/get m "a") ; -> "1"
(rs-hash-map/set m "a" "2") ; -> "1"
(rs-hash-map/get m "a")) ; -> "2"
Notes:
Value.into_rust()has a runtime type check, which fails with the error'rust-wrong-type-user-ptrif the value is auser-ptrobject of a different type.- Input parameters with reference types are interpreted as
RefCell-embeddeduser-ptrobjects. For other kinds of embedding, you will have to use aValueparameter, and acquire the reference manually, since locking strategy (including deadlock avoidance/detection) should be module-specific.use std::sync::RwLock; #[defun(user_ptr(rwlock))] fn make() -> Result<Map> { Ok(Map::new()) } #[defun] fn get(v: Value<'_>, key: String) -> Result<Value<'_>> { let lock: &RwLock<Map> = v.into_rust()?; let map = lock.try_read().map_err(|_| Error::msg("map is busy"))?; map.get(&key).into_lisp(v.env) }
Lifetime-constrained Types
When a type is constrained by a (non-static) lifetime, its value cannot be embedded unchanged. Before embedding, the lifetime must be soundly elided. In other words, static ownership must be correctly given up.
The typical example is a struct holding a reference to another struct:
pub struct Tree;
pub struct Node<'t> {
pub tree: &'t Tree,
}
impl Tree {
pub fn root_node(&self) -> Node<'_> {
...
}
}
impl<'t> Node<'t> {
pub fn child(&self) -> Node<'t> {
...
}
}
In this case, the lifetime can be elided by turning the static reference into a dynamic ref-counted pointer. The rental crate provides a convenient way to do this:
#[macro_use]
extern crate rental;
use std::{rc::Rc, marker::PhantomData};
use emacs::{defun, Result};
// PhantomData is need because map_suffix requires a type parameter.
// See https://github.com/jpernst/rental/issues/35.
pub struct PhantomNode<'t, T>(Node<'t>, PhantomData<T>);
impl<'t> PhantomNode<'t, ()> {
fn child(&self) -> Self {
PhantomNode(self.0.child(), PhantomData)
}
}
rental! {
pub mod inner {
use std::rc::Rc;
// Self-referential struct that holds both
// the actual Node and the ref-counted Tree.
#[rental(map_suffix = "T")]
pub struct RentingNode<T: 'static> {
tree: Rc<super::Tree>,
node: super::PhantomNode<'tree, T>
}
}
}
type RentingNode = inner::RentingNode<()>;
#[defun(user_ptr)]
fn root_node(tree: Value) -> Result<RentingNode> {
let rc: &Rc<Tree> = tree.into_rust()?;
Ok(RentingNode::new(rc.clone(), |tree| tree.root_node()))
}
#[defun(user_ptr)]
fn child(node: &RentingNode) -> Result<RentingNode> {
node.map(|n| n.child())
}
Note that there’s no unsafe involved directly, as the soundness proofs are already encapsulated in rental macros.
Error Handling and Signaling
Emacs Lisp’s error handling mechanism uses non-local exits. Rust uses Result enum. emacs-module-rs converts between the 2 at the Rust-Lisp boundaries (more precisely, Rust-C).
The chosen error type is the Error struct from anyhow crate:
pub type Result<T> = result::Result<T, anyhow::Error>;
Handling Lisp Errors in Rust
Lisp code exits in 2 ways: it signals an error, or it throws a value. Calling it (env.call, Value::call) surfaces these as ErrorKind::Signal and ErrorKind::Throw.
When calling a Lisp function, it’s usually a good idea to propagate signaled errors with the ? operator, letting higher level (Lisp) code handle them. If you want to handle a specific error, you can use error.downcast_ref:
match env.call("insert", [some_text]) {
Err(error) => {
// Handle `buffer-read-only` error.
if let Some(ErrorKind::Signal { symbol, .. }) = error.downcast_ref::<ErrorKind>() {
let buffer_read_only = env.intern("buffer-read-only")?;
// `symbol` is a `TempValue` that must be converted to `Value`.
let symbol = unsafe { symbol.value(env) };
if symbol == buffer_read_only {
env.message("This buffer is not writable!")?;
return Ok(());
}
}
// Propagate other errors.
Err(error)
}
Ok(_) => Ok(()),
}
Note the use of unsafe to extract the error symbol as a Value. The reason is that, ErrorKind::Signal is marked Send+Sync, for compatibility with anyhow, while Value is lifetime-bound by env. The unsafe contract here requires the error being handled (and its TempValue) to come from this env, not from another thread, or from a global/thread-local storage.
Catching Values Thrown by Lisp
This is similar to handling Lisp errors. The only difference is ErrorKind::Throw being used instead of ErrorKind::Signal.
Handling Module-layer and Rust-layer Errors in Rust
Two more origins exist.
ErrorKind::Module: The module layer (emacs-module.c) rejects some calls, for exampleextract_integeron a string.ErrorKind::Rust: The Rust layer (i.e. this crate) rejects some values, for example converting aStringfrom bytes that are not valid UTF-8.
match error.downcast_ref::<ErrorKind>() {
Some(ErrorKind::Signal { .. } | ErrorKind::Throw { .. }) => {
// Lisp code signaled or threw.
}
Some(ErrorKind::Module(_)) => {
// The module layer rejected the call.
}
Some(ErrorKind::Rust(_)) => {
// Rust code in this crate rejected the value.
}
None => {
// Not an `ErrorKind`.
}
}
ModuleError and RustError are #[non_exhaustive], so a match on their variants needs an _ arm:
match error.downcast_ref::<ErrorKind>() {
Some(ErrorKind::Module(ModuleError::WrongType { expected: LispType::Integer, .. })) => {
env.message("Expected an integer")?;
}
Some(ErrorKind::Rust(RustError::InvalidUtf8 { .. })) => {
env.message("Not valid UTF-8")?;
}
// `ModuleError` and `RustError` can grow new variants in a minor release.
_ => return Err(error),
}
Signaling Lisp Errors from Rust
The function env.signal allows signaling a Lisp error from Rust code. The error symbol must have been defined, e.g. by the macro define_errors!:
// The parentheses denote parent error signals.
// If unspecified, the parent error signal is `error`.
emacs::define_errors! {
my_custom_error "This number should not be negative" (arith_error range_error)
}
#[defun]
fn signal_if_negative(env: &Env, x: i16) -> Result<()> {
if x < 0 {
return env.signal(my_custom_error, ("associated", "DATA", 7))
}
Ok(())
}
Handling Module-layer and Rust-layer Errors in Lisp
Instead of handling the module-layer and Rust-layer errors above, Rust module functions can propagate them to Lisp. When that happens, the error is “wrapped” in a way that is compatible with using condition-case on standard errors.
- The error symbol has 2 parent symbols: the origin symbol and the standard symbol.
- For module-layer errors, the origin symbol is
rust-module-error, and the standard symbol is the original symbol thatemacs-module.cuses. - For Rust-layer errors, the origin symbol is
rust-error.
- For module-layer errors, the origin symbol is
- The signal data has the same shape as the standard symbol’s data shape in Emacs 31. For example,
rust-module-wrong-typedata is(integerp "3"), likewrong-type-argument,rust-invalid-utf-8data is(utf-8-string-p "\377").
Signal, Throw, and ModuleError::Signal are raised again unchanged. Lisp code sees the original symbol and data. ModuleError::Signal has no rust- symbol.
ErrorKind
├── Signal, Throw from Lisp code
├── Module(ModuleError) the module layer (emacs-module.c) rejected the call
│ ├── WrongType expected: LispType
│ ├── NonUnicodeString
│ ├── InvalidUtf8
│ ├── BufferTooSmall
│ ├── IndexOutOfRange
│ ├── IntegerOutOfRange
│ └── Signal no typed variant
└── Rust(RustError) this crate's Rust layer rejected the value
├── WrongTypeUserPtr
├── InvalidUtf8
└── IntegerOutOfRange
error + standard symbol
├── rust-module-error
│ ├── rust-module-wrong-type + wrong-type-argument
│ ├── rust-module-non-unicode-string + wrong-type-argument
│ ├── rust-module-invalid-utf-8 + wrong-type-argument
│ ├── rust-module-buffer-too-small + args-out-of-range, memory-buffer-too-small (31+)
│ ├── rust-module-index-out-of-range + args-out-of-range
│ └── rust-module-integer-out-of-range + overflow-error
├── rust-error
│ ├── rust-wrong-type-user-ptr + wrong-type-argument
│ ├── rust-invalid-utf-8 + wrong-type-argument
│ └── rust-integer-out-of-range + overflow-error
└── rust-panic
| Variant | Data |
|---|---|
WrongType | (PREDICATE VALUE) |
BufferTooSmall | (ACTUAL REQUIRED) |
IndexOutOfRange | (VECTOR INDEX) |
IntegerOutOfRange | (VALUE), or no data |
WrongTypeUserPtr | (EXPECTED VALUE) |
EXPECTEDis the Rust type name, not a predicate.PREDICATEis the Lisp type predicate that the value failed, for exampleintegerporuser-ptrp. Some, for exampleutf-8-string-p, are not functions.
// May signal `rust-wrong-type-user-ptr` if `value` holds a different type of hash map,
// or is a `user-ptr` defined in a non-Rust module.
let r: &RefCell<HashMap<String, String>> = value.into_rust()?;
Panics
Unwinding from Rust into C is undefined behavior. emacs-module-rs prevents that by using catch_unwind at the Rust-to-C boundary to convert a panic into a Lisp’s signal/throw of the appropriate type:
- Normally the panic is converted into a Lisp’s error signal of the type
rust-panic. Note that it is not a sub-type ofrust-error. - If the panic value is an
ErrorKind, it is converted to the corresponding signal/throw, as if aResultwas returned. This allows propagating Lisp’s non-local exits through contexts whereResultis not appropriate, e.g. callbacks whose types are dictated by 3rd-party libraries, such astree-sitter.
Communicating with Emacs from Background Threads
open_channel lets a background Rust thread send data to Emacs by writing into a pipe process’s input stream. It requires the emacs-28 feature and Emacs 28+.
Setup
Enable the feature:
cargo add emacs --features emacs-28
How It Works
Emacs’s make-pipe-process returns a process object backed by a pair of OS pipes. env.open_channel(pipe_process) opens the write end of that pipe and returns an impl Write + Send + Sync. You can move it to any thread and write to it; Emacs’s process filter function receives the data on the main thread.
use std::io::Write;
use emacs::{defun, Env, Result, Value};
/// Send DATA to PROCESS from the calling thread.
#[defun]
fn channel_send(env: &Env, process: Value<'_>, data: String) -> Result<()> {
let mut writer = env.open_channel(process)?;
writer.write_all(data.as_bytes())?;
Ok(())
}
/// Spawn a thread that sends DATA to PROCESS, then wait for it.
#[defun]
fn channel_send_from_thread(env: &Env, process: Value<'_>, data: String) -> Result<()> {
let mut writer = env.open_channel(process)?;
let handle = std::thread::spawn(move || -> std::io::Result<()> {
writer.write_all(data.as_bytes())?;
Ok(())
});
handle.join().expect("thread panicked")?;
Ok(())
}
On the Emacs side, create the pipe process and attach a filter:
(let ((proc (make-pipe-process
:name "my-pipe"
:filter (lambda (_proc data)
(message "Received: %s" data)))))
(my-module-channel-send proc "hello from Rust"))
Dropping the writer closes the write end of the pipe. Emacs’s process sentinel fires when the last writer closes.
Windows Note
On Windows, the write end of the pipe is a CRT (C runtime) file descriptor created by Emacs. Your module must link against the same CRT as Emacs. Otherwise, open_channel crashes inside get_osfhandle.
- Official GNU builds use MSVCRT. Build your module in the MSYS2 MINGW64 shell.
- Official
-UCRT64builds (Emacs 31+) and MSYS2’smingw-w64-ucrt-x86_64-emacsuse UCRT. Build your module in the MSYS2 UCRT64 shell.
The GNU toolchain uses gcc as the linker, so the first gcc in PATH selects the CRT. To check a binary’s CRT, list its imported DLLs:
objdump -p "$(which emacs)" | grep 'DLL Name'
msvcrt.dll means MSVCRT. ucrtbase.dll or api-ms-win-crt-*.dll means UCRT. See Overview for the setup steps.
Canvas Images
Value::with_canvas_data lends your module the pixel buffer of an Emacs 32 canvas image. You draw into the buffer from Rust, then ask Lisp to show it.
Setup
This feature is experimental. It tracks an unreleased module ABI.
Enable the feature:
cargo add emacs --features emacs-32-experimental
The module needs Emacs 32, built with a window system. Older Emacs versions refuse to load it. Without a window system, with_canvas_data signals error.
Drawing
A canvas is an image spec with :type canvas. :id, :data-width and :data-height are mandatory. The spec object (not an equal copy) identifies the canvas.
use emacs::{defun, Env, Result, Value};
/// Fill CANVAS with PIXEL.
#[defun]
fn fill(env: &Env, canvas: Value<'_>, pixel: u32) -> Result<()> {
canvas.with_canvas_data(|data| data.buffer.fill(pixel))?;
env.call("canvas-refresh", [canvas])?;
Ok(())
}
(defvar my-canvas (list 'image :type 'canvas :id 'my-canvas :data-width 320 :data-height 240))
(insert-image my-canvas)
(my-module-fill my-canvas #xFF3366CC)
Emacs does not show the changes until Lisp calls canvas-refresh on the spec. In a loop, call redisplay as well.
Rules
The closure receives a CanvasData with the fields buffer, width and height.
bufferis a&mut [u32]withwidth * heightelements. Pixels are in row-major order. Usebuffer.chunks_exact_mut(width)to get rows.- Each pixel is a native-endian
0xAARRGGBBvalue. Emacs ignores alpha when it displays the canvas. - A resize zeroes the buffer. Lisp resizes a canvas when it changes
:data-widthor:data-height. Pixels from before are lost. - The closure must be
Send, so it cannot capture&EnvorValue. Thus it cannot call Lisp.
The last rule is the safety argument. Lisp code can resize or free the canvas, and redisplay reads the buffer. The buffer is valid only while no Lisp code runs. Call canvas-refresh after with_canvas_data returns, as fill does.
The bound also rejects other non-Send captures, such as Rc. Copy the data out of them first.
Background Rendering
Emacs reads the buffer only on the Lisp thread. So a render thread must not hold the canvas buffer. Let the thread draw into a buffer that Rust owns, and swap it with a shared buffer. A defun, which runs on the Lisp thread, copies the shared buffer into the canvas.
use std::mem;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use emacs::{defun, Env, Result, Value};
struct Frame {
width: usize,
height: usize,
pixels: Vec<u32>,
}
static FRAME: Mutex<Frame> = Mutex::new(Frame { width: 0, height: 0, pixels: Vec::new() });
/// Render frames of WIDTH by HEIGHT pixels in a background thread.
#[defun]
fn render_start(width: usize, height: usize) -> Result<()> {
thread::spawn(move || {
let mut back = Frame { width, height, pixels: Vec::new() };
for tick in 0u32.. {
back.width = width;
back.height = height;
back.pixels.resize(width * height, 0);
for (index, pixel) in back.pixels.iter_mut().enumerate() {
*pixel = 0xFF00_0000 | (index as u32).wrapping_add(tick.wrapping_mul(1000));
}
let Ok(mut front) = FRAME.lock() else { return };
mem::swap(&mut *front, &mut back);
drop(front);
thread::sleep(Duration::from_millis(16));
}
});
Ok(())
}
/// Copy the latest frame into CANVAS, and refresh it.
#[defun]
fn render_present(env: &Env, canvas: Value<'_>) -> Result<()> {
let copied = canvas.with_canvas_data(|data| {
let Ok(frame) = FRAME.lock() else { return false };
if frame.width != data.width || frame.height != data.height {
return false;
}
data.buffer.copy_from_slice(&frame.pixels);
true
})?;
if copied {
env.call("canvas-refresh", [canvas])?;
}
Ok(())
}
(my-module-render-start 320 240)
(run-with-timer 0 (/ 1.0 30) (lambda () (my-module-render-present my-canvas)))
- The render thread draws without the lock. It holds the lock only to swap, so a copy in
render_presentdoes not stop the drawing. - The mutex guards only the Rust-owned buffers. Emacs needs no lock.
render_presentcompares dimensions first. Lisp can resize the canvas while the thread renders a frame. Without the check,copy_from_slicepanics on a length mismatch.
Known Gap
If Lisp code that runs during the call resizes the same canvas, buffer has the wrong length. This code includes post-gc-hook, the debugger, file name handlers for :file, and *Messages* updates (messages-buffer-mode and its hooks) when Emacs reports an image error. Do not resize a canvas from such code.
The length is also wrong if Lisp redefines cdr or plist-get to return false values before the module loads. The module calls the definitions that exist at load time. Advice that passes the real results through, such as trace-function, is harmless.
Testing
You can define tests using ert, then use a script to load the module and run the tests. Examples:
For continuous testing during development, use cargo-watch to rebuild the module and rerun the tests on file changes. emacs-module-rs’s script wraps it:
cargo xtask test --watch
A future version will have tighter integration with either cargo or Cask.
Live Reloading
Live code reloading is very useful during development. However, Emacs does not support unloading modules. Live reloading thus requires a custom module loader, e.g. emacs-rs-module, which is itself a dynamic module.
To use it, load it in Emacs:
(require 'rs-module)
Then use it to load other modules instead of require or module-load:
;; Will unload the old version of the module first.
(rs-module/load "full/path/to/module.so")
cargo doesn’t support installing dynamic libs yet, so you have to include emacs-rs-module as a dev dependency to compile it on your own:
cargo add --dev emacs-rs-module
magit-libgit2 is an example of how to set this all up, to have live-reloading on-save.
A future version will have tighter integration with cargo.
Notes:
- It mainly works on Linux, but potentially because Linux’s dynamic loading system is unsafe (i.e. ridden with UB traps).
- It doesn’t work on macOS 10.13+ (High Sierra and up), because macOS doesn’t unload dynamic libraries that use TLS (thread-local storage), for safety reason. See Rust’s issue #28794.
- It doesn’t work on Windows, since loading the dll prevents writing to its file.