本文不打算包含:如何從頭撰寫一個 DLL、如何用某某牌編譯器編譯 DLL、如何在某某牌編譯器使用DLL..... 等等議題。基礎 DLL 教學在網路上非常豐富,有需要的人應該很容易可以得到所需資源,在此先假設讀者都有一定的熟悉度。這裡只打算提供一些能稍稍增加彈性及可維護性的小經驗,特別是 DLL 的 header 方面。或許對很多人來說可能都是常識,只是我發現網路上比較少這方面的整合資訊,所以做了一點綜合整理。

以下我拿自己常用的 DLL header 範本為例

// mylib.hpp header file
#ifndef MYLIB_HPP
#define MYLIB_HPP

// Setup autolink ---------------------- (1)
#if !defined(MYLIB_INTERNAL)
# if defined(NDEBUG)
# pragma comment(lib, "mylib.lib")
# pragma message("Linking to lib file: mylib.lib")
# else
# pragma comment(lib, "mylibd.lib")
# pragma message("Linking to lib file: mylibd.lib")
# endif
#endif

// Setup export type ------------------- (2)
#if defined(_WIN32) && defined(MYLIB_SHARED_LINK) # if defined(MYLIB_INTERNAL) # define MYLIB_API __declspec (dllexport) # else # define MYLIB_API __declspec (dllimport) # endif #endif
#if !defined(MYLIB_API)
# define MYLIB_API
#endif

// Setup calling convention ------------ (3)
#define MYLIB_CALL __stdcall

// Export class ------------------------ (4-1)

class MYLIB_API MyClass
{
// .....
};

// Export C compatible APIs ------------ (4-2)
#ifdef __cplusplus
extern "C" {
#endif

extern MYLIB_API int var;

MYLIB_API void MYLIB_CALL GoodbyeWorld();

MYLIB_API void MYLIB_CALL ShowMsg(char* msg, int n);

#ifdef __cplusplus
}
#endif

#endif

這個 header 預期給 DLL 的實作端和使用端共用,在這裡我使用 MYLIB_INTERNAL 加以區分。在 DLL 實作檔裡面只要定義 MYLIB_INTERNAL ,就可以切換適當的 code。

// mylib.cpp source file
#define MYLIB_INTERNAL
#include "mylib.hpp"
.....

由於 macro 具有不受限制的蔓延性,所以要特別小心被使用端引入之後潛在的副作用。如果可以的話盡量在 header 末端把不必要的東西都 #undef 掉,再不然最起碼要讓這些 macro 具有不容易被誤用的獨一性。舉個例子來說,有些人喜歡拿 BUILDING_DLL 之類的菜市場名區別實作和使用端,但想想看萬一某個 DLL 實作中恰巧又使用另一個 DLL,好死不死兩個 DLL 都使用 BUILDING_DLL,這時候引用順序可能會導致一些古怪的錯誤。我自己的命名的習慣是「LIB名稱_用途」,或許不是最好但已經不太會衝突了。

其他 header 通用的慣例在這裡也適用。引用任何其他的 header 都要考慮會不會對使用端造成副作用,例如iostream 可能會導致某些全域物件被建立。還有無論如何不要在 header 裡面寫全域的 using namespace,若無撞名疑慮,實作模組中的全域 using namespace 通常無害,問題很容易控制在單一的 compilation unit 中,但是在 header 的全域 using namespace 將可能隨引用而導致烽火蔓延到天邊。

接下來看(1)。VC 和 BCB 都可透過 #pragma comment 指定連結程式庫,BCB 也支援 #pragma link 的寫法,而 GCC 沒有此功能所以會自動忽略。若 lib 檔固定位於編譯器的程式庫資料夾或與 source 放在一起,自動連結會省去不少時間;但若 lib 檔沒有固定的位置,反而會帶來一點麻煩。至於是否要為編譯組態區分不同版本,應視需要而定。

(2)是 export 的方式
若是在實作 DLL,那麼 MYLIB_API 就會被定義成 __declspec (dllexport);
若是在使用 DLL,那麼 MYLIB_API 就會被定義成 __declspec (dllimport);
若加入了 MYLIB_STATIC_LINK,那麼 MYLIB_API 就會直接定義成空字串
用這個方式很容易就可以調整 export/import,甚至改成靜態連結程式庫。

