問題所在
有時你想在運行時載入一個庫(並使用其中的函數),這在你為你的程式寫一些外掛程式或模組架構的時候經常發生。
在C語言中,載入一個庫輕而易舉(調用dlopen、dlsym和dlclose就夠了),但對C++來說,情況稍微複雜。動態載入一個C++庫的困難一部分是因為C++的name mangling(譯者註:也有人把它翻譯為“名字毀壞”,我覺得還是不翻譯好),另一部分是因為dlopen API是用C語言實現的,因而沒有提供一個合適的方式來裝載類。
在解釋如何裝載C++庫之前,最好再詳細瞭解一下name mangling。我推薦您瞭解一下它,即使您對它不感興趣。因為這有助於您理解問題是如何產生的,如何才能解決它們。
Name Mangling
在每個C++程式(或庫、目標檔案)中,所有非靜態(non-static)函數在二進位檔案中都是以“符號(symbol)”形式出現的。這些符號都是唯一的字串,從而把各個函數在程式、庫、目標檔案中區分開來。
在C中,符號名正是函數名:strcpy函數的符號名就是“strcpy”,等等。這可能是因為兩個非靜態函數的名字一定各不相同的緣故。
而C++允許重載(不同的函數有相同的名字但不同的參數),並且有很多C所沒有的特性──比如類、成員函數、異常說明──幾乎不可能直接用函數名作符號名。為瞭解決這個問題,C++採用了所謂的name mangling。它把函數名和一些資訊(如參數數量和大小)雜糅在一起,改造成奇形怪狀,只有編譯器才懂的符號名。例如,被mangle後的foo可能看起來像foo@4%6^,或者,符號名裡頭甚至不包括“foo”。
其中一個問題是,C++標準(目前是[ISO14882])並沒有定義名字必須如何被mangle,所以每個編譯器都按自己的方式來進行name mangling。有些編譯器甚至在不同版本間更換mangling演算法(尤其是g++ 2.x和3.x)。即使您搞清楚了您的編譯器到底怎麼進行mangling的,從而可以用dlsym調用函數了,但可能僅僅限於您手頭的這個編譯器而已,而無法在下一版編譯器下工作。
類
使用dlopen API的另一個問題是,它只支援載入函數。但在C++中,您可能要用到庫中的一個類,而這需要建立該類的一個執行個體,這不容易做到。
解決方案
extern "C"
C++有個特定的關鍵字用來聲明採用C binding的函數:extern "C" 。 用 extern "C"聲明的函數將使用函數名作符號名,就像C函數一樣。因此,只有非成員函數才能被聲明為extern "C",並且不能被重載。儘管限制多多,extern "C"函數還是非常有用,因為它們可以象C函數一樣被dlopen動態載入。冠以extern "C"限定符後,並不意味著函數中無法使用C++代碼了,相反,它仍然是一個完全的C++函數,可以使用任何C++特性和各種類型的參數。
載入函數
在C++中,函數用dlsym載入,就像C中一樣。不過,該函數要用extern "C"限定符聲明以防止其符號名被mangle。
樣本1.載入函數
代碼:
--------------------------------------------------------------------------------
//----------
//main.cpp:
//----------
#include <iostream>
#include <dlfcn.h>
int main() {
using std::cout;
using std::cerr;
cout << "C++ dlopen demo\n\n";
// open the library
cout << "Opening hello.so...\n";
void* handle = dlopen("./hello.so", RTLD_LAZY);
if (!handle) {
cerr << "Cannot open library: " << dlerror() << '\n';
return 1;
}
// load the symbol
cout << "Loading symbol hello...\n";
typedef void (*hello_t)();
// reset errors
dlerror();
hello_t hello = (hello_t) dlsym(handle, "hello");
const char *dlsym_error = dlerror();
if (dlsym_error) {
cerr << "Cannot load symbol 'hello': " << dlsym_error <<
'\n';
dlclose(handle);
return 1;
}
// use it to do the calculation
cout << "Calling hello...\n";
hello();
// close the library
cout << "Closing library...\n";
dlclose(handle);
}
//----------
// hello.cpp:
//----------
#include <iostream>
extern "C" void hello() {
std::cout << "hello" << '\n';
}
--------------------------------------------------------------------------------
在hello.cpp中函數hello被定義為extern "C"。它在main.cpp中被dlsym調用。函數必須以extern "C"限定,否則我們無從知曉其符號名。
警告:
extern "C"的聲明形式有兩種:上面樣本中使用的那種內聯(inline)形式extern "C" , 還有才用花括弧的extern "C" { ... }這種。 第一種內聯形式聲明包含兩層意義:外部連結(extern linkage)和C語言連結(language linkage),而第二種僅影響語言連結。
下面兩種聲明形式等價:
代碼:
--------------------------------------------------------------------------------
extern "C" int foo;
extern "C" void bar();
--------------------------------------------------------------------------------
和
代碼:
--------------------------------------------------------------------------------
extern "C" {
extern int foo;
extern void bar();
}
--------------------------------------------------------------------------------
對於函數來說,extern和non-extern的函式宣告沒有區別,但對於變數就有不同了。如果您聲明變數,請牢記:
代碼:
--------------------------------------------------------------------------------
extern "C" int foo;
--------------------------------------------------------------------------------
和
代碼:
--------------------------------------------------------------------------------
extern "C" {
int foo;
}
--------------------------------------------------------------------------------
是不同的物事(譯者註:簡言之,前者是個聲明; 而後者不僅是聲明,也可以是定義)。
進一步的解釋請參考[ISO14882],7.5, 特別注意第7段; 或者參考[STR2000],9.2.4。在用extern的變數尋幽訪勝之前,請細讀“其他”一節中羅列的文檔。
載入類
載入類有點困難,因為我們需要類的一個執行個體,而不僅僅是一個函數指標。我們無法通過new來建立類的執行個體,因為類不是在可執行檔中定義的,況且(有時候)我們連它的名字都不知道。
解決方案是:利用多態性! 我們在可執行檔中定義一個帶虛成員函數的介面基類,而在模組中定義派生實作類別。通常來說,介面類是抽象的(如果一個類含有虛函數,那它就是抽象的)。
因為動態載入類往往用於實現外掛程式,這意味著必須提供一個清晰定義的介面──我們將定義一個介面類和派生實作類別。
接下來,在模組中,我們會定義兩個附加的helper函數,就是眾所周知的“類工廠函數(class factory functions)(譯者註:或稱對象工廠函數)”。其中一個函數建立一個類執行個體,並返回其指標; 另一個函數則用以銷毀該指標。這兩個函數都以extern "C"來限定修飾。
為了使用模組中的類,我們用dlsym像樣本1中載入hello函數那樣載入這兩個函數,然後我們就可以隨心所欲地建立和銷毀執行個體了。
樣本2.載入類
我們用一個一般性的多邊形類作為介面,而繼承它的三角形類(譯者註:正三角形類)作為實現。
代碼:
--------------------------------------------------------------------------------
//----------
//main.cpp:
//----------
#include "polygon.hpp"
#include <iostream>
#include <dlfcn.h>
int main() {
using std::cout;
using std::cerr;
// load the triangle library
void* triangle = dlopen("./triangle.so", RTLD_LAZY);
if (!triangle) {
cerr << "Cannot load library: " << dlerror() << '\n';
return 1;
}
// reset errors
dlerror();
// load the symbols
create_t* create_triangle = (create_t*) dlsym(triangle, "create");
const char* dlsym_error = dlerror();
if (dlsym_error) {
cerr << "Cannot load symbol create: " << dlsym_error << '\n';
return 1;
}
destroy_t* destroy_triangle = (destroy_t*) dlsym(triangle, "destroy");
dlsym_error = dlerror();
if (dlsym_error) {
cerr << "Cannot load symbol destroy: " << dlsym_error << '\n';
return 1;
}
// create an instance of the class
polygon* poly = create_triangle();
// use the class
poly->set_side_length(7);
cout << "The area is: " << poly->area() << '\n';
// destroy the class
destroy_triangle(poly);
// unload the triangle library
dlclose(triangle);
}
//----------
//polygon.hpp:
//----------
#ifndef POLYGON_HPP
#define POLYGON_HPP
class polygon {
protected:
double side_length_;
public:
polygon()
: side_length_(0) {}
virtual ~polygon() {}
void set_side_length(double side_length) {
side_length_ = side_length;
}
virtual double area() const = 0;
};
// the types of the class factories
typedef polygon* create_t();
typedef void destroy_t(polygon*);
#endif
//----------
//triangle.cpp:
//----------
#include "polygon.hpp"
#include <cmath>
class triangle : public polygon {
public:
virtual double area() const {
return side_length_ * side_length_ * sqrt(3) / 2;
}
};
// the class factories
extern "C" polygon* create() {
return new triangle;
}
extern "C" void destroy(polygon* p) {
delete p;
}
--------------------------------------------------------------------------------
載入類時有一些值得注意的地方:
◆ 你必須(譯者註:在模組或者說共用庫中)同時提供一個創造函數和一個銷毀函數,且不能在執行檔案內部使用delete來銷毀執行個體,只能把執行個體指標傳遞給模組的銷毀函數處理。這是因為C++裡頭,new操作符可以被重載;這容易導致new-delete的不匹配調用,造成莫名其妙的記憶體流失和段錯誤。這在用不同的標準庫連結模組和可執行檔時也一樣。
◆ 介面類的解構函式在任何情況下都必須是虛函數(virtual)。因為即使出錯的可能極小,近乎杞人憂天了,但仍舊不值得去冒險,反正額外的開銷微不足道。如果基類不需要解構函式,定義一個空的(但必須虛的)解構函式吧,否則你遲早要遇到問題,我向您保證。你可以在comp.lang.c++ FAQ( http://www.parashift.com/c++-faq-lite/ )的第20節瞭解到更多關於該問題的資訊。
原始碼
你可以下載所有包含在本文檔中的程式碼封裝: http://www.isotton.com/howtos/C++-dl...xamples.tar.gz