Emacs 在 25 版本后,支持了动态模块(dynamic modules),这为 Emacs 插件的开发打开了新的一扇大门,任何能够编译生成符合 Emacs ABI 要求的语言都可以使用。
本文就来介绍,如何使用 C/Rust 两种语言来进行 Emacs 动态模块的开发。本文所有代码可在 emacs-dynamic-module 这里找到。
C
C 是开发动态模块最直接的语言,Emacs 核心部分就是用 C 开发的。一个简单的 hello world 示例如下:
|
|
把上面的代码编译成动态链接库,macOS 下可以用如下命令:
|
|
-shared
表示生成动态链接库-fpic
表示生成地址无关代码(position-independent code)
其他环境下的编译命令可参考 Building a Dynamic Library from the Command Line。 动态链接库后缀名在不同平台是不一样的:
OS | suffix |
---|---|
Linux | so |
Windows | dll |
macOS | dylib |
但 Emacs 在加载 dylib 后缀的动态库时,可能会报类似下面的错误:
|
|
而且 Emacs 在 macOS 上支持加载 so 后缀,因此建议直接生成 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 中:
|
|
在 emacs_module_init
中调用 define_elisp_function
即可将 c-add
导出到 ELisp 中,使用示例:
|
|
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 不支持成员函数,可以用宏来简化,比如:
|
|
需要注意的是,上面的宏使用了 Statement Expression,不是 C 语言的标准,是 GNU99 的扩展,但由于十分有用,大多数编译器都支持了这种语法(可通过 -std=gnu99
指定),所以可以放心使用。其次是用到了可变参的宏,这是 C99 引入的。使用方式如下:
|
|
由于 C 中的宏仅仅只是文本替换,所以即便使用了宏,代码也还是显得有些冗余。后文会介绍到,在 Rust 中是如何用宏来简化方法调用的。
热加载
在开发过程中,热加载是非常重要的需求,不能每次重启服务来让新代码生效。但是通过 module-load 加载的动态模块,是无法卸载的,那是不是必须要重启 Emacs 呢?xuchunyang 给出了一种不需要重启的热加载方案:
|
|
该方式很巧妙,虽然已经加载的 so 不能卸载,但可通过重新加载另一个功能相同的 so 来覆盖之前的,这间接实现了热加载的效果。 在 Rust 中,还有一个更有技术含量的方案,后文会具体介绍。
Rust
使用 Rust 开发动态模块要比 C 简单不少,毕竟作为新时代的语言,单就包管理这一方面,就比 C 好用不少。这里主要会用到 emacs-module-rs 这个 crate,示例代码如下:
|
|
相比 C 代码,这里的代码简洁不少,方法的参数都是 Rust 类型,内部通过 FromLisp、IntoLisp 这两个 trait,进行 C 与 Rust 的类型转化。
通过 #[defun]
将 say_hello
函数导出到 ELisp 中,并且函数名自动加上了前缀 greeting
,并提供了相应 feature
。 cargo build
成功后,执行下面的命令:
|
|
更多使用细节可以参考官方文档,里面有非常详细的描述。
- 用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 通过执行这个方法来实现热加载。热加载相关命令如下:
|
|
这会生成 libemacs_rs_module.dylib
,它会暴露 rs-module/load
方法,用这个方法去加载其他模块即可实现热加载:
|
|
参考项目
最后,列举一些使用 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