使用 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 Modules•Emacs 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 SQLite•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