1. 背景
在java中,jvm支援類的動態連結(Class.forName(String className)),用起來也很方便。動態連結是實現IOC(Inversion of Control,控制反轉,更形象的稱作依賴注入)的關鍵,用於將類間依賴從程式移到設定檔中。在架構不重新編譯的情況下,替換被依賴的類。
在linux下,C++只能通過C的dl API實現動態連結,需要先將動態連結程式庫編譯成.so,然後再調用API進行連結。下面看一下如何?動態連結C++類。和java實現IOC一樣,連結類需要依賴於介面,先定義一下架構需要依賴的介面部分:
class UserInterface {public: virtual bool func() = 0;}; 動態連結程式庫實現部分繼承這個介面,進行實現。
class ConcretInterface: public UserInterface {public: virtual bool func() { printf("A implementition of UserInterface"); }};
2. dl API
先介紹dl API部分,這些函數需要串連dl動態庫,-ldl:
void *dlopen(const char *filename, int flag);
用於開啟.so,並進行連結,返回連結後的handle。flag表示連結標誌:
RTLD_LAZY:消極式載入,在.so中第一次使用某個符號是再進行連結。只對函數符號有效,變數符號會被立即載入。
RTLD_NOW:在dlopen返回後,所有未定義的符號都需要進行連結,如果符號解析失敗,dlopen會返回失敗。
RTLD_GLOBAL:使用這個標誌,則這個庫中的符號可以被隨後的動態庫使用。
RTLD_LOCAL:與RTLD_GLOBAL相反,不可以以被隨後動態庫使用。
char *dlerror(void);
返回dl相關錯誤,如果從初始化或者上一次調用到目前為止沒有錯誤則返回NULL。
void *dlsym(void *handle, const char *symbol);
返回符號名為symbol的符號的地址,需要用到dlopen返回的動態連結程式庫的handle。需要使用dlerror來檢驗dlsym是否出錯,不能進通過返回的指標是否為NULL判斷是否出錯,因為符號地址為NULL屬於正常情況。
int dlclose(void *handle);
減少.so的引用計數,如果引用計數為0,則.so會被卸載。
3. 類載入實現
通過上面dl API可知,只能返回動態連結程式庫中的符號的地址,這個符號函數符號或者是變數符號,不能像java一樣直接返回一個類。而我們在動態連結類時,實際上只是需要這個類的執行個體對象,所以可以通過返回建立類對象的函數符號實作類別的串連。由於在C++中,可以對new操作符重載,進行其他的一些操作,分配資源等,此時調用delete釋放對象,會造成資源流失。所以為了支援重載new操作符,還需要釋放對象的函數。定義如下:
typedef bool (*user_interface_creator_t)(UserInterface **ui);typedef void (*user_interface_destroyer_t)(UserInterface *ui);
上面定義了兩個函數指標,分別是連結類的建立器和銷毀器。動態連結程式庫需要實現這兩個函數,從而是架構可以建立和銷毀對象。如果預設不支援new操作重載,可以去掉destroyer,簡化介面。
還有一點比較重要,由於C++支援了命名空間、類以及重載等機制,所以符號的命名規則和C不同。而dl API屬於C的部分,使用的C的命名規則,所以在實現上述兩個函數時(也就是說需要通過動態連結的C++函數)需要使用C的符號命名規則。這需要實現extern 'C'實現,具體如下:
extern 'C' {bool my_creator(UserInterface **ui){ if (NULL == ui) { printf("invalid param"); return false; } *ui = new (std::nothrow) ConcrectInterface; return true;}void my_destroyer(UserInterface *ui){ if (ui != NULL) { delete ui; }}}; my_creator和my_destroyer就是動態連結程式庫對介面的實現,可以通過函數名調用dlsym找到對應的地址,完成對象的建立和銷毀。下面,介紹一下架構,也就是動態連結的部分。
#include <dlfcn.h>#include <stdio.h>#include "user_interface.hpp"const char *so_path = "./user_impl.so";const char *creat_func = "my_creator";const char *destroy_func = "my_destroyer";int main (){ // load .so void *handle = dlopen(so_path, RTLD_LAZY); if (NULL == handle) { printf("failed to open %s, error: %s", so_path, dlerror()); return 1; } // reset errors dlerror(); user_interface_creator_t creator = (user_interface_creator_t)dlsym(handle, creat_func); const char* err = dlerror(); if (err != NULL) { printf("failed to load creator, error: %s", err); return 1; } // reset errors dlerror(); user_interface_destroyer_t destroyer = (user_interface_destroyer_t)dlsym(handle, destroy_func); err = dlerror(); if (err != NULL) { printf("failed to load destroyer, error: %s", err); return 1; } UserInterface *ui; if (!(*creator)(&ui)) { printf("failed to creat ui"); return 1; } ui->func(); (*destroyer)(ui); dlclose(handle);} 這裡需要注意每次調用dl API函數後需要調用dlerror清除error狀態。
4. 坑
如果動態連結程式庫和可執行程式依賴同一個庫,比如依賴同一個日誌庫,在編譯動態連結程式庫和可執行程式時需要注意一下。要確保對於庫的符號引用的解析會指向同一個符號。具體做法:
(1)編譯.so時,不連結依賴的庫,對於庫的符號的引用都是undefined。
(2)可執行程式需要連結依賴的庫,並且要指定連結選項-rdynamic,將全域符號可以用於動態連結程式庫的符號引用的解析。
如果沒有做到以上兩點,會出現同一個符號引用會被解析到兩個符號,若符號依賴的是一個變數的符號(比如符號本身就是一個變數,或者是函數符號,但依賴於某個變數),那麼就會出現個別古怪、異常的現象。
對於.so依賴一個庫,但是可執行程式沒有依賴,那麼編譯.so時必須把這個庫連結,否則會出現undefined symbol。
再介紹下調試動態連結程式庫時用到的工具,因為經常會出現各種未定義的符號,所以需要查看.so或者可執行程式的符號資訊,可以使用nm,比如,類型U或者類型T的符號。還可以使用c++filt將符號名解析成可讀的字串。