Lisp 原语是用 C 实现的 Lisp 函数。通过少量 C 宏即可完成 C 函数与 Lisp 的调用接口对接。真正理解如何编写新 C 代码的唯一方法是阅读源码,但我们可以在此说明一些要点。
特殊形式的一个示例是 eval.c 中 or 的定义。(普通函数的整体格式与此相同。)
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)
要定义为函数名的 Lisp 符号名;在上述示例中,该值为 or。
该函数对应的 C 函数名。这是 C 代码中调用该函数使用的名称。按照惯例,名称以 ‘F’ 开头,Lisp 名称中的所有连字符(‘-’)替换为下划线。因此,从 C 代码调用该函数时,使用 For。
用于存储子例程对象数据的 C 变量名,该对象在 Lisp 中表示当前函数。该结构将 Lisp 符号名传递给初始化例程,由其创建符号并将子例程对象存储为符号的定义。按照惯例,该名称始终是将 fname 中的 ‘F’ 替换为 ‘S’。
函数必需的最小参数数量。函数 or 的最小参数数量为 0。
函数接受的最大参数数量(若存在固定最大值)。或者,该值可以是 UNEVALLED,表示接收未求值参数的特殊形式;或 MANY,表示接受任意数量的已求值参数(等价于 &rest)。UNEVALLED 和 MANY 均为宏。若 max 为数值,则必须大于 min 且小于 8。
交互式规范字符串,用法与 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: /* ... */)
文档字符串。它使用 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_t 和 Lisp_Object *。由于 Lisp_Object 可以存储任意数据类型的 Lisp 对象,你只能在运行时确定实际数据类型;因此若希望原语仅接受特定类型的参数,必须使用合适的谓词显式检查类型(see 类型谓词)。
在 For 函数内部,局部变量 args 引用的对象由 Emacs 的栈标记垃圾回收器管理。尽管垃圾回收器不会回收从 C Lisp_Object 栈变量可达的对象,但它可能移动对象的部分组件(如字符串内容或缓冲区文本)。因此,访问这些组件的函数必须在执行 Lisp 求值后重新获取地址。这意味着代码不应保存指向字符串内容或缓冲区文本的 C 指针,而应保存缓冲区或字符串位置,并在执行 Lisp 求值后根据位置重新计算 C 指针。Lisp 求值可通过直接或间接调用 eval_sub 或 Feval 触发。
注意循环内的 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 中可见,且值为 t 或 nil。注意,使用 DEFVAR_BOOL 定义的变量会自动添加到字节编译器使用的 byte-boolean-vars 列表中。
这些宏均接收三个参数:
lnameLisp 程序使用的变量名。
vnameC 源码中的变量名。
doc变量的文档,以 C 注释形式编写。See 文档基础 查看更多细节。
按照惯例,定义原生类型(int 和 bool)变量时,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_local 和 DEFSYM:
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 函数 call0、call1、call2 等提供了便捷调用固定参数数量 Lisp 函数的方法,它们均通过调用 Ffuncall 实现。
eval.c 是查看示例的绝佳文件;lisp.h 包含部分重要宏和函数的定义。
若你定义的函数无副作用或为纯函数,分别为其设置非 nil 的 side-effect-free 或 pure 属性(see 标准符号属性)。参见 ‘byte-opt.el’ 中定义的列表。