vlambda博客
学习文章列表

使用 Rust/C 开发 Emacs 插件

Emacs 在 25 版本后,支持了动态模块(dynamic modules),这为 Emacs 插件的开发打开了新的一扇大门,任何能够编译生成符合 Emacs ABI 要求的语言都可以使用。

本文就来介绍,如何使用 C/Rust 两种语言来进行 Emacs 动态模块的开发。本文所有代码可在 emacs-dynamic-module 这里找到。

C

C 是开发动态模块最直接的语言,Emacs 核心部分就是用 C 开发的。一个简单的 hello world 示例如下:

// emacs 动态模块的头文件,一般在 Emacs 安装目录内可找到
#include <emacs-module.h>
#include <string.h>
// 声明该模块是 GPL 兼容的
int plugin_is_GPL_compatible;

// 模块的入口函数,相当于普通 C 程序的 main
int emacs_module_init (struct emacs_runtime *ert)
{
  emacs_env *env = ert->get_environment(ert);

  emacs_value message = env->intern(env, "message");
  char *msg = "hello world";
  emacs_value args[] = { env->make_string(env, msg, strlen(msg)) };
  env->funcall(env, message, 1, args);
  return 0;
}

把上面的代码编译成动态链接库,macOS 下可以用如下命令:

cc -dynamiclib -o helloworld.dylib -I"/Applications/Emacs.app/Contents/Resources/include/" main.c

其他环境下的编译命令可参考 Building a Dynamic Library from the Command Line。动态链接库后缀名在不同平台是不一样的,Linux 下是 so, Windows 下是 dll。生产动态链接库后,可以用下面的命令加载:

(module-load (expand-file-name "~/helloworld.dylib"))

这时,会在 *Message* 内打印出 hello world , module-load 函数本身返回 t 。

为了简化数据类型在 C 与 ELisp 之间的转化,Emacs 提供了一系列函数,比如:

C–>Elisp Elisp–>C
make_integer extract_integer
make_float extract_float
make_string copy_string_contents

更多类型转化可参考官方文档:

Conversion Between Lisp and Module Values

这里着重介绍下如何将 C 里面的函数导出到 ELisp 中:

emacs_value c_add(emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) {
  intmax_t ret = 0;
  for(int i=0;i<nargs;i++) {
    ret += env->extract_integer(env, args[i]);
  }
  return env->make_integer(env, ret);
}

void define_elisp_function(emacs_env *env) {
  emacs_value func = env->make_function (env, 1, emacs_variadic_function, // 任意多个参数,类似 &rest
                                         c_add, "C-based adder"NULL);
  emacs_value symbol = env->intern (env, "c-add");
  emacs_value args[] = {symbol, func};
  env->funcall (env, env->intern (env, "defalias"), 2, args);
}

在 emacs_module_init 中调用 define_elisp_function 即可将 c-add 导出到 ELisp 中,使用示例:

