上一章:宏
Emacs 从版本 25 开始支持动态模块。所谓动态模块,即 C 语言编写的共享库 1 。Emacs 的动态模块,就是 Elisp 的动态模块。因此,倘若 Elisp 语言编写的程序某些环节存在性能瓶颈,可借 C 语言之力予以缓解。对于其他编程语言,只要能够调用 C 程序库,皆能用于编写 Emacs 的动态模块。本章仅讲述如何使用 C 语言完成此事,所用的 C 编译器为 gcc。
又一个 Hello world!
Hello world 程序总是能够帮助我们忽略大量的细节,而掌握一个程序的基本相貌,这一经验对于如何编写 Emacs 动态模块依然适用。
我建议使用 Emacs 但是并不禁止使用其他文本编辑器创建 C 程序源文件 foo.c,在其中郑重其事地写下
#include
int plugin_is_GPL_compatible;
使用 C 语言为 Emacs 编写的任何一个动态模块皆以上述代码作为开头。
接下来应该写 main
函数了。每个 C 程序皆以 main
函数作为程序的入口和出口。但是,Emacs 动态模块的入口和出口不是 main
,而是
int emacs_module_init (struct emacs_runtime *ert)
{
return 0;
}
跟 C 程序的 main
函数相似,返回 0 表示成功,返回其他整型数值意味着失败。
还记得 C 程序的 Hello world 吗?
#include
int main(void)
{
printf("Hello world!\n");
return 0;
}
Emacs 的动态模块在以上述的代码为基础,也能写出类似的 Hello world 程序。下面给出 foo.c 的全部内容:
#include
#include
int plugin_is_GPL_compatible;
int emacs_module_init (struct emacs_runtime *ert)
{
printf("Hello world!\n");
return 0;
}
执行以下命令
$ gcc -I /usr/include/emacs-27 -fPIC -shared foo.c -o foo.so
便可将 foo.c 编译为共享库 foo.so。注意,上述命令里,/usr/include/emacs-27
是我机器上的 Linux 系统里 emacs-module.h
文件所在路径,不同的 Emacs 版本或不同的操作系统,需要因地制宜。
将 foo.so 放到系统变量 EMACSLOADPATH
定义的目录或 Elisp 的 load-path
列表里定义的目录里。完成上述工作,便可在 Elisp 程序里载入 foo.so,例如创建 Elisp 程序 foo.el,令其内容为
(load "foo" nil t)
然后执行
$ emacs -Q --script foo.el
可得到以下输出:
Hello world!
这就是 Emacs 动态模块的 Hello world。成功加载这个模块后,心里不禁有些小激动呢。
创建可在 Elisp 程序里调用的 C 函数
现在考虑用 C 写一个可以计算宇宙的终极答案的函数,然后在 Elisp 里调用。这样的函数称为模块函数。
在动态模块的 C 代码里,可在 Elisp 程序调用的 C 函数,其格式必须像下面这样
emacs_value func (emacs_env *env,
ptrdiff_t nargs,
emacs_value *args,
void *data)
{
}
上述代码仅仅是一个空壳函数,因为现在我还不知道 emacs_value
这个类型的返回值该如何构造。由于宇宙的终极答案是 42,经过认真阅读 Elisp 手册,我找到了一个办法。emacs_env
里有一个函数 make_integer
,用它可以构造 emacs_value
类型的实例,例如
emacs_value foo_answer (emacs_env *env,
ptrdiff_t nargs,
emacs_value *args,
void *data)
{
return env->make_integer(env, 42);
}
在尚未搞清楚 env
,nargs
,args
以及 data
等参数的含义的情况下,我已经写出 foo_answer
。学习的过程,要学会临时放弃一些东西。接下来要考虑的问题是,如何让 foo_answer
这个 C 函数变成 Elisp 体制内的函数。
Elisp 手册里提供了示例代码,我针对 foo_answer
对其略作修改并置入 emacs_module_init
函数里,如下
int emacs_module_init (struct emacs_runtime *ert)
{
emacs_env *env = ert->get_environment(ert);
emacs_value func = env->make_function(env, 0, 0, foo_answer, "", NULL);
emacs_value symbol = env->intern(env, "foo-anwser");
emacs_value args[] = {symbol, func};
env->funcall(env, env->intern(env, "defalias"), 2, args);
return 0;
}
为了方便代码复制,在本地机器上算出宇宙的终极答案,在此我不厌其烦,给出 foo.c 全部的代码:
#include
int plugin_is_GPL_compatible;
emacs_value foo_answer (emacs_env *env,
ptrdiff_t nargs,
emacs_value *args,
void *data)
{
return env->make_integer(env, 42);
}
int emacs_module_init (struct emacs_runtime *ert)
{
emacs_env *env = ert->get_environment(ert);
emacs_value symbol = env->intern(env, "foo-anwser");
emacs_value func = env->make_function(env, 0, 0, foo_answer, "", NULL);
emacs_value args[] = {symbol, func};
env->funcall(env, env->intern(env, "defalias"), 2, args);
return 0;
}
重新编译 foo.c,并将编译所得 foo.so 放到它应该在的目录里。然后,在 Elisp 程序 foo.el 里,载入 foo.so,并调用 foo-anwser
函数,即
(load "newbie" nil t)
(load "foo" nil t)
(print! (foo-anwser))
执行 foo.el 程序,
$ emacs -Q --script foo.el
可输出
42
城乡结合部
上一节的示例代码,大多数都是莫名其妙的。尽管如此,大致上它们的举动无法是将一个 Elisp 里一个体制内的符号 foo-anwser
绑定到模块函数 foo_anwser
,而真正完成此事的代码是
env->funcall(env, env->intern(env, "defalias"), 2, args);
首先看 env
,它是怎么来的?来自 Emacs 运行时 emacs_runtime
,即
emacs_env *env = ert->get_environment(ert);
emacs_runtime
怎么来的呢?是 Emacs 传给 emacs_module_init
函数的。问题追溯至此,便可以结束了。身处城乡结合部,就不必再问城市是怎么来的了。
可以再问的是,env->intern(env, "defalias")
是什么意思?是让 Elisp 解释器派遣一个符号 defalias
过来。如果 Elisp 解释器所维护的符号表里有没有这个符号,如果没有就创建一个,然后以 emacs_value
的形式封装这个符号,将其作为 env->intern
的返回值。简而言之,env->intern
返回一个符号。
由于 env->intern(env, "defalias")
是 env->funcall
的参数,那么后者拿到前者返回的符号,要做什么呢?如果前者返回的符号绑定了一个 Elisp 函数,那么 env->funcall
便可以通过这个符号调用它绑定的函数。那么,Elisp 符号 defalias
绑定的是一个 Elisp 函数吗?是的。Elisp 函数 defalias
可以用于定义一个函数,类似于 defun
,二者的区别是,defalias
是函数,而 defun
实际上是宏。env->funcall
可以调用函数,但不可以调用宏。
env->funcall
要调用 defalias
函数,就需要给它传递两个参数,一个是符号,一个是函数的定义,以下代码便是为 defalias
函数准备参数:
emacs_value symbol = env->intern(env, "foo-anwser");
emacs_value func = env->make_function(env, 0, 0, foo_answer, "", NULL);
emacs_value args[] = {symbol, func};
基于上述解释,
env->funcall(env, env->intern(env, "defalias"), 2, args);
的含义就基本上清晰了。env->funcall
调用了 Elisp 函数 defalias
,将 symbol
和 func
这两个参数传递给了 defalias
,由 defalias
在 Elisp 环境里,亦即上述代码里几乎无处不在的 env
,将一个符号 foo-anwser
绑定了一个函数 func
。
func
是怎么来的呢?它实际上是一个匿名函数,是 env->make_function
的返回值。这不奇怪,Elisp 语言可以将函数像数据一样传来传去。env->make_function
创建并返回的,实际上是一个匿名函数。
匿名函数
匿名函数也叫 Lambda 表达式。在 Elisp 语言里,几乎所有的函数本质上都是匿名函数,它们之所以有名字,是因为有符号绑定了它们。defalias
的用处就是将一个符号绑定到一个 Lambda 表达式。例如
(defalias 'foo
(lambda ()
(print! "Hello world!")))
defalias
将符号 foo
绑定了 Lambda 表达式
(lambda ()
(print! "Hello world!"))
这个 Lambda 表达式就是一个函数,可在终端里输出 Hello world!
。
如果使用 Elisp 函数 funcall
,可以调用 defalias
,将 foo
绑定到上述的 Lambda 表达式,例如
(funcall 'defalias
'foo
(lambda ()
(print! "Hello world")))
在 Emacs 的动态模块里,用 env->make_function
创建并返回的匿名函数,其定义就是符合格式要求的 C 函数。因此,上述 Elisp 代码完全可以用 Emacs 动态模块的代码予以模拟,即
#include
#include
int plugin_is_GPL_compatible;
emacs_value lambda_func (emacs_env *env,
ptrdiff_t nargs,
emacs_value *args,
void *data)
{
printf("Hello world\n");
return env->make_integer(env, 0);
}
int emacs_module_init (struct emacs_runtime *ert)
{
emacs_env *env = ert->get_environment(ert);
emacs_value symbol = env->intern(env, "foo");
emacs_value lambda = env->make_function(env, 0, 0, lambda_func, "", NULL);
emacs_value args[] = {symbol, lambda};
env->funcall(env, env->intern(env, "defalias"), 2, args);
return 0;
}
emacs_env
在 emacs_module_init
函数里,一旦从 emacs_runtime
里获得 emacs_env
,即
emacs_env *env = ert->get_environment(ert);
便相当于在 C 程序里得到了一个 Emacs 的全部功能,同时这也意味着,Emacs 可以从 C 程序里得到它想要的东西。所以,前文中我用了一个隐喻「城乡结合部」形容 emacs_env
,它的作用就是沟通 Emacs 和 C 程序,通过它,C 程序里的数据和函数可以传送到 Elisp 的世界里,反过来,Elisp 世界里的的一切也可以通过它传送到 C 程序的世界里。
在计算宇宙终极答案的 C 代码里,已经见识了使用 env->make_integer
函数将 C 程序里的数据 42 封装为 Elisp 世界里的整型数,即
env->make_integer(env, 42);
反过来,使用 env->extract_integer
函数可以从 Elisp 世界里的整型数里取出 C 程序需要的数据,例如
emacs_value foo = env->make_integer(env, 42);
int bar = env->extract_inter(env, foo);
浮点类型和字符串类型的实例也能通过 emacs_env
包含的函数在两个世界里来回转换,具体方法,可查阅 Elisp 手册 2 。
结语
Elisp 程序能够通过动态模块调用一个能够计算宇宙终极答案的 C 函数,这意味着……这个教程可能需要结束了。
- 在 Windows 系统中,共享库即动态连接库。 ↩
- https://www.gnu.org/software/...