1 问题简介
正常情况下,dlopen 和 dlsym 是用来处理 C 库中的函数的,但对 C++ 来说,情况稍微复杂,如在 Android framework media 框架中加载 C++ 软解库组件时使用到 dlsym 来链接函数符号
typedef SoftOMXComponent *(*CreateSoftOMXComponentFunc)(
const char *, const OMX_CALLBACKTYPE *,
OMX_PTR, OMX_COMPONENTTYPE **);
CreateSoftOMXComponentFunc createSoftOMXComponent =
(CreateSoftOMXComponentFunc)dlsym(
libHandle,
"_Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPE"
"PvPP17OMX_COMPONENTTYPE");
这个函数符号名 “_Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPE” 跟 C++ 库中实际定义的函数名 “createSoftOMXComponent” 有很大的不同,这是为什么呢?这节主要来探究这个问题。
2. 原因分析
在 C/C++ 程序(库、目标文件)中,所有非静态的(non-static)函数在二进制文件中都是以 “符号(symbol)” 形式出现的。这些符号都是唯一的字符串,从而把各个函数在程序、库、目标文件中区分开来。
我们可以使用 nm 或者 readelf -s 命令来查看二进制文件中的符号信息,如 libffmpeg C 库的符号信息。
$ readelf -s libffmpeg.so
...
// 一些方法的符号信息
1065: 000969ac 540 FUNC GLOBAL DEFAULT 7 av_packet_merge_side_data
1066: 00096bc8 556 FUNC GLOBAL DEFAULT 7 av_packet_split_side_data
1067: 00096df4 112 FUNC GLOBAL DEFAULT 7 av_packet_shrink_side_dat
// 一些变量的符号信息
2559: 006b9848 72 OBJECT GLOBAL DEFAULT 12 ff_vqf_demuxer
2560: 006b9890 72 OBJECT GLOBAL DEFAULT 12 ff_w64_demuxer
2561: 006b98d8 72 OBJECT GLOBAL DEFAULT 12 ff_wav_demuxer
...
在 C 中,符号名正是函数名,strcpy 函数的符号名就是 “strcpy”,因为在 C 中两个非静态函数的名字各不相同。
但是在 C++ 中允许重载,不同的函数可能有相同的函数名但不同的参数,并且有很多 C 所没有的特征,比如类、成员函数、异常说明等等,因此不可能直接用函数名来作为符号名。
为了解决这个问题,C++ 采用了所谓的 name mangling(名字混淆),它把函数名和参数信息杂糅在一起,改造成只有编译器才懂的符号,例如前面的 “_Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPE” 符号即是 createSoftOMXComponent 函数名带了些参数信息。
每个编译器都按自己的方式来进行 Name Mangling 。所以各个编译器给二进制文件生成的符号可能都不相同,如用 Android 编译生成的 C++ 二进制文件符号都带前缀 “_ZNxandroid”。
$ readelf -s libstagefright.so
5497: 000ae647 24 FUNC WEAK DEFAULT 12 _ZNK7android6VectorINS_4S
5498: 0012dd15 22 FUNC GLOBAL DEFAULT 12 _ZNK9mkvparser4Tags3Tag12
5499: 0013d4f9 56 FUNC WEAK DEFAULT 12 _ZNSt3__16vectorIhNS_9all
5500: 0008f01d 6 FUNC GLOBAL DEFAULT 12 _ZThn8_N7android6ACodec25
5501: 000ba611 316 FUNC GLOBAL DEFAULT 12 _ZN7android10JPEGSource4r
3 解决方案
3.1 完整符号名传递
明白了 C++ 编译器 Name Mangling 的原理,自然而然有了第一种解决方案,就跟引言中 Google 的做法是一样的。使用 nm 命令获取库函数的完整符号名,直接传给 dlsym 即可。
$ nm xxx.so | grep Fun_Name
3.2 extern “C”
C++ 有个特定的关键字用来声明采用 C binding 的函数:extern “C” 。 用 extern “C” 声明的函数将使用函数名作符号名,就像 C 函数一样。因此,只有非成员函数才能被声明为 extern “C”,并且不能被重载。
尽管限制多多,extern “C” 函数还是非常有用,因为它可以像 C 函数一样被 dlopen 动态加载。冠以 extern “C” 限定符后,并不意味着函数中无法使用 C++ 代码了,相反,它仍然是一个完全的 C++ 函数,可以使用任何 C++ 特性和各种类型的参数。
extern “C” 有两种声明方式,下方第一个示例使用内联(inline)形式还有就是使用 extern “C” { … } 花括号包含这种。
extern "C" int FunA;
extern "C" void FunB();
和
extern "C" {
extern int FunA;
extern void FunB();
}
加载类
冠以 extern “C” 限定符后,并不意味着函数中无法使用 C++ 代码了,相反,它仍然是一个完全的 C++ 函数,可以使用任何 C++ 特性和各种类型的参数。
我们利用函数 new 一个对象返回即可。
extern "C" polygon* create() {
return new polygon;
}
extern "C" void destroy(polygon* p) {
delete p;
}