EMACS-DOCUMENT

=============>集思广益

emacs-modules简介

1 dynamic-module的工作原理

Emacs25新增了一个新功能就是允许你加载动态模块. 也就是说,除了Emacs Lisp,你可以使用原生编译的代码来扩展Emacs, 使用原生代码扩展Emacs的两个优势在于:能够更好与Emacs进行整合,而且速度也更快.

所谓模块,类似于其他程序中的插件. 每个模块都被编译成共享库的形式(根据操作系统的不同,它的文件后缀可能为.so, .dylib 或 .dll). 这种共享库提供了一些函数可以供Emacs主程序调用. 插件API或者说模块的接口描述了主程序的入口点是怎样的,以及模块可以用哪些东西来与主程序进行交互.

具体来说,该API原型定义在 emacs-module.h 头文件中. 要创建一个Emacs模块无需链接任何Emacs的库或者执行程序,只需要引用该头文件然后实现该API即可.

emacs-module.h 文件定义了很多东西,其中最重要的是以下几点:

  • 一种名叫 emacs_value 的类型,用于表示与Emacs传递Lisp类型的值.
  • 一个名为 emacs_runtime 的结构体,用于获取一个指向环境的指针.
  • 一个名为 emacs_env 的结构体,该结构体包含了各种指向API的函数指针.
  • 模块主入口点的函数原型.

    extern int emacs_module_init (struct emacs_runtime *ert);
    

当加载动态模块时,Emacs会调用模块主入口点的函数,并传递給该函数一个指向"emacs运行期"的指针作为参数. 通过该指针,你可以获取到指向"emacs环境"的指针:

emacs_env *env = ert->get_environment(ert);

"emacs环境"中提供了许多函数来让你与lisp解析器进行交互. 所有的这些函数的第一个参数都需要是指向"emacs环境"的指针.

2 一个module的简单例子

下面我们会定义一个名为 mymode 的模块,该模块中定义了一个函数 mymod-test, 该函数返回整数42.

首先我们需要得到一份Emacs源码的拷贝. 因为你需要以支持module的方式来编译Emacs(默认是不支持module的), 同时你也需要 emacs_module.h 头文件.

Emacs源代码可以从这里下载: http://alpha.gnu.org/gnu/emacs/pretest/emacs-25.0.92.tar.xz

编译的命令为 ./configure --with-modules && make. 你可以通过运行 ./src/emacs -Q 来测试刚编译出来的emacs. 这里 -Q 表示Emacs启动时不要加载初始化文件.

2.1 Makefile

# 设置emacs源代码路径
# (你可以在makefile中直接设置,也可以make时在命令行中提供)
#ROOT    =
CC      = gcc
LD      = gcc
CFLAGS  = -ggdb3 -Wall
LDFLAGS =

all: mymod.so

# make shared library out of the object file
%.so: %.o
    $(LD) -shared $(LDFLAGS) -o $@ $<

# compile source file to object file
%.o: %.c
    $(CC) $(CFLAGS) -I$(ROOT)/src -fPIC -c $<

2.2 mymod.c

Emacs模块必须遵循GPL协议. 为了保证这一点,Emacs在加载模块之前,会监测该模块是否包含有名为 plugin_is_GPL_compatible 的变量.

#include <emacs-module.h>

/* 强制申明GPL符号 */
int plugin_is_GPL_compatible;

/* 定义新emacs lisp函数. 所有暴露給Emacs使用的函数都必须符合该原型 */
static emacs_value
Fmymod_test (emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data)
{
  return env->make_integer (env, 42);
}

/* 将函数Sfun以name为名称,暴露給emacs使用 */
static void
bind_function (emacs_env *env, const char *name, emacs_value Sfun)
{
  /* 使用fset函数来将SFUN函数绑定到名为NAME的符号 */

  /* 通过intern将字符串转换为symbol */
  emacs_value Qfset = env->intern (env, "fset");
  emacs_value Qsym = env->intern (env, name);

  /* 准备好参数数组*/
  emacs_value args[] = { Qsym, Sfun };

  /* 调用fset函数,这里的2为参数的个数*/
  env->funcall (env, Qfset, 2, args);
}

/* Provide FEATURE to Emacs.  */
static void
provide (emacs_env *env, const char *feature)
{
  /* call 'provide' with FEATURE converted to a symbol */

  emacs_value Qfeat = env->intern (env, feature);
  emacs_value Qprovide = env->intern (env, "provide");
  emacs_value args[] = { Qfeat };

  env->funcall (env, Qprovide, 1, args);
}

int
emacs_module_init (struct emacs_runtime *ert)
{
  emacs_env *env = ert->get_environment (ert);

  /* 创建匿名函数,该匿名函数被封装成一个emacs_value*/
  emacs_value fun = env->make_function (env,
              0,            /* 最少需要的参数个数*/
              0,            /* 支持的最大参数个数*/
              Fmymod_test,  /* 实际函数的指针*/
              "doc",        /* docstring */
              NULL          /* user pointer of your choice (data param in Fmymod_test) */
  );

  bind_function (env, "mymod-test", fun);
  provide (env, "mymod");

  /* loaded successfully */
  return 0;
}

2.3 测试

编译该模块,编译时需要将ROOT变量指向Emacs源代码目录(可以直接修改makefile,或者用类似下面的命令编译)

make ROOT=$HOME/prog/emacs-25.0.92
# gcc -ggdb3 -Wall -I/home/aaptel/prog/emacs-25.0.92/src -fPIC -c mymod.c
# gcc -shared  -o mymod.so mymod.o
# rm mymod.o

然后可以在Emacs加载并测试该模块了. 使用 -L 来将模块所在目录添加到load-path变量后.

~/prog/emacs-25.0.92/src/emacs -Q -L $PWD

在*scratch* buffer中加载该模块,然后调用 mymod-test 方法. 若一切顺利的话,应该有以下结果(一种,C-j为快捷键表示执行前面的S-Form并插入运算结果):

(require 'mymod) <C-j>
;; mymod

(mymod-test) <C-j>
;; 42