关于lisp:Elisp-11动态模块

43次阅读

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

在尚未搞清楚 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/…

正文完
 0