<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    怎樣從一個DLL中導出一個C++類

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接

    原文作者:Alex Blekhman    翻譯:朱金燦

     

    原文來源:

    http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx

    譯文來源:http://blog.csdn.net/clever101

     

     

    C++語言畢竟能和Windows DLLs能夠和平共處。

     

     

    介紹

     

     

         自從Windows的開始階段動態鏈接庫(DLL)就是Windows平臺的一個組成部分。動態鏈接庫允許在一個獨立的模塊中封裝一系列的功能函數然后以一個顯式的C函數列表提供外部使用者使用。在上個世紀80年代,當Windows DLLs面世時,對于廣大開發者而言只有C語言是切實可行的開發手段。所以, Windows DLLs很自然地以C函數和數據的形式向外部暴露功能。從本質來說,一個DLL可以由任何語言實現,但是為了使DLL用于其它的語言和環境之下,一個DLL接口必須后退到最低要求的母體——C語言。

     

     

    使用C接口并不自動意味一個開發者應該應該放棄面向對象的開發方式。甚至C接口也能用于真正的面向對象編程,盡管它有可能被認為是一種單調乏味的實現方式。很顯然世界上使用人數排第二的編程語言是C++,但它卻不得不被DLL所誘惑。然而,和C語言相反,在調用者和被調用者之間的二進制接口被很好的定義并被廣泛接受,但是在C++的世界里卻沒有可識別的應用程序二進制接口。實際上,由一個C++編譯器產生的二進制代碼并不能被其它C++編譯器兼容。再者,在同一個編譯器但不同版本的二進制代碼也是互不兼容的。所有這些導致從一個DLL中一個C++類簡直就是一個冒險。

     

     

    這篇文章就是演示幾種從一個DLL模塊中導出C++類的方法。源碼演示了導出虛構的Xyz對象的不同技巧。Xyz對象非常簡單,只有一個函數:Foo

     

     

    下面是Xyz對象的圖解:

    Xyz

    int Foo(int)

     

     

    Xyz對象在一個DLL里實現,這個DLL能作為一個分布式系統供范圍很廣的客戶端使用。一個用戶能以下面三種方式調用Xyz的功能:

     

    使用純C

    使用一個規則的C++

    使用一個抽象的C++接口

    源碼(譯注:文章附帶的源碼)包含兩個工程:

     

    XyzLibrary  一個DLL工程

    XyzExecutable  一個Win32 使用"XyzLibrary.dll"的控制臺程序

    XyzLibrary工程使用下列方便的宏導出它的代碼:

     

    #if defined(XYZLIBRARY_EXPORT) // inside DLL

    #   define XYZAPI   __declspec(dllexport)

    #else // outside DLL

    #   define XYZAPI   __declspec(dllimport)

    #endif  // XYZLIBRARY_EXPORT

     

         

    XYZLIBRARY_EXPORT標識符僅僅在XyzLibrary工程定義,因此在XYZAPI宏在DLL生成時被擴展為__declspec(dllexport)而在客戶程序生成時被擴展為__declspec(dllimport)

     

     

    C語言方式

     

    句柄

     

      經典的C語言方式進行面向對象編程的一種方式就是使用晦澀的指針,比如句柄。一個用戶能夠使用一個函數創建一個對象。實際上這個函數返回的是這個對象的一個句柄。接著用戶能夠調用這個對象相關的各種操作函數只要這個函數能夠接受這個句柄作為它的一個參數。一個很好的例子就是在Win32窗口相關的API中句柄的習慣是使用一個HWND句柄來代表一個窗口。虛構的Xyz對象通過下面這樣一種方式導出一個C接口:

     

     

    typedef tagXYZHANDLE {} * XYZHANDLE;

     

    // 創建一個Xyz對象實例的函數

    XYZAPI XYZHANDLE APIENTRY GetXyz(VOID);

     

    // 調用Xyz.Foo函數

    XYZAPI INT APIENTRY XyzFoo(XYZHANDLE handle, INT n);

    // 釋放Xyz實例和占用的資源

    XYZAPI VOID APIENTRY XyzRelease(XYZHANDLE handle);

     

    // APIENTRY is defined as __stdcall in WinDef.h header.

     

               

     

    下面是一個客戶端調用的C代碼:

     

     

    #include "XyzLibrary.h"

     

    ...

     

    /* 創建Xyz實例*/

    XYZHANDLE hXyz = GetXyz();

     

    if(hXyz)

    {

        /* 調用 Xyz.Foo函數*/

        XyzFoo(hXyz, 42);

     

        /*析構 Xyz實例并釋放已取得的資源. */

        XyzRelease(hXyz);

      

        hXyz = NULL;

    }

     

           

    使用這種方式,一個DLL必須提供顯式的對象構建和刪除函數。

     

    調用協定

     

         對于所有的導出函數記住它們調用協定是重要的。對于很多初學者來說忘記添加調用協定是非常普遍的錯誤。只要客戶端的調用協定和DLL的調用協定匹配,一切都能運行。但是,一旦客戶端改變了它的調用協定,開發者將會產生一個難以察覺的直到運行時才發生的錯誤。XyzLibrary工程使用一個APIENTRY宏,這個宏在"WinDef.h"這個頭文件里被定義為__stdcall

     

     

    異常安全性

     

     

         DLL范圍內不允許發生C++異常。在一段時間內,C語言不識別C++的異常,并且不能正確處理它們。假如一個對象的方法需要報告一個錯誤,這時一個返回碼需要用到。

     

     

    優點

     

     

        一個DLL能被最廣泛的合適的開發者所使用。幾乎每一種現代編程語言都支持純C函數的互用性。

     

        一個DLLC運行時庫和它的客戶端是互相獨立的。因為資源的獲取和釋放完全發生在DLL模塊的內部,所以一個客戶端不受一個DLLC運行時庫選擇的影響。

     

    缺點

     

         獲取正確對象的合適的方法的責任落在DLL的使用者的肩上。比如在下面的代碼片斷,編譯器不能捕捉到其中發生的錯誤:

     

        

     

    /* void* GetSomeOtherObject(void)是別的地方定義的一個函數 */

    XYZHANDLE h = GetSomeOtherObject();

     

    /* 錯誤在錯誤的對象實例上調用Xyz.Foo函數*/

    XyzFoo(h, 42);

     

        顯式要求創建和摧毀一個對象的實例。其中特別煩人的是對象實例的刪除。客戶端必須極仔細地在一個函數的退出點調用XyzRelease函數。假如開發者忘記調用XyzRelease函數,那時資源就會泄露,因為編譯器不能跟蹤一個對象實例的生命周期。那些支持析構函數或垃圾收集器的語言通過在C接口上作一層封裝有助于降低這個問題發生的概率。

     

     

        假如一個對象的函數返回或接受其它對象作為參數,那時DLL作者也就不得不為這些對象提供一個正確的C接口。假如退回到最大限度的復用,也就是C語言,那么只有以字節創建的類型(如int, double, char*等等)可以作為返回類型和函數參數

     

     

         C++天然的方式:導出一個類

     

     

         Windows平臺上幾乎每一個現代的編譯器都支持從一個DLL中導出一個類。導出一個類和導出一個C函數非常相似。用那種方法一個開發者被要求做就是在類名之前使用__declspec(dllexport/dllimport)關鍵字來指定假如整個類都需要被導出,或者在指定的函數聲明前指定假如只是特定的類函數需要被導出。這兒有一個代碼片斷:

     

     

    // 整個CXyz類被導出,包括它的函數和成員

    class XYZAPI CXyz

    {

    public:

        int Foo(int n);

    };

     

    // 只有 CXyz::Foo函數被導出

    //

    class CXyz

    {

    public:

        XYZAPI int Foo(int n);

    };

     

                

    在導出整個類或者它們的方法沒有必要顯式指定一個調用協定。根據預設,C++編譯器使用__thiscall作為類成員函數的調用協定。然而,由于不同的編譯器具有不同的命名修飾法則,導出的C++類只能用于同一類型的同一版本的編譯器。這兒有一個MS Visual C++編譯器的命名修飾法則的應用實例:

     

     RFID設備管理軟件 

     

       注意這里修飾名是怎樣不同于C++原來的名字。下面是屏幕截圖顯示的是通過使用Dependency Walker工具對同一個DLL的修飾名進行破譯得到的:

     

     RFID設備管理軟件  

     

         只有MS Visual C++編譯器能使用這個DLL.DLL和客戶端代碼只有在同一版本的MS Visual C++編譯器才能確保在調用者和被調用者修飾名匹配。這兒有一個客戶端代碼使用Xyz對象的例子:

     

    #include "XyzLibrary.h"

     

    ...

    // 客戶端使用Xyz對象作為一個規則C++.

    CXyz xyz;

    xyz.Foo(42);

            

     

    正如你所看到的,導出的C++類的用法和其它任何C++類的用法幾乎是一樣的。沒什么特別的。

     

     

    重要事項:使用一個導出C++類的DLL和使用一個靜態庫沒有什么不同。所有應用于有C++代碼編譯出來的靜態庫的規則完全適用于導出C++類的DLL

     

     

    所見即所得

     

     

         一個細心的讀者必然已經注意到Dependency Walker工具顯示了額外的導出成員,那就是CXyz& CXyz::operator =(const CXyz&)賦值操作符。在工作你所看到的正是C++的收入(譯注:我估計這是原文作者幽默的說法,意思是你沒有定義一個=賦值操作符,而編譯器幫你自動定義一個,不是收入是什么?)。根據C++標準,每一個類有四個指定的成員函數:

     

    默認構造函數

    拷貝構造函數

    析構函數

    賦值操作符 (operator =)

         假如類的作者沒有聲明同時沒有提供這些成員的實現,那么C++編譯器會聲明它們,并產生一個隱式的默認的實現。在CXyz類,編譯器斷定它的默認構造函數,拷貝構造函數和析構函數都毫無意義,經過優化后把它們排除掉了。而賦值運算符在優化之后還存活并從DLL中導出。

     

         重要事項:使用__declspec(dllexport)來指定類導出來告訴編譯器來嘗試導出任何和類相關的東西。它包括所有類的數據成員,所有類的成員函數(或者顯式聲明,或者由編譯器隱式生成),所有類的基類和所有它們的成員。考慮:

               

     

    class Base

    {

        ...

    };

     

    class Data

    {

        ...

    };

     

    // MS Visual C++ compiler 會發出C4275 warning ,因為沒有導出基類

    class __declspec(dllexport) Derived :

        public Base

    {

        ...

     

    private:

        Data m_data;    // C4251 warning,因為沒有導出數據成員.

    };

              

              

        在上面的代碼片斷,編譯器會警告你沒有導出基類和類的數據成員。所以,為了成功導出一個類,一個開發者被要求導出所有相關基類和所有類的已定義的數據成員。這個滾雪球般的導出要求是一個重大缺點。這也是為什么,比如,導出派生自STL模板類或者使用STL模板類對象作為數據成員是非常困難和令人生厭的。比如一個STL容器比如std::map<>實例可能要求導出數十個額外的內部類。

     

     

    異常安全性

     

     

    一個導出的C++類可能會在沒有任何錯誤發生的情況下拋出異常。因為一個DLL和它的客戶端使用同一版本的同一類型的編譯器的事實,C++異常將在超出DLL的范圍進行捕捉和拋出好像DLL沒有分界線一樣。記住,使用一個帶有導出C++代碼和使用帶有相同代碼的靜態庫是完全一樣的。

     

     

    優點

     

        一個導出的C++類和其它任何C++類的用法是一樣的

     

        客戶端能毫不費力地捕捉在DLL發生的異常

     

        當在一個DLL模塊內有一些小的代碼改動時,其它模塊也不用重新生成。這對于有著許多復雜難懂代碼的大工程是非常有用的。

     

        在一個大工程中按照業務邏輯分成不同的DLL實現可以被認為真正的模塊劃分的第一步。總的來說,它是使工程達到模塊化值得去做的事

     

     

    缺點

     

     

        從一個DLL中導出C++類在它的對象和使用者需要保持緊密的聯系。DLL應該被視作一個帶有考慮到代碼依賴的靜態庫。

     

        客戶端代碼和DLL都必須和同一版本的CRT(譯注:C運行時庫)動態連接在一起。為了能夠在模塊之間修正CRT資源的紀錄,這一步是必需的。假如一個客戶端和DLL連接到不同版本的CRT,或者靜態連接到CRT,那么在一個CRT實例申請的資源有可能在另一個CRT實例中釋放。它將損壞CRT實例的內在狀態并企圖操作外部資源,并很可能導致運行失敗。

     

        客戶端代碼和DLL必須在異常處理和產生達成一致,同時在編譯器的異常設置也必須一致

     

        導出C++類要求同時導出這個類的所有相關的東西,包括:所有它的基類、所有類定義的用到的數據成員等等。

     

     

    C++成熟的方法:使用抽象接口

     

     

     

          一個C++抽象接口(比如一個擁有純虛函數和沒有數據成員的C++類)設法做到兩全其美:對對象而言獨立于編譯器的規則的接口以及方便的面向對象方式的函數調用。為達到這些要求去做的就是提供一個接口聲明的頭文件,同時實現一個能返回最新創建的對象實例的工廠函數。只有這個工廠函數需要使用__declspec(dllexport/dllimport)指定。接口不需要任何額外的指定。

     

    // Xyz object的抽象接口

    // 不要求作額外的指定

    struct IXyz

    {

        virtual int Foo(int n) = 0;

        virtual void Release() = 0;

    };

     

    // 創建Xyz對象實例的工廠函數

    extern "C" XYZAPI IXyz* APIENTRY GetXyz();

     

     

    在上面的代碼片斷中,工廠函數GetXyz被聲明為extern XYZAPI。這樣做是為了防止函數名被修飾(譯注:如上面提到的導出一個C++類,其成員函數名導出后會被修飾)。這樣,這個函數在外部表現為一個規則的C函數,并且很容易被和C兼容的編譯器所識別。這就是當使用一個抽象接口時客戶端代碼看起來和下面一樣:

            

     

    #include "XyzLibrary.h"

     

    ...

    IXyz* pXyz = ::GetXyz();

     

    if(pXyz)

    {

        pXyz->Foo(42);

     

        pXyz->Release();

        pXyz = NULL;

    }

            

             

    C++不用為接口提供一個特定的標記以便其它編程語言使用(比如C#Java)。但這并不意味C++不能聲明和實現接口。設計一個C++的接口的一般方法是去聲明一個沒有任何數據成員的抽象類。這樣,派生類可以繼承這個接口并實現這個接口,但這個實現對客戶端是不可見的。接口的客戶端不用知道和關注接口是如何實現的。它只需知道函數是可用的和它們做什么。

     

     

    內部機制

     

     

          在這種方法背后的思想是非常簡單的。一個由純虛函數組成的成員很少的類只不過是一個虛函數表——一個函數指針數組。在DLL范圍內這個函數指針數組被它的作者填充任何他認為必需的東西。這樣這個指針數組在DLL外部使用就是調用接口的實際上的實現。下面是IXyz接口的用法說明圖表。

    RFID設備管理軟件   

     

        上面的圖表演示了IXyz接口被DLLEXE模塊二者都用到。在DLL模塊內部,XyzImpl類派生自IXyz接口并實現它的方法。在EXE的函數調用引用DLL模塊經過一個虛表的實際實現。

     

     

    這種DLL為什么能和其它的編譯器一起運行

     

     

        簡短的解釋是:因為COM技術和其它的編譯器一起運行。現在作一個詳細解釋,實際上,在模塊之間使用一個成員很少的虛基類作為接口準確來說是COM對外暴露了一個COM接口。如我們所知的虛表的概念,能很精確地添加COM標準的標記。這不是一個巧合。C++語言,作為一個至少跨越了十年的主流開發語言,已經廣泛地應用在COM編程。因為C++天生地支持面向對象的特性。微軟將它作為產業COM開發的重量級的工具是毫不奇怪的。作為COM技術的所有者,微軟已經確保COM的二進制標準和它們擁有的在Visual C++編譯器實現的C++對象模型能以最小的成本實現匹配。

     

    難怪其它的編譯器廠商都和微軟采用相同的方式實現虛表的布局。畢竟,每個人都想支持COM技術,并做到和微軟已存在的解決方法兼容。假設某個C++編譯器不能有效支持COM,那么它注定會被Windows市場所拋棄。這就是為什么時至今日,通過一個抽象接口從一個DLL導出一個C++類能和Windows平臺上過得去的編譯器能可靠地運行在一起。

     

     

    使用一個智能指針

     

     

    為了確保正確的資源釋放,一個虛接口提供了一個額外的函數來清除對象實例。手動調用這個函數令人厭煩并容易導致錯誤發生。我們都知道這個錯誤在C世界里這是一個很普遍的錯誤,因為在那兒開發者不得不記得釋放顯式函數調用獲取的資源。這就是為什么典型的C++代碼借助于智能指針使用RAII(資源獲取即初始化)的習慣。XyzExecutable工程提供了一個例子,使用了AutoClosePtr模板。AutoClosePtr模板是一個最簡單的智能指針,這個智能指針調用了一個類消滅一個實例的主觀方法來代替delete操作符。這兒有一段演示帶有IXyz接口的一個智能指針的用法的代碼片斷:

     

        

    #include "XyzLibrary.h"

    #include "AutoClosePtr.h"

     

    ...

    typedef AutoClosePtr<IXyz, void, &IXyz::Release> IXyzPtr;

     

    IXyzPtr ptrXyz(::GetXyz());

     

    if(ptrXyz)

    {

        ptrXyz->Foo(42);

    }

     

    // 不需要調用ptrXyz->Release(). 智能指針將在析構函數里自動調用這個函數

     

     

    不管怎樣,使用智能指針將確保Xyz對象能正當地適當資源。因為一個錯誤或者一個內部異常的發生,函數會過早地退出,但是C++語言保證所有局部對象的析構函數能在函數退出之前被調用。

     

    異常安全性

     

    COM接口一樣不再允許因為任何內部異常的發生而導致資源泄露,抽象類接口不會讓任何內部異常突破DLL范圍。函數調用將會使用一個返回碼來明確指示發生的錯誤。對于特定的編譯器,C++異常的處理都是特定的,不能夠分享。所以,在這個意義上,一個抽象類接口表現得十足像一個C函數。

     

     

    優點:

     

     

        一個導出的C++類能夠通過一個抽象接口,被用于任何C++編譯器

     

        一個DLLC運行庫和DLL的客戶端是互相獨立的。因為資源的初始化和釋放都完全發生在DLL內部,所以客戶端不受DLLC運行庫選擇的影響。

     

        真正的模塊分離能高度完美實現。結果模塊可以重新設計和重新生成而不受工程的剩余模塊的影響。

     

        如果需要,一個DLL模塊能很方便地轉化為真正的COM模塊。

     

    缺點:

     

        一個顯式的函數調用需要創建一個新的對象實例并刪除它。盡管一個智能指針能免去開發者之后的調用

     

        一個抽象接口函數不能返回或者接受一個規則的C++對象作為一個參數。它只能以內置類型(如intdoublechar*等)或者另一個虛接口作為參數類型。它和COM接口有著相同的限制。

     

     

    STL模板類是怎樣做的

     

     

     

    C++標準模板庫的容器(如vector,listmap)和其它模板并沒有設計為DLL模塊(以抽象類接口方式)。有關DLLC++標準是沒有的因為DLL是一種平臺特定技術。C++標準不需要出現在沒有用到C++語言的其它平臺上。當前,微軟的Visual C++編譯器能夠導出和導入開發者顯式以__declspec(dllexport/dllimport)關鍵字標識的STL類實例。編譯器會發出幾個令人討厭的警告,但是還能運行。然而,你必須記住,導出STL模板實例和導出規則C++類是完全一樣的,有著一樣的限制。所以,在那方面STL是沒什么特別的。

     

    總結

     

    這篇文章討論了幾種從一個DLL模塊中導出一個C++對象的不同方法。對每種方法的優點和缺點的詳細論述也已給出。下面是得出的幾個結論:

     

        以一個完全的C函數導出一個對象有著最廣泛的開發環境和開發語言的兼容性。然而,為了使用現代編程范式一個DLL使用者被要求使用過時的C技巧對C接口作一層額外的封裝。

     

        導出一個規則的C++類和以C++代碼提供一個單獨的靜態庫沒什么區別。用法非常簡單和熟悉,然而DLL和客戶端有著非常緊密的連接。DLL和它的客戶端必須使用相同版本和相同類型的編譯器。

     

        定義一個無數據成員的抽象類并在DLL內部實現是導出C++對象的最好方法。到目前為止,這種方法在DLL和它的客戶端提供了一個清晰的,明確界定的面向對象接口。這樣一種DLL能在Windows平臺上被任何現代C++編譯器所使用。接口和智能指針一起結合使用的用法幾乎和一個導出的C++類的用法一樣方便。

     

     

    授權

     

    這篇文章,包括任何源碼和文件,遵循The Code Project Open License (CPOL)協議。

     

     RFID設備管理軟件

       作者簡介: Alex Blekhman

     

       職業:軟件開發者

     

       國籍:以色列

    RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全