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;
}