使用 C/Rust 开发 Emacs 动态模块
文章目录
Emacs 在 25 版本后,支持了动态模块(dynamic modules),这为 Emacs 插件的开发打开了新的一扇大门,任何能够编译生成符合 Emacs ABI 要求的语言都可以使用。
本文就来介绍,如何使用 C/Rust 两种语言来进行 Emacs 动态模块的开发。本文所有代码可在 emacs-dynamic-module 这里找到。
C
C 是开发动态模块最直接的语言,Emacs 核心部分就是用 C 开发的。一个简单的 hello world 示例如下:
1// emacs 动态模块的头文件,一般在 Emacs 安装目录内可找到
2#include <emacs-module.h>
3#include <string.h>
4// 声明该模块是 GPL 兼容的
5int plugin_is_GPL_compatible;
6
7// 模块的入口函数,相当于普通 C 程序的 main
8int emacs_module_init (struct emacs_runtime *ert)
9{
10 emacs_env *env = ert->get_environment(ert);
11
12 emacs_value message = env->intern(env, "message");
13 char *msg = "hello world";
14 emacs_value args[] = { env->make_string(env, msg, strlen(msg)) };
15 env->funcall(env, message, 1, args);
16 return 0;
17}把上面的代码编译成动态链接库,macOS 下可以用如下命令:
1cc -shared -fpic -o helloworld.so -I"/Applications/Emacs.app/Contents/Resources/include/" $main-shared表示生成动态链接库-fpic表示生成地址无关代码(position-independent code)
其他环境下的编译命令可参考 Building a Dynamic Library from the Command Line。 动态链接库后缀名在不同平台是不一样的:
| OS | suffix |
|---|---|
| Linux | so |
| Windows | dll |
| macOS | dylib |
但 Emacs 在加载 dylib 后缀的动态库时,可能会报类似下面的错误:
1Error: error ("omg-dyn.dylib:0:0: error: scan-error: (Containing expression ends prematurely 82277 82278)")而且 Emacs 在 macOS 上支持加载 so 后缀,因此建议直接生成 so 后缀即可。
生产动态链接库后,可以用下面的命令加载:
1(module-load (expand-file-name "~/helloworld.so"))这时,会在 *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 |
更多类型转化可参考官方文档:
这里着重介绍下如何将 C 里面的函数导出到 ELisp 中:
1emacs_value c_add(emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) {
2 intmax_t ret = 0;
3 for(int i=0;i<nargs;i++) {
4 ret += env->extract_integer(env, args[i]);
5 }
6 return env->make_integer(env, ret);
7}
8
9void define_elisp_function(emacs_env *env) {
10 emacs_value func = env->make_function (env, 1, emacs_variadic_function, // 任意多个参数,类似 &rest
11 c_add, "C-based adder", NULL);
12 emacs_value symbol = env->intern (env, "c-add");
13 emacs_value args[] = {symbol, func};
14 env->funcall (env, env->intern (env, "defalias"), 2, args);
15}在 emacs_module_init 中调用 define_elisp_function 即可将 c-add 导出到 ELisp 中,使用示例:
1(c-add 1 2)
2;; 3
3(apply 'c-add (number-sequence 1 100))
4;; 5050
5(c-add)
6;; Debugger entered--Lisp error: (wrong-number-of-arguments #<module function c_add from /tmp/helloworld.so> 0)M-x describe-function RET c-add RET 返回如下:
c-add is a module function. (c-add ARG1 &rest REST) C-based adder
上面的示例代码虽然功能简单,但把开发『动态模块』的核心功能都介绍到了,像如何进行错误处理、如何在 C 与 ELisp 间传递自定义结构等高级功能,可以参考文档:
简化方法调用
从上面介绍的示例可看出,基本所有函数都需要 env 这个参数,这是由于 C 的 struct 不支持成员函数,可以用宏来简化,比如:
1#define lisp_integer(env, integer) \
2 ({ \
3 emacs_env *_env_ = env; \
4 _env_->make_integer(_env_, (integer)); \
5 }) \
6
7#define lisp_string(env, string) \
8 ({ \
9 emacs_env *_env_ = env; \
10 char* _str_ = string; \
11 _env_->make_string(_env_, _str_, strlen(_str_)); \
12 })
13
14#define lisp_funcall(env, fn_name, ...) \
15 ({ \
16 emacs_env *_env_ = env; \
17 emacs_value _args_[] = { __VA_ARGS__ }; \
18 int _nargs_ = sizeof(_args_) / sizeof(emacs_value); \
19 _env_->funcall(_env_, \
20 env->intern(env, (fn_name)), \
21 _nargs_, \
22 _args_ \
23 ); \
24 })需要注意的是,上面的宏使用了 Statement Expression,不是 C 语言的标准,是 GNU99 的扩展,但由于十分有用,大多数编译器都支持了这种语法(可通过 -std=gnu99 指定),所以可以放心使用。其次是用到了可变参的宏,这是 C99 引入的。使用方式如下:
1lisp_funcall(env,
2 "message",
3 lisp_string(env, "(1+ %d) is %d"),
4 (lisp_integer(env, 1)),
5 lisp_funcall(env, "1+", lisp_integer(env, 1)));由于 C 中的宏仅仅只是文本替换,所以即便使用了宏,代码也还是显得有些冗余。后文会介绍到,在 Rust 中是如何用宏来简化方法调用的。
热加载
在开发过程中,热加载是非常重要的需求,不能每次重启服务来让新代码生效。但是通过 module-load 加载的动态模块,是无法卸载的,那是不是必须要重启 Emacs 呢?xuchunyang 给出了一种不需要重启的热加载方案:
1(defun fake-module-reload (module)
2 (interactive "fReload Module file: ")
3 (let ((tmpfile (make-temp-file
4 (file-name-nondirectory module) nil module-file-suffix)))
5 (copy-file module tmpfile t)
6 (module-load tmpfile)))该方式很巧妙,虽然已经加载的 so 不能卸载,但可通过重新加载另一个功能相同的 so 来覆盖之前的,这间接实现了热加载的效果。 在 Rust 中,还有一个更有技术含量的方案,后文会具体介绍。
Rust
使用 Rust 开发动态模块要比 C 简单不少,毕竟作为新时代的语言,单就包管理这一方面,就比 C 好用不少。这里主要会用到 emacs-module-rs 这个 crate,示例代码如下:
1use emacs::{defun, Env, Result, Value};
2
3emacs::plugin_is_GPL_compatible!();
4
5// 相当于 C 里面的 emacs_module_init
6#[emacs::module(name = "greeting")]
7fn init(_: &Env) -> Result<()> { Ok(()) }
8
9#[defun]
10fn say_hello(env: &Env, name: String) -> Result<Value<'_>> {
11 env.message(&format!("Hello, {}!", name))
12}相比 C 代码,这里的代码简洁不少,方法的参数都是 Rust 类型,内部通过 FromLisp、IntoLisp 这两个 trait,进行 C 与 Rust 的类型转化。
通过 #[defun] 将 say_hello 函数导出到 ELisp 中,并且函数名自动加上了前缀 greeting ,并提供了相应 feature 。 cargo build 成功后,执行下面的命令:
1(module-load "/tmp/helloworld-rust/target/debug/libhelloworld_rust.dylib")
2
3(greeting-say-hello "rust")
4;; 输出 "Hello, rust!"
5
6;; 或把 dylib 所在目录追加到 load-path,然后执行
7;; (require 'greeting)更多使用细节可以参考官方文档,里面有非常详细的描述。
- 用Rust扩展Emacs功能 | NIL,这篇文章算是对官方文档的中文翻译,供读者参考
实现原理
emacs-module-rs 使用了大量过程宏来简化代码的编写,比如上面的 defun, emacs::module ,利用 cargo-expand 可以将这些宏代码展开,可以看到实现原理如下:
- 使用 defun 声明的函数会被添加到
__INIT_FNS__,这是一个全局的 map - 在生成的
emacs_module_init中,去遍历__INIT_FNS__,调用 fset 将 Rust 到 C 的 binding 函数导出到 ELisp 中
完整的宏展开代码在 expanded.rs,对细节感兴趣的读者可自行研究。
热加载
使用 emacs-module-rs 开发的动态模块,除了会生成 emacs_module_init 外,还会额外生成一个 emacs_rs_module_init 函数,rs-module/load 通过执行这个方法来实现热加载。热加载相关命令如下:
1git clone https://github.-com/ubolonton/emacs-module-rs.git
2cd emacs-module-rs && cargo build这会生成 libemacs_rs_module.dylib ,它会暴露 rs-module/load 方法,用这个方法去加载其他模块即可实现热加载:
1(module-load "/path/to/emacs-rs-module/target/debug/libemacs_rs_module.dylib")
2
3(rs-module/load "/tmp/helloworld-rust/target/debug/libhelloworld_rust.dylib")参考项目
最后,列举一些使用 C/Rust 开发动态模块的实际项目,供读者参考:
- 1History/eww-history-ext: Persist EWW histories into SQLite
- jiacai2050/oh-my-github: Oh My GitHub is a delightful, open source tool for managing your GitHub repositories
- rustify-emacs/fuz.el: Fast and precise fuzzy scoring/matching utils for Emacs
- emacs-tree-sitter/elisp-tree-sitter: Tree-sitter bindings for Emacs Lisp
收听方式

反馈
- 对节目有想法或发现内容错误?欢迎来信交流️