现象描述

若导出的 API 使用了 C++ 标准库类型(如 std::vectorstd::mapstd::stringstd::shared_ptr 等),在不同编译环境、编译器、标准库版本 .so 调用时可能出现编译、链接或运行时异常,主要表现为:

链接符号未定义(undefined symbol)
模板实例化或类型信息不一致导致崩溃
接口数据结构错乱、内存访问异常
结构体成员内容失真,数据读取错误

ABI 与 API 的区别

API(应用编程接口):即头文件中的接口、类型和函数声明,描述了编译器和程序员可以用来编程的内容。

ABI(应用二进制接口):是编译后实体(如对象布局、名称修饰、虚表等)的具体二进制格式,决定不同二进制模块(如动态库、应用)之间能否兼容并联动。

库 API + 编译器 ABI = 库 ABI
不同编译器(或同一编译器的不同版本/不同参数)生成的 ABI 规则可能不同,同样的 API 编译出来二进制层面可能不兼容。
只有库 API 和编译 ABI 规范都一致,最终的库 ABI 才和应用程序期待的一致。二者只要有一方不一致,库 ABI 就不一样,直接影响到二进制兼容问题。

进一步说明

  • C++ 的 ABI 由 多方面共同决定。 编译器实现、标准库实现、编译选项
  • 不同操作系统、编译器(GCC/Clang/MSVC)、标准库版本,以及不同 -std=c++XX/-O2/-g/各种 ABI 宏参数,均会影响生成的二进制布局。

编译器实现不同:分别用 GCC 和 MSVC 这两种编译器来编译。
GCC 符号:_Z8set_nameR6PersonRKSs
MSVC 符号:?set_name@@YAXAUPerson@@AAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z

标准库实现不同:C++11 前后一些关键类的名字在 ABI 层面生了改变(比如 std::string 和 std::list),导致 C++11 之前和之后编译出来的目标文件不兼容。换句话说如果某个库的提供方使用的是 C++11 之前的 ABI 编译的,那么依赖这个库的项目必须也用旧的 ABI 编译。

编译选项不同:GCC 的 _GLIBCXX_USE_CXX11_ABI,不同设置会导致 std::stringstd::vector 等内部结构变化。

STL 库实现差异导致的 ABI 不兼容

现象描述

在编译 so 时使用的是 LLVM libc++,在另一平台使用该 so 时,编译报链接错误,缺少在 std::__ndk1 下对应的实现(so 中提供的是 std::__1 下的实现)。该现象存在于使用 C++ 的 STL 库的地方。

std::__1 是 LLVM libc++(比如 macOS、iOS、部分非 NDK 构建下)的内部命名空间。

std::__ndk1 是 Android NDK 下 LLVM libc++ 实现的 STL(标准模板库)内部命名空间。

__1 与 __ndk1 的代码和接口几乎一样,但 二进制符号完全隔离,任何跨命名空间混用都是致命错误。

使用命令 nm -DC xxx.so | grep shared_ptr 查看


std::__1
std::__ndk1
适用平台
macOS/iOS/Xcode/部分Linux
Android NDK
头文件实现路径
cxx-stl/llvm-libc++/include
cxx-stl/llvm-libc++/include
名字空间
std::__1
std::__ndk1
二进制符号名
mangled __1
mangled __ndk1
兼容性
不能混用
不能混用

std::__1 vs std::__ndk1 是库 ABI 差异的一种(属于标准库实现者特意增加的符号级 ABI 隔离策略),而不是库 API 差异,也不是单纯的编译器 ABI 差异。

具体示例

std::shared_ptr

_// 实现_
void EventChannel::setStreamHandler(std::shared_ptr<StreamHandler> _handler_)
{
    auto requestHandler =
    std::make_shared<IncomingStreamRequestHandler>(handler, messenger_, name_);
    if (taskQueue_ != nullptr) {
    messenger_->setMessageHandler(name_, requestHandler,
    std::shared_ptr<BinaryMessenger::TaskQueue>(taskQueue_));
    }
    else {
    messenger_->setMessageHandler(name_, requestHandler);
    }
}
_// 调用_
mTestEventHandler = std::make_shared<MyStreamHandler>();
mTestEventChannel->setStreamHandler(mTestEventHandler);

std::shared_ptr   std::string

_// 实现_
EventChannel::EventChannel(std::shared_ptr<BMessenger> messenger, const std::string &channelName)
    : EventChannel(messenger, channelName, nullptr) {}

_// 调用_
std::shared_ptr<DMessenger> messenger = executor->getDMessenger();
my_class::EventChannel eventChannel(messenger, "com.example.stream");

解决思路

  • 新旧 ABI 共存(只适用于 GCC(libstdc++)系列的 STL):
    通过在新实现的命名空间加 std::__cxx11:: 前缀(inline namespace),新老实现的类型获得不同的符号。例如:
      std::__cxx11::list<int> vs std::list<int>

  • 控制 ABI 切换的宏(只适用于 GCC(libstdc++)系列的 STL):
    -D_GLIBCXX_USE_CXX11_ABI=1(默认,为新 ABI),设为 0 则用老 ABI。不同源码文件可以单独选择使用新/旧 ABI。

  • 采用 C 接口

    1. C 接口是明确规范的二进制接口,任何平台都能兼容,是业界跨模块/语言边界的标准方案。
    2. C++ 可以自由引用 C 的代码而不出现二进制兼容问题。

官方建议
编译相互依赖的代码时,不要混用新旧 ABI 产物
ABI Policy and Guidelines

最佳方案

  • 对外只暴露 C 接口
  • 标准库/模板/复杂对象全部只在 C++ 内部使用
  • 修改代码生成新 so 后,与原先 so 进行 ABI 检查,确保兼容性
  • 可参考开源项目的 C 接口暴露方法,如 RocksDB

