ABI 边界与兼容策略 / ABI Boundaries and Compatibility Policy
1. ABI 是编译后仍然存在的契约
源码兼容不等于二进制兼容。一个头文件看起来只新增了成员函数,也可能改变类布局、inline 代码、符号、异常传播或分配边界。本篇目标是把 ABI 风险写成团队能执行的 policy。
2. 最简单心智模型
ABI 是已编译代码之间的二进制契约。 C++ ABI 受编译器、标准库、运行库、异常、RTTI、布局和可见性共同影响。 稳定 ABI 的常见方法是缩小边界:C 接口、不透明句柄、PIMPL、版本协商或进程隔离。
public header
-> source API
-> generated symbols
-> object layout / vtable
-> runtime ownership rules
-> binary compatibility promise3. 核心术语
C ABI:比 C++ ABI 更稳定、更容易跨编译器协作的函数调用和符号约定,常用于插件、SDK 和动态库边界。
对象布局:对象在内存中的字段顺序、对齐、填充和基类布局。它一变,旧二进制按旧偏移访问就可能出错。
vtable:虚函数调用依赖的表结构。新增、删除或重排虚函数可能改变二进制调用位置。
异常边界:异常是否允许跨动态库、编译器或语言边界传播。稳定 ABI 通常要求边界处捕获并转换成错误码或状态对象。
分配器边界:谁分配、谁释放内存的约定。跨运行库或跨模块释放对象,是 C++ SDK 里很常见的事故源。
兼容矩阵:列出旧消费者、新消费者、旧库、新库之间哪些组合必须工作,哪些组合明确不承诺。
4. 机制展开
兼容承诺要写清楚
库可以承诺源码兼容、二进制兼容、协议兼容或完全不承诺。
不写清楚时,使用者会默认你什么都承诺,维护者却按内部实现自由修改,冲突迟早发生。
常见 ABI 破坏
公开类新增数据成员、调整虚函数顺序、改变 enum 大小、改变 inline 函数语义、跨边界抛异常、跨边界释放内存,都是高风险行为。
模板和 header-only 库常把 ABI 问题转移到源码重新编译,但仍可能受 ODR 和依赖版本影响。
old-consumer/new-library 测试
ABI 兼容最实际的验证之一,是用旧版本头文件和旧 consumer 二进制,替换为新库运行。
这比只构建最新源码更接近用户升级路径。
5. 最小示例
下面的示例不是完整项目,而是为了把本篇概念落到可审查文本。阅读代码时关注它暴露了哪些边界,而不是照抄每个名字。
extern "C" {
struct mesh_context;
using mesh_status = int;
mesh_status mesh_create(mesh_context** out, const mesh_config* config);
mesh_status mesh_run(mesh_context* ctx, const mesh_input* input, mesh_output* output);
void mesh_destroy(mesh_context* ctx);
const char* mesh_version();
}6. 工程取舍
| 选择 | 收益 | 风险 | 需要的证据 |
|---|---|---|---|
| 只在本机验证 | 反馈最快 | 环境不可复现 | 仅适合草稿,不适合作为发布结论 |
| CI 快速门禁 | 阻止常见错误 | 覆盖不完整 | 明确矩阵、日志和失败归属 |
| 完整发布门禁 | 证据最强 | 成本更高 | 制品、符号、SBOM、ABI 和回滚记录 |
| 最小消费者测试 | 最接近用户 | 需要维护样例 | 独立目录、安装包、运行输出 |
7. 常见错误
- 把“当前源码树能构建”误认为“安装包能被别人消费”。
- 把 PRIVATE 实现细节通过头文件、导出符号或包配置泄漏出去。
- 只保存最终二进制,不保存构建输入、依赖锁、调试符号和版本信息。
- 只在 Debug 测试,不在 Release 或发布配置里测试。
- 把 CI 缓存当成依赖管理,把绿色状态当成发布结论。
- 遇到链接或 ABI 问题时只改 flags,不留下能够复盘的符号证据。
8. 调试与验证方法
定义公开承诺
写清楚本库承诺源码兼容、二进制兼容、协议兼容还是仅内部使用。承诺越模糊,升级事故越难判责。
observe vtable
-> isolate 异常边界
-> run minimal proof
-> store evidence建立干净环境
用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
observe 对象布局
-> isolate vtable
-> run minimal proof
-> store evidence固定工具版本
记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
observe C ABI
-> isolate 对象布局
-> run minimal proof
-> store evidence保存失败证据
失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
observe 兼容矩阵
-> isolate C ABI
-> run minimal proof
-> store evidence区分私有与公开
私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
observe 分配器边界
-> isolate 兼容矩阵
-> run minimal proof
-> store evidence审查跨平台差异
Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
observe 异常边界
-> isolate 分配器边界
-> run minimal proof
-> store evidence限制全局状态
全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
observe vtable
-> isolate 异常边界
-> run minimal proof
-> store evidence做升级测试
old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
observe 对象布局
-> isolate vtable
-> run minimal proof
-> store evidence设置回滚入口
发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
observe C ABI
-> isolate 对象布局
-> run minimal proof
-> store evidence写入 CI
能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
observe 兼容矩阵
-> isolate C ABI
-> run minimal proof
-> store evidence管理例外
允许临时豁免时必须记录原因、过期时间和补偿测试,否则门禁会慢慢失去意义。
observe 分配器边界
-> isolate 兼容矩阵
-> run minimal proof
-> store evidence保持证据总账
把构建输入、依赖锁、测试、ABI、性能、SBOM、签名和制品 hash 放在同一发布记录中。
observe 异常边界
-> isolate 分配器边界
-> run minimal proof
-> store evidence9. 本篇专属检查表
- 公开类型布局是否冻结。
- 异常是否跨 ABI 边界。
- 内存由谁分配谁释放。
- 是否有 ABI diff baseline。
- 是否测试旧消费者升级。
| 检查点 | 通过标准 | 失败时优先看哪里 |
|---|---|---|
| C ABI | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| 对象布局 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| vtable | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| 异常边界 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| 分配器边界 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| 兼容矩阵 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
10. 练习任务
- 建立一个只有一个库和一个 consumer 的最小工程,证明安装树可用。
- 故意破坏一个 PUBLIC/PRIVATE 作用域,观察消费者构建失败方式。
- 保存一次链接或 ABI 报告,并写下它能证明什么、不能证明什么。
- 把一个手工验证命令改成 CI 步骤,并让失败消息能指向具体文件或配置。
- 写一份 release evidence 小清单,列出制品、符号、依赖、测试和回滚入口。
12. 总结
《ABI 边界与兼容策略 / ABI Boundaries and Compatibility Policy》的核心不是多记几个命令,而是把 C ABI、对象布局、vtable、异常边界、分配器边界、兼容矩阵 变成可审查、可复现、可回滚的工程证据。
深化问题 1:对象布局 怎样变成证据
围绕 对象布局 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
rule: 对象布局
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 2:vtable 怎样变成证据
围绕 vtable 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
rule: vtable
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 3:异常边界 怎样变成证据
围绕 异常边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
rule: 异常边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 4:分配器边界 怎样变成证据
围绕 分配器边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
rule: 分配器边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 5:兼容矩阵 怎样变成证据
围绕 兼容矩阵 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
rule: 兼容矩阵
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 6:C ABI 怎样变成证据
围绕 C ABI 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
rule: C ABI
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 7:对象布局 怎样变成证据
围绕 对象布局 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
rule: 对象布局
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 8:vtable 怎样变成证据
围绕 vtable 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
rule: vtable
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 9:异常边界 怎样变成证据
围绕 异常边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
rule: 异常边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 10:分配器边界 怎样变成证据
围绕 分配器边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:允许临时豁免时必须记录原因、过期时间和补偿测试,否则门禁会慢慢失去意义。
rule: 分配器边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 11:兼容矩阵 怎样变成证据
围绕 兼容矩阵 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:把构建输入、依赖锁、测试、ABI、性能、SBOM、签名和制品 hash 放在同一发布记录中。
rule: 兼容矩阵
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 12:C ABI 怎样变成证据
围绕 C ABI 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:写清楚本库承诺源码兼容、二进制兼容、协议兼容还是仅内部使用。承诺越模糊,升级事故越难判责。
rule: C ABI
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 13:对象布局 怎样变成证据
围绕 对象布局 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
rule: 对象布局
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 14:vtable 怎样变成证据
围绕 vtable 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
rule: vtable
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 15:异常边界 怎样变成证据
围绕 异常边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
rule: 异常边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 16:分配器边界 怎样变成证据
围绕 分配器边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
rule: 分配器边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 17:兼容矩阵 怎样变成证据
围绕 兼容矩阵 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
rule: 兼容矩阵
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 18:C ABI 怎样变成证据
围绕 C ABI 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
rule: C ABI
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 19:对象布局 怎样变成证据
围绕 对象布局 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
rule: 对象布局
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 20:vtable 怎样变成证据
围绕 vtable 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
rule: vtable
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 21:异常边界 怎样变成证据
围绕 异常边界 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
rule: 异常边界
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception