加一门语言¶
一份文件就够了:packages/pier-abi/include/sdk/abi.h。它能被 C11 解析,FFI 工具直接吃。
这个仓库里没有别的东西是绑定的依赖。
参考实现是 bindings/rust/pier-sys-rs,也就是第一个官方绑定。先读这四步,再读那个 crate;
不要照抄它,因为契约是那份头文件,不是任何一个绑定。
四步¶
一、只读 sdk/abi.h¶
头文件就是产品。仓库里其余的一切都只是它的一个实现。
二、无条件镜像 PierApi¶
每一个字段都要声明,不带任何目标分支。布局在所有构建目标下完全相同, 能力缺席表现为槽位是 NULL,而不是字段消失。
这是四条里违反后果最严重的一条。当年有一份带条件的字段集,让表尾的偏移按目标漂移, 分岔点之后的每一个槽都打到了错的函数指针上,而两边都编得过。别把它带回来。
三、导出 pier_main¶
| C | |
|---|---|
往 out 里填 struct_size、abi_version、mod_flags 和三个生命周期回调。
宿主只找这一个符号,找不到就明确拒绝装载,不做任何回退。
struct_size 填的是你这一侧编出来的长度,宿主只读这个长度以内的字段。
这正是 vtable 日后能追加回调而不升 ABI 版本的原因。
四、每个非核心调用都过两道闸¶
两道闸,顺序不能反:
| C | |
|---|---|
只查非空,会在宿主更老、表更短时读越界,而越界读回来的东西常常看起来像一个合法的 函数指针。只查长度,会在表够长但能力包没编进宿主时调一个空指针。
三条不可商量的规矩¶
跨边界的缓冲区,由产出方分配、产出方释放。 接收方只在回调期间读,要留就拷贝。 ABI 上永远不会返回一个需要对方释放的指针,因为那需要跨边界的分配器契约, 而两边的分配器不是同一个。所有输出都走 sink。
除非头文件另有说明,每个槽都只能在服务器线程上调。 例外逐槽注明。
panic 或异常不许跨回来。 无论你的语言把栈展开叫什么,在边界上把它拦住。
ABI 版本不变,绑定的公开接口就一样都不删。 要去掉的东西先标为弃用:用这门语言自己的机制, 没有机制就写成文档说明,并指出替代品。等 ABI 版本变化的那个版本再删,那时所有模组本来就要重新编译。 唯一的例外是声明本身就不安全的接口:立刻修正,但在语言允许的范围内保持旧写法仍能编译。完整规则见契约 §2.5。
握手¶
宿主按这个顺序查三件事,你这一侧最好也查同样三件,这样从任何一边的日志都能解释失败:
- 长度。 表短到连核心槽都覆盖不到时,后两项根本读不出来。
- 版本区间。
MIN_SUPPORTED <= 你的 abi_version <= 宿主的。兼容是区间不是相等。 - 目标标志。
mod_flags与host_flags的 bit 0 必须相等。不匹配就明确拒绝, 而不是放它进来、然后在第一个只有一侧才有的槽上崩掉。
绑定放在哪¶
bindings/<语言>/,不进主仓库的必编列表。Pier 维护的是契约。
CONTRACT.md 第十节是本页的
权威版本。