原生模块
EKP 可以用 C 动态模块代替 Emacs Lisp 计算断行。模块是可选的:它是包内 native/ 目录的构建产物,需要显式加载,而且它产出的每个结果在使用前都会对照 Emacs Lisp 引擎校验。它买到的是速度——用编译代码运行同一个动态规划,并并行规划多段落字符串的各段——而它绝不能改变布局,因此围绕它的接口很小,失败模式也很明确。
本章定义模块的构建与加载命令,以及报告后端破坏契约的条件。决定已加载模块何时被使用的选项是 ekp-use-c-module 与 ekp-auto-justify-native-append,记录在参数参考;模块所计算的模型见断行原理。
模块只加速排版流水线中的一步:断点搜索。一切与字体有关的部分都留在 Emacs Lisp——切分、测量、盒子与胶的数组、断字以及缓存键——并以数组的形式传给模块。因此模块在同样的数据上运行同一个 Knuth-Plass 动态规划,这也是为什么可以要求它的结果在两者都可用时与 Emacs Lisp 引擎完全一致。
模块是包内 native/ 目录里的共享库,名为 ekp 加平台后缀。它由该目录里的 make 构建;构建 profile 选择编译设置,从可移植的优化构建,到带链接时优化的本机 CPU 构建,再到调试构建与开启地址及未定义行为消毒器的构建。构建从不隐式发生:模块由显式命令加载,require EKP 不会加载它。
加载之后,模块服务两条路径。单段落排版把准备好的数组交给单段入口。含多个段落的字符串走批量入口,按每段一个任务交给一池工作线程处理;线程池在第一次这样的批量时惰性创建,大小跟随机器的处理器数并封顶在固定上限。队列满时提交线程会被阻塞,而不是丢下一个段落。
模块带版本,EKP 要求一个最低版本。低于它的模块在加载时被停用而不是继续使用,从而防止升级后的 Elisp 层调用已经变更的接口。模块缺失、过旧、或返回「没有结果」这类软失败时,改由 Emacs Lisp 引擎计算该段落,因此排版总是可用。包的一致性测试让两个引擎互相校验。
已加载的模块只在它返回的数据范围内被信任。每个非 nil 结果都会被校验:断点列表必须是递增的盒子序号列表、以段落的盒子总数为终点,代价必须是数字。校验失败的结果会抛契约错误而不是被渲染。nil 结果与 nil 断点列表是已记录的软失败,含义是「回退到 Emacs Lisp」而非错误;这个区别很重要,因为部分失败绝不能静默地产出不同布局。
Function: ekp-c-module-build (&optional profile)
Section titled “Function: ekp-c-module-build (&optional profile)”用 make 按 PROFILE 构建 C 模块。
参数。 PROFILE 是 portable(默认)、native、debug 或 sanitize 之一。portable 是到处都能跑的优化构建;native 加上构建机器的指令集与链接时优化;debug 与 sanitize 用速度换诊断信息。交互调用弹出 profile 提示,预选 portable。
语义。 构建在包的 native/ 目录里异步运行 make PROFILE=…,因此命令在编译器工作时就返回,构建输出进入专用缓冲区。构建成功退出时会加载刚构建出的模块并报告结果;失败时显示构建缓冲区里的编译器输出并报告退出状态。因为成功会重新加载模块,一条命令同时完成编译与启用。
返回。 未指定;构建及成功后的加载就是效果。
错误。 PROFILE 不是四个名字之一时 error→ user-error,消息为 Unknown EKP C build profile: PROFILE;包内没有 native/Makefile 时消息为 Makefile not found in native/ directory;找不到 make 时消息为 The make executable is not available。编译器失败不是错误信号:它报告在构建缓冲区里。
示例。
(ekp-c-module-build 'native) ⇒ compilation starts in the background and, on success, loads the built module参见。 ekp-c-module-load、ekp-backend-contract-error。
Function: ekp-c-module-load ()
Section titled “Function: ekp-c-module-load ()”加载 EKP C 模块(若可用)。
参数。 无。
语义。 它在包的 native/ 目录里按平台后缀寻找模块库。文件不存在时报告这一点并让模块保持未加载。文件存在时,它从临时副本加载,以便本会话早先重新构建的模块可以用同一条命令覆盖旧模块,随后初始化并检查版本是否达到本 EKP 要求的最低版本。低于最低版本时模块被停用,消息提示重新构建并说明将改用 Emacs Lisp;兼容的模块被启用并报告版本与工作线程数。加载是显式且幂等的:require 包从不做这件事。
返回。 未指定;启用或停用模块就是效果。
错误。 自身没有错误。包括模块缺失或过旧在内的每种结局都在回显区报告,同时 Emacs Lisp 引擎始终可用。
参见。 ekp-c-module-build、ekp-use-c-module。
模块与 Emacs Lisp 引擎通过一个值契约对话:一次计算返回断点列表与总代价的 cons,或者 nil。契约有三类失败,对应三种不同响应。整体结果为 nil 或断点列表为 nil 是软失败,回退到 Emacs Lisp,因为它可以合法地表示该段落无法用模块的接口表示。非 nil 的畸形结果是契约违规并抛条件,因为渲染它就等于渲染两个引擎都没有选过的布局。模块自身抛出的信号原样穿过公共格式化器;分发层不会抓住并隐藏它。
这一设计使快速路径永远不会变成另一种算法:模块缺失、不兼容或部分失败时降级为 Emacs Lisp 的结果,模块返回胡说时中止调用而不是发布胡说。
Error: ekp-backend-contract-error
Section titled “Error: ekp-backend-contract-error”畸形的 C 后端结果。
语义。 原生模块返回非 nil 且未通过校验的结果时抛出该条件。数据是列表,首元素命名失败类型,第二元素是出问题的结果:结果不是 cons 时为 malformed-result,代价不是数字时为 nonnumeric-cost,断点列表不是递增的盒子序号列表、不以段落盒子总数为终点时为 malformed-breaks。nil 结果与 nil 断点列表被刻意排除:它们回退到 Emacs Lisp,不抛条件。
恢复。 这种不匹配意味着已加载的模块不属于这层 Emacs Lisp,最常见的原因是升级后模块过期。用 ekp-c-module-build 重新构建,或在 native/ 里用 make 重建后调用 ekp-c-module-load;在那之前,把 ekp-use-c-module 设为 nil 强制使用 Emacs Lisp 引擎,包仍可正常使用。畸形结果没有被发布,因此没有布局状态需要修复。
参见。 ekp-c-module-load、ekp-c-module-build、ekp-use-c-module。
本章没有任何东西是正确排版所必需的。模块缺失、模块不兼容与软失败都会让 Emacs Lisp 引擎接手,所以快速路径的失败模式是排版变慢,而不是排不出来。构建命令是唯一对用户失误抛条件的入口,而且都在任何编译开始之前:未知 profile、缺少 Makefile、缺少 make;编译器自身通过它的缓冲区报告。
契约错误是唯一会中止调用的结局,而且是刻意的。它意味着模块返回了两个引擎都没有选择的东西,因此安全的响应是拒绝该结果并让调用方修好模块:重新构建、重新加载,或用 ekp-use-c-module 设为 nil 在本会话停用它。重新加载成本很低,而从临时副本加载意味着重建之后无需重启 Emacs。