(c-add 1 2)
;; 3
(apply 'c-add (number-sequence 1 100))
;; 5050
(c-add)
;; Debugger entered--Lisp error: (wrong-number-of-arguments #<module function c_add from /tmp/helloworld.dylib> 0)

M-x describe-function RET c-add RET 返回如下:

c-add is a module function.

(c-add ARG1 &rest REST)

C-based adder

上面的示例代码虽然功能简单,但是把开发『动态模块』所需功能都介绍到了,如果需要更复杂的功能,可以参考文档:

Writing Dynamically-Loaded ModulesEmacs modules | Philipp’s documents

简化方法调用

从上面介绍的示例可看出,基本所有函数都需要 env 这个参数,这是由于 C 的 struct 不支持成员函数,可以用宏来简化些,比如:

#define lisp_integer(env, integer)              \
  ({                                            \
    emacs_env *_env_ = env;                     \
    _env_->make_integer(_env_, (integer));      \
  })                                            \

#define lisp_string(env, string)                        \
  ({                                                    \
    emacs_env *_env_ = env;                             \
    char* _str_ = string;                               \
    _env_->make_string(_env_, _str_, strlen(_str_));    \
  })


#define lisp_funcall(env, fn_name, ...)                 \
  ({                                                    \
    emacs_env *_env_ = env;                             \
    emacs_value _args_[] = { __VA_ARGS__ };             \
    int _nargs_ = sizeof(_args_) / sizeof(emacs_value); \
    _env_->funcall(_env_,                               \
                   env->intern(env, (fn_name)),         \
                   _nargs_,                             \
                   _args_                               \
                   );                                   \
  })

需要注意的是,上面的宏使用了 Statement Expression,不是 C 语言的标准,是 GNU99 的扩展,但由于十分有用,大多数编译器都支持了这种语法(可通过 -std=gnu99 指定),所以可以放心使用。其次是用到了可变参的宏,这是 C99 引入的。使用方式如下:

  emacs_value ret = lisp_funcall(env, "1+", lisp_integer(env, 1));
  lisp_funcall(env, "message",
               lisp_string(env, "(1+ %d) is %d"),
               (lisp_integer(env, 1)),
               ret);

热加载

在开发过程中,最重要的是热加载,不能每次重启服务来让新代码生效,但是这里通过 module-load 加载的动态模块,是无法卸载的,只能重启 Emacs 解决,这不是很友好,可以通过一种变通的方式来实现:

(defun fake-module-reload (module)
  "通过加载临时文件的方式来模拟热加载
https://emacs.stackexchange.com/a/36501/16450"
  (interactive "fReload Module file: ")
  (let ((tmpfile (make-temp-file
                  (file-name-nondirectory module) nil module-file-suffix)))
    (copy-file module tmpfile t)
    (module-load tmpfile)))

在 Rust 中,还有一个方案,即 rs-module/load,后文会具体介绍。

Rust

使用 Rust 开发动态模块要比 C 简单不少,毕竟作为新时代的语言,但包管理这一方面就甩 C 好几条街。这里主要会用到 emacs-module-rs 这个 crate,示例代码如下:

use emacs::{defun, Env, Result, Value};

emacs::plugin_is_GPL_compatible!();

// 相当于 C 里面的 emacs_module_init
#[emacs::module(name = "greeting")]
fn init(_: &Env) -> Result<()> { Ok(()) }

#[defun]
fn say_hello(env: &Env, name: String) -> Result<Value<'_>> {
    env.message(&format!("Hello, {}!", name))
}

相比 C 代码,这里的代码简洁不少,通过 #[defun] 将 say_hello 函数导出到 ELisp 中,并且函数名自动加上了前缀 greeting ,并提供了相应 feature 。 cargo build 成功后执行:

(module-load "/tmp/helloworld-rust/target/debug/libhelloworld_rust.dylib")

(greeting-say-hello "rust")
;; 输出 "Hello, rust!"

;; 或把 dylib 所在目录追加到 load-path,然后执行
;; (require 'greeting)

更多使用细节可以参考官方文档,里面有非常详细的描述。

用Rust扩展Emacs功能 | NIL,这篇文章算是对官方文档的中文翻译,供读者参考

热加载

使用 emacs-module-rs 开发的动态模块,会暴露一个 reload 的函数 emacs_rs_module_init,然后利用 rs-module/load 即可实现热加载。相关命令如下:

git clone https://github.-com/ubolonton/emacs-module-rs.git
cd emacs-module-rs && cargo build

这会生成 libemacs_rs_module.dylib ,它会暴露 rs-module/load 方法,用这个方法去加载其他模块即可实现热加载:

(module-load "/path/to/emacs-rs-module/target/debug/libemacs_rs_module.dylib")

(rs-module/load "/tmp/helloworld-rust/target/debug/libhelloworld_rust.dylib")

参考项目

最后,列举一些使用 Rust 开发动态模块的实际项目,供读者参考:

1History/eww-history-ext: Persist EWW histories into SQLiterustify-emacs/fuz.el: Fast and precise fuzzy scoring/matching utils for Emacsemacs-tree-sitter/elisp-tree-sitter: Tree-sitter bindings for Emacs Lisp