跳转至

为什么有 Pier

Pier 的前身是 levilamina-rust-loader, 当初只为回答一个问题:LeviLamina 的模组能不能用 Rust 写?

能。而回答它的过程带出了一个更难的问题,那才是 Pier 要解决的。

那个装载器错在哪

它的接口是一次加一个函数长出来的——需要什么就加什么。这在底下的形状还不重要的时候管用, 等它开始重要,四件事就错了,而且是补丁补不掉的错。

布局按构建目标分岔

函数表里的条件编译,让它的尾部在客户端构建和服务端构建上落在不同的偏移。

为此在版本号高位加了个标记补救。可它护不住任何一个没重新编译的模组:分岔之前编的模组 照常装载,而分岔点之后的每一个槽位都打到了错的函数指针上。两边都编得过, 症状是一次调用做了件毫不相干的事。

有一次错开了七个槽。没有任何东西报出来,因为没有任何东西能报。

中心直接呼叫每个能力包

派发表里写着各个包的函数名,而每一个这样的名字都是一条从中心伸向能力包的链接边。 于是把某个能力从构建里去掉,链接就断了。

「可选」因此是文档里的一个词,不是代码的性质——那些被写成可选的包,实际上删不掉。

读不到的值变成了看起来合理的值

一个读不出来的字段返回 0,而对大多数被读的东西来说,0 是个合法答案。

最清楚的一例:土地保护模组读不出事件载荷里的维度,写了 unwrap_or(0), 于是每一个这样的事件都被当成发生在主世界。保护在主世界里拒绝,在别的地方全部放行。 一行日志都没有。在没人去自定义维度里测之前,这个行为看起来完全像是有意为之。

事件名做子串匹配

事件 id 是用子串搜索解析的。上游哪天新增一个和某个合成事件共享词干的事件, 所有订阅那个真事件的人都会被静默劫持到载荷形状完全不同的合成事件上,而且没有办法再订回去。

这一条没人撞上。它只是在那儿等着。

这些加起来说明了什么

单独看每一条都能修,也确实补过几个。但合起来它们说明的是一件补丁修不了的事: 这个接口从来不是设计出来的,是长出来的,而那些补丁是在守一个本就不该存在的形状。

那个装载器是测试版。测试版的作用就是告诉你该怎么正经地建,它做到了。它不再维护。

Pier 做了什么不一样的

Pier 把顺序倒了过来:先写头文件,后写实现。上面四个问题,每一个都从「要记住的规矩」 变成了结构性的性质:

原来的问题 换成了什么
布局按目标分岔 一份布局通吃所有目标;能力缺席就是槽位为 NULL
中心直呼能力包 能力包经 SPI 向内注册;删掉一个,宿主代码一行不动
读不到变成看起来合理 没有东西用答不上来的值作答;「问不出来」与「答案是否」分开
子串匹配 精确 id 或带命名空间的后缀,一个匹配器,不下落

这几条各有一个检查脚本,放在 tools/checks/,每次推送都会运行。这些脚本是在一条写好了、大家也都相信的规则被发现在四个包上失效之后才加上的;每个脚本都写明了它覆盖什么、看不到什么。

是怎么设计的 逐个讲这些机制。