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, Emacs 25/26, macOS/Linux.

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).

Setting up

  • Install the Rust toolchain with rustup.
  • Make sure that your Emacs was compiled with module support. Check that module-file-suffix is not nil, and the function module-load is defined.

Hello, Emacs!

Create a new project:

cargo new greeting
cd greeting

Modify Cargo.toml:

[package]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
emacs = "0.7.0"

Write code in src/lib.rs:


# #![allow(unused_variables)]
#fn main() {
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<impl IntoLisp>.

In addition, in order to be loadable by Emacs, the module must be declared GPL-compatible.


# #![allow(unused_variables)]
#fn main() {
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, name of the feature provided by the module is the crate's name (with _ replaced by -). There is no need to explicitly call provide inside the initialization function. This option allows the function's name, or a string, to be used instead.
  • separator: Function names in Emacs are conventionally prefixed with the feature name, followed by -, this option allows a different separator to be used.
  • mod_in_name: Whether to put module path in function names. Default to true. This can also be overridden for each individual function, by an option of the same name in #[defun].

# #![allow(unused_variables)]
#fn main() {
// 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(()) }
#}

# #![allow(unused_variables)]
#fn main() {
// Use `/` as the separator that goes after feature's name, like some popular packages.
#[emacs::module(separator = "/")]
fn init(_: &Env) -> Result<()> { Ok(()) }
#}

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 declare 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. These functions must have the signature fn(..) -> Result<T>, where T is any type that implements IntoLisp. Arguments must be of types that implement FromLisp.


# #![allow(unused_variables)]
#fn main() {
#[defun]
fn inc(x: i64) -> Result<i64> {
    Ok(x + 1)
}

#[defun]
fn sum(x: f64, y: f64) -> Result<f64> {
    Ok(x + y)
}
#}

Naming

By default, the function's Lisp name has the form <feature-prefix>[mod-prefix]<base-name>.

  • feature-prefix is the feature's name, followed by -. This can be customized by the name and separator options on #[emacs::module].
  • mod-prefix is constructed from the function's Rust module path (with _ and :: replaced by -). This can be turned off crate-wide, or for individual function, using the option mod_in_name.
  • base-name is the function's Rust name (with _ replaced by -). This can be overridden with the option name.

Examples:


# #![allow(unused_variables)]
#fn main() {
// Assuming crate's name is `native_parallelism`.

#[emacs::module(separator = "/")]
fn init(_: &Env) -> Result<()> { Ok(()) }

mod shared_state {
    mod thread {
        // Ignore the nested mod's.
        // native-parallelism/make-thread
        #[defun(mod_in_name = false)]
        fn make_thread(name: String) -> Result<Value<'_>> {
            ..
        }
    }

    mod process {
        // native-parallelism/shared-state-process-launch
        #[defun]
        fn launch(name: String) -> Result<Value<'_>> {
            ..
        }

        // Explicitly named, since Rust identifier cannot contain `:`.
        // native-parallelism/process:pool
        #[defun(mod_in_name = false, name = "process:pool")]
        fn pool(name: String, min: i64, max: i64) -> Result<Value<'_>> {
            ..
        }
    }
}
#}

Interacting with the Lisp runtime

To interact with the Lisp runtime, e.g. calling a Lisp function, declare an additional argument of type &Env. This argument does not appear in the function's Lisp signature.


# #![allow(unused_variables)]
#fn main() {
// Assuming crate's name is `misc`.
use std::time::SystemTime;

#[defun]
fn what_time_is_it(env: &Env) -> Result<Value<'_>> {
    env.message(format!("{:#?}", SystemTime::now()))
}
#}
;; No argument
(misc-what-time-is-it)

Calling Lisp Functions

Frequently-used Lisp functions are exposed as methods on env:


# #![allow(unused_variables)]
#fn main() {
env.intern("defun")?;

env.message("Hello")?;

env.type_of(5.into_lisp(env)?)?;

env.provide("my-module")?;
#}

To call arbitrary Lisp functions, use env.call(&str, &[Value]):


# #![allow(unused_variables)]
#fn main() {
// (list "1" 2)
env.call("list", &[
    "1".into_lisp(env)?,
    2.into_lisp(env)?,
])?;

