欢迎光临
我们一直在努力

Elisp 11:动态模块又一个 Hello world!创建可在 Elisp 程序里调用的 C 函数城乡结合部匿名函数emacs_env结语

上一章:

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);
}

在尚未搞清楚 envnargsargs 以及 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,将 symbolfunc 这两个参数传递给了 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 函数,这意味着……这个教程可能需要结束了。


  1. 在 Windows 系统中,共享库即动态连接库。
  2. https://www.gnu.org/software/…

https://segmentfault.com/a/1190000039802460

赞(0)
未经允许不得转载:ITyet » Elisp 11:动态模块又一个 Hello world!创建可在 Elisp 程序里调用的 C 函数城乡结合部匿名函数emacs_env结语
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址