使用 dlopen 和 dlsym 來使用 C++ 中的函數、類

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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章