C++ dlopen mini HOWTO

C++ dlopen mini HOWTO
作者:Aaron Isotton <[email protected]> 2006-03-16
譯者:[email protected] 2006-08-05

------------------------------------------------
摘要
  如何使用dlopen API動態地加載C++函數和類

------------------------------------------------
目錄
  介紹
    版權和許可證
    不承諾
    貢獻者
    反饋
    術語
  問題所在
    Name Mangling
    類
  解決方案
    extern "C"
    加載函數
    加載類
  源代碼 
  FAQ
  其他
  參考書

------------------------------------------------
介紹
  如何使用dlopen API動態地加載C++函數和類,是Unix C++程序員經常碰到的問題。事實上,情況偶爾有些複雜,需要一些解釋。這正是寫這篇mini HOWTO的緣由。
  理解這篇文檔的前提是對C/C++語言中dlopen API有基本的瞭解。這篇HOWTO的維護鏈接是
http://www.isotton.com/howtos/C++-dlopen-mini-HOWTO/

  版權和許可證
  這篇文檔《C++ dlopen mini HOWTO》版權爲Aaron Isotton所有(copyrighted (c) 2002-2006),任何人在遵守自由軟件基金會制定的GPLv2許可證條款前提下可以自由拷貝、分發和修改這份文檔。

  不承諾
  本文不對文中的任何內容作可靠性承諾。您必須爲您自己使用文中任何概念、示例和信息承擔風險,因爲其中可能存在錯誤和不準確的地方,或許會損壞您的系統──儘管幾乎不可能發生此類事故,但您還是小心行事──作者不會爲此負任何責任。

  貢獻者
  在這篇文檔中,我欣然致謝(按字母順序):
  ◆ Joy Y Goodreau <joyg (at) us.ibm.com> 她的編輯工作.
  ◆ D. Stimitis <stimitis (at) idcomm.com> 指出一些formatting和name mangling的問題, 還指出extern “C”的一些微妙之處。

  反饋
  歡迎對本文檔的反饋信息!請把您的補充、評論和批評發送到這個郵件地址:<[email protected]>。

  術語
  dlopen API
    關於dlclose、dlerror、dlopen和dlsym函數的描述可以在 dlopen(3) man手冊頁查到。
    請注意,我們使用“dlopen”時,指的是dlopen函數,而使用“dlopen API”則是指整個API集合。
------------------------------------------------
問題所在
  有時你想在運行時加載一個庫(並使用其中的函數),這在你爲你的程序寫一些插件或模塊架構的時候經常發生。
  在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++-dlopen-mini-HOWTO/examples.tar.gz

FAQ
(譯者注:下文翻譯暫時省略)
1.I'm using Windows and I can't find the dlfcn.h header file! What's the problem?

The problem is that Windows doesn't have the dlopen API, and thus there is no dlfcn.h header. There is a similar API around the LoadLibrary function, and most of what is written here applies to it, too. Please refer to the Microsoft Developer Network Website for more information.

2.Is there some kind of dlopen-compatible wrapper for the Windows LoadLibrary API?

I don't know of any, and I don't think there'll ever be one supporting all of dlopen's options.

There are alternatives though: libtltdl (a part of libtool), which wraps a variety of different dynamic loading APIs, among others dlopen and LoadLibrary. Another one is the Dynamic Module Loading functionality of GLib. You can use one of these to ensure better possible cross-platform compatibility. I've never used any of them, so I can't tell you how stable they are and whether they really work.

You should also read section 4, “Dynamically Loaded (DL) Libraries”, of the Program Library HOWTO for more techniques to load libraries and create classes independently of your platform.


其他

* The dlopen(3) man page. It explains the purpose and the use of the dlopen API.
* The article Dynamic Class Loading for C++ on Linux by James Norton published on the Linux Journal.
* Your favorite C++ reference about extern "C", inheritance, virtual functions, new and delete. I recommend [STR2000].
* [ISO14882]
* The Program Library HOWTO, which tells you most things you'll ever need about static, shared and dynamically loaded libraries and how to create them. Highly recommended.
* The Linux GCC HOWTO to learn more about how to create libraries with GCC.

參考書目
[ISO14482] ISO/IEC 14482-1998 — The C++ Programming Language. Available as PDF and as printed book from
http://webstore.ansi.org/.
[STR2000] StroustrupBjarne The C++ Programming Language, Special Edition. ISBN 0-201-70073-5. Addison-Wesley

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章