跳转至

ABI

契约就是一份文件: packages/pier-abi/include/sdk/abi.h。

它能被 C11 和 C++20 各解析一遍,CI 每次推送都两种都编。它同时也是参考文档: 每个槽位都写着参数含义、失败时返回什么、对线程有什么要求。任何语言的绑定作者 除了它没有别的可读——这正是那份头文件不受仓库其余部分注释预算约束的原因。

本页讲形状。槽位本身请读头文件。

形状

C
1
2
3
4
5
6
7
8
typedef struct PierApi {
    uint32_t struct_size;     /* 这个宿主编出来的长度 */
    uint32_t abi_version;
    uint32_t host_flags;      /* bit 0:这是客户端构建 */
    uint32_t reserved;

    /* ... 每个槽位,都是一个可空函数指针 ... */
} PierApi;

从这个头部推出三条性质:

布局在所有构建目标下相同。 PierApi 里任何地方都没有条件编译。客户端那组和维度那组 永远占着位置,只在对应包没编进来时为 NULL。

能力缺席就是槽位为 NULL。 有没有某个能力,就是它的槽是不是空——一个运行期的问题, 有运行期的答案。

struct_size 是前向兼容的全部依据。 宿主报出自己编出来的长度,模组只读这个长度以内的东西。

两道闸

调用任何非核心槽之前:

C
if (api->struct_size < offsetof(PierApi, slot) + sizeof(void*)) { /* 宿主太老 */ }
if (api->slot == NULL)                                          { /* 没编进来 */ }

缺一不可,顺序也不能反。只查非空,会在表短时读越界,而越界读回来的东西常常看起来像 一个合法的函数指针。只查长度,会在能力包没编进来时调一个空指针。

Rust 侧的 require_slot! 宏两道一起做,并产出一条指明是哪一道失败的错误。

所有权

任何跨边界的缓冲区,由产出方分配、产出方释放,接收方在回调期间读、要留就拷。

形状 谁分配 接收方能做什么
PierStr(指针 + 长度) 调用方 返回前读完,要留自己拷
Sink 回调 宿主 在 sink 内拷走,返回后指针失效
车道的 data / vtable 提供方 只在存活标志为真时调用

ABI 上永远不会返回一个需要对方释放的指针。那需要跨边界的分配器契约,而两边的 分配器不是同一个。所有输出都走 sink。

结构化数据是 SNBT

这条 ABI 上没有按值传递的结构体。事件载荷、表单、物品、方块状态、实体快照、 命令参数、服务请求,全都是 SNBT 字符串。

Rust SDK 用 NbtValue 替你解析。别的语言的绑定,要么自己写一个 SNBT 解析器, 要么走 nbt_snbt_to_binary 再用现成的 NBT 库。

线程

默认每个槽都只能在服务器线程上调,例外逐槽注明。标了线程安全的是 schedule_for、log 和 kvdb_* 这一族。

数据包拦截回调是反方向的例外:它根本不在服务器线程上跑。

演进

只增不减。新增能力就是在表尾追加,版本号不动。重排、删除或改签名, 则 PIER_ABI_VERSION 和 PIER_ABI_MIN_SUPPORTED 一起推进到同一个数。

这件事发生过一次。ABI 目前是 v2:26.32.2 删掉了 26.20.3 起废弃的四个维度槽, 两个数一起推进到 2,按 v1 编的模组在装载时被拒绝,需要用 v2 的 SDK 重编。 之后新增的东西都是追加,没有再动版本号。

tools/abi-v1.slots 是基线,abi-additive 检查每次推送都对着它比, 所以非追加式的变更没法悄悄落地。文件名沿用最初那个,版本记在文件自己里。

在真实宿主上看一眼

tools/pier-probe 是一个走完整张表的 mod,逐个槽位报告宿主有没有、答什么。 它只读,所以有玩家在线也能跑。

它同时也在证明本页的说法:这个 mod 只包含 sdk/abi.h,不链任何库,也不需要构建系统, build-msvc.bat 就是一条 cl 命令。哪天这条不再成立,说明 ABI 长出了一个它没承认的依赖。

入口点要写 PIER_MAIN_EXPORT,这个宏由头文件提供。给符号起名不等于把它导出: Windows 的 DLL 不主动要求就什么都不导出,光声明能编过、加载时才被拒。 ELF 构建默认全导出,所以这一条值得写出来,而不是留给读者自己踩。

四种判定。ABSENT 按原因分两种:超出 struct_size 是宿主比 mod 旧, 槽位为 NULL 是宿主没编进填它的那个包。REFUSED 是槽位被调用了、报告失败, 在 BDS 1.26.32 上这是一批能力的既定答案。THREW 是唯一一种永远算缺陷的结果。