// (add-hook 'text-mode-hook 'variable-pitch-mode)
env.call("add-hook", &[
    env.intern("text-mode-hook")?,
    env.intern("variable-pitch-mode")?,
])?;
#}

Type Conversions

The type Value represents Lisp values:

  • They can be copied around, but cannot outlive the env they 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>.


# #![allow(unused_variables)]
#fn main() {
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<&str> = value.into_rust()?; // None if Lisp value is nil
#}

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<'_>>.


# #![allow(unused_variables)]
#fn main() {
"abc".into_lisp(env)?;
"a\0bc".into_lisp(env)?; // NulError (Lisp string cannot contain null byte)

5.into_lisp(env)?;
65.3.into_lisp(env)?;

().into_lisp(env)?; // nil
true.into_lisp(env)?; // t
false.into_lisp(env)?; // nil
#}

Embedding Rust values in Lisp

If a type implements Transfer, its Box-wrapped values can be moved into Lisp, to be owned by the GC.

Lisp code sees these as opaque "embedded user pointers" (whose printed representations look like#<user-ptr ...>). For these values to be usable, 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. To make them useful, interior mutability is usually needed. Therefore Transfer is implemented for RefCell, Mutex, and RwLock. Note that currently, only RefCell is useful, since a GC-integrated equivalent of Arc has not been implemented.

For example, a module that allows Emacs to use Rust's HashMap may look like this:


# #![allow(unused_variables)]
#fn main() {
use std::cell::RefCell;
use std::collections::HashMap;
use emacs::{defun, Env, Result, Value};

#[emacs::module(name = "rs-hash-map", separator = "/")]
fn init(env: &Env) -> Result<Value<'_>> {
    type Map = RefCell<HashMap<String, String>>;

    #[defun]
    fn make() -> Result<Map> {
        Ok(RefCell::new(HashMap::new()))
    }

    #[defun]
    fn get<'e>(env: &'e Env, map: &Map, key: String) -> Result<Value<'e>> {
        map.borrow().get(&key).into_lisp(env)
    }

    #[defun]
    fn set(map: &Map, key: String, value: String) -> Result<Option<String>> {
        Ok(map.borrow_mut().insert(key,value))
    }
}
#}
(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"

Error Handling

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 failure crate:


# #![allow(unused_variables)]
#fn main() {
pub type Result<T> = result::Result<T, failure::Error>;
#}

Handling Lisp errors in Rust

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:


# #![allow(unused_variables)]
#fn main() {
match env.call("insert", &[some_text]) {
    Err(error) => {
        // Handle `buffer-read-only` error.
        if let Some(&Signal { ref 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 { Ok(symbol.value(env)) };
            if env.eq(symbol, buffer_read_only) {
                env.message("This buffer is not writable!")?;
                return Ok(())
            }
        }
        // Propagate other errors.
        Err(error)
    },
    v => v,
}
#}

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 failure, 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.

Handling Rust errors in Lisp

In addition to standard errors, Rust module functions can signal Rust-specific errors, which can also be handled by condition-case:

  • rust-error: The message is Rust error. This covers all generic Rust-originated errors.
  • rust-wrong-type-user-ptr: The message is Wrong type user-ptr. This happens when Rust code is passed a user-ptr of a type it's not expecting. It is a sub-type of rust-error.
    
    # #![allow(unused_variables)]
    #fn main() {
    // May signal 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, converting a panic into a Lisp's error signal of type rust-panic. Note that it is not a sub-type of rust-error.

Catching values thrown by Lisp

This is similar to handling Lisp errors. The only difference is ErrorKind::Throw being used instead of ErrorKind::Signal.

Testing

You can define tests using ert, then use a bash script to load the module and run the tests. Examples:

Continuous testing during development can be done using cargo-watch:

cargo watch -x 'build --all' -s bin/test.sh

A future version will have tighter integration with either cargo or Cask.

Live Reloading

Note: This doesn't work on macOS 10.13+ (High Sierra and up). See Rust's issue #28794.

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:

[dev-dependencies]
emacs-rs-module = { version = "0.7.0" }

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.