(3)是函數 calling convention。光是這個主題就得另外寫一篇文章才能完整探討,這裡只做簡單的介紹。Calling convention 決定了函數的參數順序、參數傳遞及返回方式(i.e. 透過堆疊或暫存器)、以及誰負責復原堆疊。

目前在 32 位元 x86 上常用的有四種,其中 stdcall 和 cdecl 都是以由右至左順序將參數推入堆疊,用 EAX 回傳,不同點在於前者的被呼叫端必須在返回前復原堆疊,而後者由呼叫端還原。fastcall 和 thiscall 都會利用暫存器傳遞參數,但沒有一定的實作標準,隨編譯器而定。其他不同機器還有其他的規格,這裡不多說,只要知道用錯了可能導致堆疊被破壞即可。

對於只在單一編譯器下使用的 DLL,最好的方式就是用預設,亦即直接把 MYLIB_CALL 定義為空字串,然後再也不必煩惱這個問題。通常 C 編譯器預設都是 cdecl,然而許多開發環境只支援 stdcall,所以若是有跨開發環境的考量最好還是使用 stdcall。但問題沒這麼簡單,很多編譯器會針對不同 calling convention 的套用不同的名稱修飾,更麻煩的是修飾規則無統一標準。

舉例來說,函數 void func(int n) 用 VC 相容的修飾規則:
在 stdcall 變成 _func@4
在 cdecl 變成 _func
這會導致於使用端用無法透過 func 這個名稱找到所需要的函數。解決的方法只有透過 def 檔,或編譯連結時多加一些條件,這不在本文討論的範圍,有機會我再介紹。

接下來我想先談(4-2) extern "C"函數的部分。大家應該都知道這是保留與 C 相容的連結名稱,而不會被 C++ name mangling 附加亂七八糟的符號。但這裡很容易有個誤解,以為只要 extern "C" 就會保留和原來一模一樣的名稱,這是錯的,extern "C" 只保證不會用 C++ mangling 方式改名,但還是有可能會遭到其他 C 相容的規則改名,例如前面提到的 calling convention 修飾就不受 extern "C" 影響,所以要用另外的手段處理

若只在單一的編譯環境下使用,extern "C" 就毫無意義,用了 extern "C" 就表示已經預期面對其他編譯環境,所以介面應該降到大多數程式工具所能處理的規格,也就是無名稱修飾的 stdcall 函數,而且最多只使用純 C 的 struct,即俗稱的 POD,而不應該使用 C++ 物件。例如傳遞字串應該使用 char* 或 wchar*,而不應該用 std::string,不過在內部實作還是鼓勵盡量用 std::string,但介面要多一層轉換。

另一個容易被忽略的問題是 exception,所有 DLL 內部丟出的 exception 都應該在介面之前被吸收掉,跨越 DLL 邊界的 exception 幾乎可以肯定不會被正常處理。即使自身程式碼沒有丟出 exception,也要考慮使用到的程式庫會不會丟,例如 new 一個物件失敗應該先將 std::bad_alloc 抓下來,再 return 失敗值給 DLL 使用端。

(4-1)是export class的方法。一旦 export class,這個 DLL 差不多就綁定在特定一家編譯器特定一個版本上,甚至連 Debug 和 Release 都不一定相容。不過只要放棄了和其他語言之間的相容性,在DLL撰寫上可以充分得到最大功能以及最小使用限制,不必理會上述(4-2)的規則,幾乎和撰寫一般模組無異。

由於 template 只在 source level 運作,所以沒有 DLL export 的問題。但有時候我們會需要 export 特定一個具現化,因為 DLL 的 binary interface 當中用到了這個具現化,例如某個 class 裡面的成員是 vector<int>,那麼我們就需要明確的 export vector<int>,方式如下

template class MYLIB_API std::vector<int>;

不過真的這樣寫可能還是會失敗,因為我們還是得繼續手工 export allocator<int>..... 直到所有該具現化的東西都被滿足。然而已知在某些情況下,從 DLL import 的 template 具現化會和原地具現化的 template 衝突。詳見

http://msdn.microsoft.com/en-us/library/ms174286.aspx

(暫存待修,歡迎建設性的批評)

novus 發表在 痞客邦 PIXNET 留言(2) 人氣()


留言列表 (2)

發表留言
  • damody
  • [無論如何不要在 header 裡面寫全域的 using namespace]沒錯!
    之前就被這樣寫的library害的很苦。
  • me too

    novus 於 2010/07/17 10:39 回覆

  • lagendre
  • 好文章 Thx^^