必须用extern "C" + opaque pointer封装C++实现,头文件仅含typedef struct MySdkHandle MySdkHandle;及纯C函数声明,禁止导出任何C++类型、STL容器或成员函数,MySdkHandle须为固定大小POD类型,且头文件需能被gcc -x c直接编译。

不能靠“尽量不改”来保 ABI 兼容性,必须从第一天就放弃暴露 C++ 类型、虚函数、模板和 STL 容器——否则任何一次看似安全的升级都可能让下游程序在 std::string::~string() 里崩溃。
用 extern "C" + opaque pointer 封装所有 C++ 实现
这是唯一能真正落地的长期 ABI 稳定方案。C 接口天然规避名字修饰、虚表、异常传播、std::vector 内存布局等全部雷区。
- 头文件里只写
typedef struct MySdkHandle MySdkHandle;,绝不定义结构体内容 - 构造函数是
MySdkHandle* my_sdk_create(const char* config),内部用new MyClass并return reinterpret_cast<mysdkhandle>(ptr)</mysdkhandle> - 所有方法都带
MySdkHandle*参数,如int my_sdk_process(MySdkHandle*, const uint8_t*, size_t) - 析构必须显式:
void my_sdk_destroy(MySdkHandle*),内部delete reinterpret_cast<myclass>(h)</myclass> - 头文件必须能被
gcc -x c直接编译通过——这是 ABI 稳定性的最低门槛
禁止在导出接口中出现任何 C++ 语言特性
哪怕只是头文件里多写一个 std::string 或 class 关键字,ABI 就已经不可控了。不是“建议避免”,而是“绝对禁止”。
- 禁止导出含
std::vector、std::shared_ptr、std::string成员的类(哪怕只在头文件声明) - 禁止函数返回或接收 C++ 类型:不能有
MyClass foo()、void bar(std::string s) - 禁止使用
extern "C"包裹 C++ 成员函数——它不会让成员函数变成 C ABI,只会让链接失败或行为未定义 - 禁止宏定义中隐含 C++ 类型,例如
#define MY_API_RETURN std::error_code - 所有字符串传参统一用
const char*+size_t,所有权由调用方完全控制
用静态断言和 abi-dumper 在 CI 中阻断 ABI 破坏
人工 review 永远漏掉 offsetof 偏移变化或虚函数表重排。必须把 ABI 验证变成门禁级检查。
立即学习“C++免费学习笔记(深入)”;
- 在关键结构体中插入编译期校验:
_Static_assert(offsetof(Config, timeout_ms) == 8, "timeout_ms offset changed"); - CI 流水线中自动生成 ABI 快照:
abi-dumper libmysdk.so -o abi-$(git rev-parse --short HEAD).dump - 对比新旧版本:
abi-compliance-checker -l mysdk -old abi-v1.2.0.dump -new abi-v1.3.0.dump - 报告中只要出现
CHANGED STRUCTURE或REMOVED SYMBOL,就自动拒绝合并 - 发布时强制使用带版本号的 SONAME:
libmysdk.so.1.3.0,并确保libmysdk.so.1是符号链接而非硬链接
类内部可自由重构,但 PIMPL 的指针偏移必须永远固定
PIMPL 不是万能的——如果 sizeof(Impl) 变了,或者你把 std::unique_ptr<impl></impl> 换成裸指针,sizeof(MySdkHandle) 就会变,ABI 依然破坏。
-
MySdkHandle必须是 POD 类型,且大小永远为 8 字节(64 位平台)或 4 字节(32 位平台) - 推荐直接定义为
typedef void* MySdkHandle;,而不是struct—— 更难出错 - 若坚持用
struct,则必须在头文件中用_Static_assert(sizeof(MySdkHandle) == sizeof(void*), "...");锁死 - Impl 的内存分配方式可以变(malloc/new/arena),但 handle 本身的二进制形态不能变
- 不要试图用
std::aligned_storage或std::byte[]在 handle 里内嵌 Impl——这等于自己实现一个不稳定的小堆,毫无必要
最常被忽略的一点:ABI 稳定性不取决于你写了什么,而取决于你**导出了什么**。哪怕内部全是 C++20 概念和模块,只要头文件里只有 extern "C" 函数和 opaque pointer,下游就永远不会感知到。真正的长期演进,是从导出接口的第一行代码开始就拒绝任何 C++ 表面语法的诱惑。


















