共计 5626 个字符,预计需要花费 15 分钟才能阅读完成。
上一章:宏
Emacs 从版本 25 开始反对动静模块。所谓动静模块,即 C 语言编写的共享库 1。Emacs 的动静模块,就是 Elisp 的动静模块。因而,假使 Elisp 语言编写的程序某些环节存在性能瓶颈,可借 C 语言之力予以缓解。对于其余编程语言,只有可能调用 C 程序库,皆能用于编写 Emacs 的动静模块。本章仅讲述如何应用 C 语言实现此事,所用的 C 编译器为 gcc。
又一个 Hello world!
Hello world 程序总是可能帮忙咱们疏忽大量的细节,而把握一个程序的根本面貌,这一教训对于如何编写 Emacs 动静模块仍然实用。
我倡议应用 Emacs 然而并不禁止应用其余文本编辑器创立 C 程序源文件 foo.c,在其中郑重其事地写下
#include <emacs-module.h> | |
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 <stdio.h> | |
int main(void) | |
{printf("Hello world!\n"); | |
return 0; | |
} |
Emacs 的动静模块在以上述的代码为根底,也能写出相似的 Hello world 程序。上面给出 foo.c 的全部内容:
#include <stdio.h> | |
#include <emacs-module.h> | |
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 <emacs-module.h> | |
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 <stdio.h> | |
#include <emacs-module.h> | |
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/…