E.7 编写 Emacs 原语

Lisp 原语是用 C 实现的 Lisp 函数。通过少量 C 宏即可完成 C 函数与 Lisp 的调用接口对接。真正理解如何编写新 C 代码的唯一方法是阅读源码,但我们可以在此说明一些要点。

特殊形式的一个示例是 eval.cor 的定义。(普通函数的整体格式与此相同。)

DEFUN ("or", For, Sor, 0, UNEVALLED, 0,
       doc: /* Eval args until one of them yields non-nil,
then return that value.
The remaining args are not evalled at all.
If all args return nil, return nil.
usage: (or CONDITIONS...)  */)
  (Lisp_Object args)
{
  Lisp_Object val = Qnil;

  while (CONSP (args))
    {
      val = eval_sub (XCAR (args));
      if (!NILP (val))
        break;
      args = XCDR (args);
      maybe_quit ();
    }

  return val;
}

我们从精确解释 DEFUN 宏的参数开始。以下是参数模板:

DEFUN (lname, fname, sname, min, max, interactive, doc)
lname

要定义为函数名的 Lisp 符号名;在上述示例中,该值为 or

fname

该函数对应的 C 函数名。这是 C 代码中调用该函数使用的名称。按照惯例,名称以 ‘F’ 开头,Lisp 名称中的所有连字符(‘-’)替换为下划线。因此,从 C 代码调用该函数时,使用 For

sname

用于存储子例程对象数据的 C 变量名,该对象在 Lisp 中表示当前函数。该结构将 Lisp 符号名传递给初始化例程,由其创建符号并将子例程对象存储为符号的定义。按照惯例,该名称始终是将 fname 中的 ‘F’ 替换为 ‘S’。

min

函数必需的最小参数数量。函数 or 的最小参数数量为 0。

max

函数接受的最大参数数量(若存在固定最大值)。或者,该值可以是 UNEVALLED,表示接收未求值参数的特殊形式;或 MANY,表示接受任意数量的已求值参数(等价于 &rest)。UNEVALLEDMANY 均为宏。若 max 为数值,则必须大于 min 且小于 8。

interactive

交互式规范字符串,用法与 Lisp 函数中 interactive 的参数一致(see 使用 interactive)。对于 or,该值为 0(空指针),表示 or 无法交互式调用。值 "" 表示函数交互式调用时不接收参数。 若值以 ‘"(’ 开头,字符串会作为 Lisp 表达式求值。示例:

DEFUN ("foo", Ffoo, Sfoo, 0, 3,
       "(list (read-char-by-name \"Insert character: \")\
              (prefix-numeric-value current-prefix-arg)\
              t)",
       doc: /* ... */)
doc

文档字符串。它使用 C 注释语法而非 C 字符串语法,因为注释语法无需特殊处理即可包含多行。‘doc:’ 标识后续注释为文档字符串。注释的起止符 ‘/*’ 和 ‘*/’ 不属于文档字符串。

若文档字符串的最后一行以关键字 ‘usage:’ 开头,该行剩余内容会作为文档用的参数列表。这样你可以在文档字符串中使用与 C 代码不同的参数名。若函数接受任意数量参数,则必须使用 ‘usage:’。

部分原语存在多个定义(每个平台一个),例如 x-create-frame。这种情况下,无需在每个定义中编写相同的文档字符串,仅一个定义包含实际文档即可,其余定义使用以 ‘SKIP’ 开头的占位符,解析 DOC 文件的函数会忽略这些占位符。

Lisp 代码中文档字符串的所有通用规则(see 文档字符串编写技巧)同样适用于 C 代码文档字符串。

文档字符串后可跟随实现原语的 C 函数的属性列表,格式如下:

DEFUN ("bar", Fbar, Sbar, 0, UNEVALLED, 0
       doc: /* ... */
       attributes: attr1 attr2 ...)

你可以依次指定多个属性。目前仅支持以下属性:

noreturn

