Skip to content

通用ABI的CPP库API设计

WangBin edited this page Jun 2, 2019 · 2 revisions

ABI Universal C++ API Design

众所周知,一个C++库的ABI取决于编译器,比如gcc编译器的gnu abi,vc编译器的msvc abi,clang编译器的各种abi。同时C++标准库也会影响C++库的ABI,比如clang同时支持的libstdc++和libc++有不同的namespace,从而导致含STL类型的导出符号不一致。所以发布C++二进制库面临的一个问题是如何解决这些ABI问题。当然你可以针对不同的abi都发布一个二进制库,但这将带来维护成本。下面介绍的方法能让同一个二进制库能被不同的编译环境使用,即对库使用者的编译器、标准库无任何限制。这里暂且称之为ABI分层设计:C++ ABI层为实现部分,C ABI/API层为通用ABI部分,C++ API层为可供任意环境使用的接口。

ABI分层设计

C++ ABI层

该层代码实现了库的主要功能,并可选择导出或不导出此层的接口。这里导出符号的ABI就取决于编译器和标准库了。比如如下代码不导出符号,仅实现了功能并供C接口和内部使用

// abi/mylib.h
namespace MyLib {
    void SetName(const std::string& name);
}

C通用ABI/API层

C的ABI在一个系统里是唯一的,因此可以针对要导出的C++ ABI层接口进行一个C接口的浅封装提供给用户。比如

// c/mylib.h
MY_API void MyLib_GetName(const char* name); // MY_API: dllexport, __attribute__((visibility("default")))

实现为

#include "abi/mylib.h"
const char* MyLib_SetName() {
    MyLib::SetName(name);
}

C++ API层

由于C++可能比C更加简洁易用,所以可以在C接口的基础上再进行C++的浅封装作为对外接口。但是这里要求提供纯头文件的接口从而避免导出符号的ABI问题(或者把源码也提供给用户,就像chromium的cef一样)

// mylib.h
namespace MyLib {
    static inline void SetName(const std::string& name) {
        MyLib_SetName(name.data());
    }
}

C++ ABI层符号导出

以上例子的C++ ABI层代码未导出,所以使用者只能通过C或C++ API层进行调用。导出C++ ABI层相关接口也有其使用场景,比如:

  • 直接调用更高效率(忽略不计)
  • 便于提供C++ ABI层测试程序
  • C++ ABI层功能更丰富

于是C++ ABI层的导出接口如下

// abi/mylib.h
namespace MyLib {
    MY_API void SetName(const std::string& name);
}

C++ ABI和API层接口调用无缝切换

用户或测试程序调用MyLib::SetName()的代码如下

#include <mylib.h>
using namespace MyLib;
void test_mylib() {
    SetName("whatever");
}

若要使用ABI层接口,可以不修改用户代码,只修改编译参数-Iabi实现切换。由于C++ ABI和API层的接口是一样的,若不隐藏不必要的符号(如visibility("hidden")),则调用方也会导出相通的符号,可能导致链接的符号不符合预期。

inline namespace

为了从符号上区分C++ ABI层和API层的导出符号,可以用到C++11的inline namespace功能。这已用于标准库的,进行库的区分和版本的区分,比如libstdc++的__cxx11 inline namespace。我们可以仿照libc++对ABI层的接口增加一个namespace,abi层就变为

// abi/mylib.h
#define MYLIB_BEGIN_NS namespace MyLib {inline namespace abi {
#define MYLIB_END_NS }
MYLIB_BEGIN_NS
    void SetName(const std::string& name);
MYLIB_END_NS

api层为

// abi/mylib.h
#define MYLIB_BEGIN_NS namespace MyLib {
#define MYLIB_END_NS }
MYLIB_BEGIN_NS
    void SetName(const std::string& name);
MYLIB_END_NS

于是同样的用户代码,包含不同层的头文件其引用的符号也就不一样了,虽然表面上都是using namespace MyLib