简单修改示例

std::vector 封装

头文件(C 接口)

//vector_c_api.h
typedef void* VectorHandle;
VectorHandle vector_create();
void vector_destroy(VectorHandle v);
void vector_push_back(VectorHandle v, int value);
int vector_get(VectorHandle v, size_t index);
size_t vector_size(VectorHandle v);

C++ 实现(so 内部)

#include "vector_c_api.h"
#include <vector>

struct VectorWrapper {
    std::vector<int> vec;
};
VectorHandle vector_create() {
    return new VectorWrapper();
}
void vector_destroy(VectorHandle _v_) {
    delete static_cast<VectorWrapper*>(v);
}
void vector_push_back(VectorHandle _v_, int _value_) {
    static_cast<VectorWrapper*>(v)->vec.push_back(value);
}
int vector_get(VectorHandle _v_, size_t _index_) {
    return static_cast<VectorWrapper*>(v)->vec[index];
}
size_t vector_size(VectorHandle _v_) {
    return static_cast<VectorWrapper*>(v)->vec.size();
}

具体代码分析

由于在我的项目中遇到的是 STL 库实现差异导致的 ABI 不兼容,所以仅针对 STL 库进行修改。

思路是把 STL 形参改为 C 语言形式的形参,然后函数内部转换回 STL 形式。

示例 1

原始代码

void EventChannel::setStreamHandler(std::shared_ptr<StreamHandler> _handler_)
{
    auto requestHandler =
    std::make_shared<IncomingStreamRequestHandler>(handler, messenger_, name_);
    if (taskQueue_ != nullptr) {
    messenger_->setMessageHandler(name_, requestHandler,
    std::shared_ptr<BinaryMessenger::TaskQueue>(taskQueue_));
    }
    else {
    messenger_->setMessageHandler(name_, requestHandler);
    }
}

修改后

void EventChannel::setStreamHandler(StreamHandler *_handler_)
{
    auto handler_sp = std::shared_ptr<StreamHandler>(handler, [](StreamHandler *) {});
    auto requestHandler =
    std::make_shared<IncomingStreamRequestHandler>(handler_sp, messenger_, name_);
    if (taskQueue_ != nullptr) {
        messenger_->setMessageHandler(name_, requestHandler,
        std::shared_ptr<BinaryMessenger::TaskQueue>(taskQueue_));
    }
    else {
        messenger_->setMessageHandler(name_, requestHandler);
    }
}

示例 2

原始代码

void showSystemOverlays(std::vector<const std::string> _overlays_) {
    if (overlays.empty()) {
        ALOGD("showSystemOverlays overlays is null");
        return;
    }
    ALOGD("showSystemOverlays called");
}

修改后

void showSystemOverlays(const char* const* _overlays_, size_t _count_) {
    if (overlays == nullptr || count == 0) {
        ALOGD("showSystemOverlays overlays is null or count==0");
        return;
    }
    std::vector<std::string> overlay_list;
    overlay_list.reserve(count);
    for(size_t i=0; i<count; ++i) {
_        // NULL_
        if (overlays[i])
        overlay_list.emplace_back(overlays[i]);
        else
        overlay_list.emplace_back();
    }
    ALOGD("PlatformMessageHandlerImpl::showSystemOverlays called, overlays count = %zu", count);
}

修改模板

std::shared_ptr

_//h_
void process_foo(void* _foo_);

_//cpp_
class Foo {};
void process_foo(void* _foo_) {
    Foo* p = static_cast<Foo*>(foo);
_    // pC++_
}

std::string

_//h_
void process_string(const char* _str_);

_//cpp_
void process_string(const char* _str_) {
std::string s(str);_ // std::string_
_// s_
}

std::vector

_//h_
void process_numbers(const int* _arr_, size_t _count_);
void process_strings(const char* const* _arr_, size_t _count_);

_//cpp_
void process_numbers(const int* _arr_, size_t _count_) {
    std::vector<int> v(arr, arr + count);
_    // ..._
}
void process_strings(const char* const* _arr_, size_t _count_) {
    std::vector<std::string> vec;
    vec.reserve(count);
    for (size_t i = 0; i < count; ++i) {
        if (arr[i]) vec.emplace_back(arr[i]);
        else vec.emplace_back();
    }
_    // ..._
}

std::map

_//h_
typedef struct {
    const char* key;
    int value;_ // or double/const char*_
} MapItem;
void process_map(const MapItem* _items_, size_t _count_);

_//cpp_
void process_map(const MapItem* _items_, size_t _count_) {
    std::map<std::string, int> m;
    for (size_t i = 0; i < count; ++i) {
        m[items[i].key] = items[i].value;
    }
_    // ...C++ map_
}

ABI 检测

使用工具

abigail-tools

安装

sudo apt install abigail-tools

基本命令

abidiff libfoo_1.so libfoo_2.so

返回值

参考:abidiff

名称
位值
说明
ABIDIFF_ERROR
1
有错误
ABIDIFF_USAGE_ERROR
2
用法错误(必须带 ABIDIFF_ERROR)
ABIDIFF_ABI_CHANGE
4
有 ABI 变化
ABIDIFF_ABI_INCOMPATIBLE_CHANGE
8
有 ABI 不兼容变化(必须带 ABI_CHANGE)

常见情况对 ABI 的影响

变化类型
是否破坏 ABI
说明
增加/删除公有接口
删除/修改才破坏
增加一般安全
结构体成员增删/重排

会导致对象布局变化
虚表/多态函数增删/重排

运行时出错
静态/全局变量符号增删

链接错误
编译选项/宏改变
可能
看是否影响布局/符号
STL/三方库升级
可能
与所用编译器相关