声明 C 函数永不返回。对应 C23 的 [[noreturn]]、C11 的 _Noreturn 以及 GCC 的 __attribute__ ((__noreturn__))(see Function Attributes in Using the GNU Compiler Collection)。(内部实现中,Emacs 自身的 C 代码使用 _Noreturn,因为它可以在不支持该特性的 C 平台上定义为宏。)

const

声明函数仅检查参数,无除返回值外的其他副作用。对应 C23 的 [[unsequenced]] 以及 GCC 的 __attribute__ ((__const__))

noinline

对应 GCC 的 __attribute__ ((__noinline__)) 属性,禁止函数内联。这可能用于抵消链接时优化对栈变量的影响。

DEFUN 宏调用之后,必须编写 C 函数的参数列表(包含参数类型)。若原语接受固定数量的 Lisp 参数,则每个 Lisp 参数对应一个 C 参数,且参数类型必须为 Lisp_Object。(用于创建 Lisp_Object 类型值的各类宏和函数声明在 lisp.h 文件中。)若原语是特殊形式,则必须接收一个包含未求值 Lisp 参数的 Lisp 列表作为单个 Lisp_Object 类型参数。若原语对已求值 Lisp 参数的数量无上限,则必须仅有两个 C 参数:第一个是 Lisp 参数数量,第二个是存储参数值的内存块地址,类型分别为 ptrdiff_tLisp_Object *。由于 Lisp_Object 可以存储任意数据类型的 Lisp 对象,你只能在运行时确定实际数据类型;因此若希望原语仅接受特定类型的参数,必须使用合适的谓词显式检查类型(see 类型谓词)。

For 函数内部,局部变量 args 引用的对象由 Emacs 的栈标记垃圾回收器管理。尽管垃圾回收器不会回收从 C Lisp_Object 栈变量可达的对象,但它可能移动对象的部分组件(如字符串内容或缓冲区文本)。因此,访问这些组件的函数必须在执行 Lisp 求值后重新获取地址。这意味着代码不应保存指向字符串内容或缓冲区文本的 C 指针,而应保存缓冲区或字符串位置,并在执行 Lisp 求值后根据位置重新计算 C 指针。Lisp 求值可通过直接或间接调用 eval_subFeval 触发。

注意循环内的 maybe_quit 调用:该函数检查用户是否按下 C-g,若是则中止处理。任何可能执行大量迭代的循环都应调用该函数;本例中参数列表可能非常长。这能提升 Emacs 的响应速度,改善用户体验。

除非 Emacs 转储后变量不会被写入,否则不得对静态或全局变量使用 C 初始化器。这些带初始化器的变量分配在内存区域中,Emacs 转储后(在部分操作系统上)该区域会变为只读。See 纯净存储

仅定义 C 函数不足以让 Lisp 原语生效;还必须为原语创建 Lisp 符号,并在其函数单元中存储合适的子例程对象。代码如下:

defsubr (&sname);

其中 sname 是你用作 DEFUN 第三个参数的名称。

若你向已有 Lisp 原语定义的文件中添加新原语,找到文件末尾名为 syms_of_something 的函数,并在其中添加 defsubr 调用。若文件没有该函数,或你创建了新文件,则添加 syms_of_filename 函数(如 syms_of_myfile)。然后在 emacs.c 中找到所有此类函数的调用位置,并添加 syms_of_filename 调用。

syms_of_filename 函数也是定义需作为 Lisp 变量可见的 C 变量的位置。DEFVAR_LISP 使 Lisp_Object 类型的 C 变量在 Lisp 中可见。DEFVAR_INT 使 int 类型的 C 变量在 Lisp 中可见,且值始终为整数。DEFVAR_BOOL 使 int 类型的 C 变量在 Lisp 中可见,且值为 tnil。注意,使用 DEFVAR_BOOL 定义的变量会自动添加到字节编译器使用的 byte-boolean-vars 列表中。

这些宏均接收三个参数:

lname

Lisp 程序使用的变量名。

vname

C 源码中的变量名。

doc

变量的文档,以 C 注释形式编写。See 文档基础 查看更多细节。

按照惯例,定义原生类型(intbool)变量时,C 变量名是 Lisp 变量名将 - 替换为 _。若变量类型为 Lisp_Object,惯例是在 C 变量名前添加前缀 V。示例:

DEFVAR_INT ("my-int-variable", my_int_variable,
           doc: /* An integer variable.  */);

DEFVAR_LISP ("my-lisp-variable", Vmy_lisp_variable,
           doc: /* A Lisp variable.  */);

在 Lisp 中,有时需要引用符号本身而非符号的值。例如临时覆盖变量值时,Lisp 中使用 let,C 源码中则通过定义对应的常量符号并使用 specbind 实现。按照惯例,Qmy_lisp_variable 对应 Vmy_lisp_variable;使用 DEFSYM 宏定义该符号。

DEFSYM (Qmy_lisp_variable, "my-lisp-variable");

执行实际绑定:

specbind (Qmy_lisp_variable, Qt);

在 Lisp 中,符号有时需要引用。在 C 中实现相同效果时,同样使用对应的常量符号 Qmy_lisp_variable。例如,在 Lisp 中创建缓冲区局部变量(see 缓冲区局部变量)的写法:

(make-variable-buffer-local 'my-lisp-variable)

在 C 中,对应代码结合使用 Fmake_variable_buffer_localDEFSYM

DEFSYM (Qmy_lisp_variable, "my-lisp-variable");
Fmake_variable_buffer_local (Qmy_lisp_variable);

若希望让 C 中定义的 Lisp 变量表现得如同 defcustom 声明的变量,在 cus-start.el 中添加合适的条目。see 定义自定义变量 查看格式说明。

若直接定义文件作用域的 Lisp_Object 类型 C 变量,必须在 syms_of_filename 中调用 staticpro 保护其免受垃圾回收影响,示例:

staticpro (&variable);

以下是另一个示例函数,参数更复杂。该代码来自 window.c,演示了使用宏和函数操作 Lisp 对象。

DEFUN ("coordinates-in-window-p", Fcoordinates_in_window_p,
       Scoordinates_in_window_p, 2, 2, 0,
       doc: /* Return non-nil if COORDINATES are in WINDOW.
  ...
  or `right-margin' is returned.  */)
  (register Lisp_Object coordinates, Lisp_Object window)
{
  struct window *w;
  struct frame *f;
  int x, y;
  Lisp_Object lx, ly;

  w = decode_live_window (window);
  f = XFRAME (w->frame);
  CHECK_CONS (coordinates);
  lx = Fcar (coordinates);
  ly = Fcdr (coordinates);
  CHECK_NUMBER (lx);
  CHECK_NUMBER (ly);
  x = FRAME_PIXEL_X_FROM_CANON_X (f, lx) + FRAME_INTERNAL_BORDER_WIDTH (f);
  y = FRAME_PIXEL_Y_FROM_CANON_Y (f, ly) + FRAME_INTERNAL_BORDER_WIDTH (f);

  switch (coordinates_in_window (w, x, y))
    {
    case ON_NOTHING:            /* NOT in window at all.  */
      return Qnil;

    ...

    case ON_MODE_LINE:          /* In mode line of window.  */
      return Qmode_line;

    ...

    case ON_SCROLL_BAR:         /* On scroll-bar of window.  */
      /* Historically we are supposed to return nil in this case.  */
      return Qnil;

    default:
      emacs_abort ();
    }
}

注意,C 代码无法按名称调用非 C 定义的函数。调用 Lisp 编写的函数需使用 Ffuncall,它对应 Lisp 函数 funcall。由于 Lisp 函数 funcall 接受任意数量参数,在 C 中它接收两个参数:Lisp 层参数数量和存储参数值的一维数组。第一个 Lisp 层参数是要调用的 Lisp 函数,其余为传递给该函数的参数。

C 函数 call0call1call2 等提供了便捷调用固定参数数量 Lisp 函数的方法,它们均通过调用 Ffuncall 实现。

eval.c 是查看示例的绝佳文件;lisp.h 包含部分重要宏和函数的定义。

若你定义的函数无副作用或为纯函数,分别为其设置非 nilside-effect-freepure 属性(see 标准符号属性)。参见 ‘byte-opt.el’ 中定义的列表。