C#调用C/C++ 动态链接库DLL(四) -- 调用C++类

对于在C#中调用C++类的情况比较复杂,至少有三种情况,见下文的Introduction部分,并详细讲述了P/Invoke


以下内容转自:

1. Introduction

This article has been revised. Checkherefor updates.

There are many reasons why you would want to reuse unmanaged C/C++libraries; the most important one is perhaps that you want to use existing tools, utilities, andclasses written in unmanaged C/C++. They could be third-party tools or in-house libraries. When choosing an approach to reusing unmanaged libraries, you normally have three options:

  1. IJWor It Just Works. This is one of the greatest features that .NET Framework has provided to developers. You just recompile the old code on the new .NET platform. No or little changes are necessary. Don't forget though; it works in theC++language only.
  2. COM. The COM model works on both the unmanaged and managed environments. It's straightforward to perform a COM Invoke on .NET. But, if your unmanagedclasses are not COM-ready, you probably won't rewrite all the old code to support COM.
  3. P/Invokeor Platform Invoke. This mechanism allows you to import aclassas functions at the attribute level. Basically, you importclassmethods one by one as individual functions, as you do with Win32 APIs.

If your unmanagedC++libraries are not COM-ready, you can choose between IJW and P/Invloke. Also, you may combine the two approaches in your importing practice. As IJW requiresC++source code, if you don't have the source code, P/Invoke probably is the only option available. Using Win32 API via[DllImport]attributes is a typical example of P/Invoke in .NET development.

This article will discuss how we can use unmanagedC++classes exported from a DLL. No source code for the unmanagedC++libraries are required to be present. In particular, I will demonstrate how to wrap up your unmanagedclasses into managed ones so that any .NET application can use them directly. I will take a practical approach and omit theoretical discussions where possible. All the samples and source code provided in this article are simple and for tutorial purposes only. In order to use the source code included in the article, you should have Visual Studio 2005 and .NET Framework 2.0 installed. However, the wrapping technique remains the same on VS 2003 and .NET Framework 1.x. The unmanaged DLL has been compiled on VisualC++6.0, which is not required if you don't recompile the unmanaged source.

2. Sample UnmanagedC++Library

Go to Top

The following segment is the definition of a baseclass"Vehicle" and its derivedclass"Car":

// The following ifdef block is the standard way of creating macros which make exporting 
// from a DLL simpler. All files within this DLL are compiled with the CPPWIN32DLL_EXPORTS
// symbol defined on the command line. this symbol should not be defined on any project
// that uses this DLL. This way any other project whose source files include this file see 
// CPPWIN32DLL_API functions as being imported from a DLL, whereas this DLL sees symbols
// defined with this macro as being exported.

#ifdef CPPWIN32DLL_EXPORTS
#define CPPWIN32DLL_API __declspec(dllexport) 
#else 
#define CPPWIN32DLL_API __declspec(dllimport) 
#endif 

// This class is exported from the CppWin32Dll.dll
class CPPWIN32DLL_API Vehicle 
{
public:
    Vehicle(char* idx);
    // Define the virtual destructor

    virtual ~Vehicle();
    
    char* GetId() const;
    // Define a virtual method

    virtual void Move();
    
protected:
    char* id;
};

class CPPWIN32DLL_API Car : public Vehicle
{
public:
    ~Car();
    // Override this virtual method

    void Move();
};

By all means, the twoclasses are very simple. However, they bear two most important characteristics:

  1. The baseclasscontains a virtual destructor.
  2. The derivedclassoverrides a virtual method of the baseclass.

To demonstrate the invoke sequence, I've inserted aprintfstatement in each method. For your reference, here is the complete source of "CppWin32Dll.cpp":

#include "stdafx.h"
#include "CppWin32Dll.h"

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
};

// This is the constructor of a class that has been exported.
// see CppWin32Dll.h for the class definition

Vehicle::Vehicle(char* idx) : id(idx)
{ 
    printf("Called Vehicle constructor with ID: %s\n", idx);
};

Vehicle::~Vehicle() 
{ 
    printf("Called Vehicle destructor\n");
};    
char* Vehicle::GetId() const 
{ 
    printf("Called Vehicle::GetId()\n");
    return id;
};
void Vehicle::Move() 
{ 
    printf("Called Vehicle::Move()\n");
};

Car::~Car() 
{ 
    printf("Called Car destructor\n");
};
void Car::Move() 
{ 
    printf("Called Car::Move()\n");
};

I have built the twoclasses into a Win32 DLL called "CppWin32Dll.dll" on VisualC++6.0. All our importing work will be based on this DLL and the header, "CppWin32Dll.h". We are not going to use the unmanaged source hereafter.

As with all unmanaged DLLs, we cannot use "CppWin32Dll.dll" as an assembly/reference. Although P/Invoke allows us to import functions exported by the DLL, we cannot importclasses. What we can do is import all the methods in aclassand wrap them in a managedclass, which then can be used by .NET applications written in any .NET compatible language,C++, C#, VB, or J#.

3. Retrieve Exported Information from the DLL

Go to Top

As the first step, we are going to importclassmethods from the DLL. As we don't have access to the source code, we use the Microsoft dumping tool "dumpbin.exe" to retrieve the decorated name for each function from the DLL. After executing "dumpbin /exports CppWin32Dll.dll", we get:

Sample screenshot

The ordinal segment contains all the names for all the functions. Although it lists all the functions from the DLL, you should determine which functions are accessible methods based on theclassdefinitions in the header. Mapping of the mangled names to theclassmembers is listed in the following table:

C++Decorated Name

ClassMember

Note

??0Vehicle@@QAE@ABV0@@Z

Default constructor

Added by compiler

??0Vehicle@@QAE@PAD@Z

Vehicle::Vehicle(char *)

??1Vehicle@@UAE@XZ

Vehicle::~Vehicle()

??4Vehicle@@QAEAAV0@ABV0@@Z

Classdefault structure

Added by compiler

??_7Vehicle@@6B@

Virtual table (VTB)

Added by compiler

?GetId@Vehicle@@QBEPADXZ

Vehicle::GetId()

?Move@Vehicle@@UAEXXZ

Vehicle::Move()

??0Car@@QAE@ABV0@@Z

Default constructor

Added by compiler

??1Car@@UAE@XZ

Car::~Car()

??4Car@@QAEAAV0@ABV0@@Z

Classdefault structure

Added by compiler

??_7Car@@6B@

Virtual table (VTB)

Added by compiler

?Move@Car@@UAEXXZ

Car::Move()

Be wary that the exact details of "name mangling" are compiler-dependent, and they may vary from one version to another. Interestingly, if you add/remove/changeclassmembers to the Win32 project, you will notice that the new DLL may have different "mangled names" for the constructor or otherclassmembers. This is because the "mangled name" contains all the information about theclassmember and its relationship with the rest of theclass. Any changes to this relationship will be reflected in its "mangled name" in the new DLL.

Anyway, it appears the unmanaged DLLs built by VC++6.0 on NT-based platforms (NT/2000/XP) will work with .NET applications. At the time of this writing, it is difficult to verify whether unmanaged DLLs built by older compilers on older Windows will still work. This is more like a compatibility issue.

4. Perform Platform Invoke

Go to Top

I have imported four methods: the constructor, the destructor,GetId, andMove, and put them in another unmanagedclasscalled "VehicleUnman":

/// Create a unmanaged wrapper structure as the placeholder for unmanaged class 
/// members as exported by the DLL. This structure/class is not intended to be
/// instantiated by .NET applications directly.

public struct VehicleUnman
{
    /// Define the virtual table for the wrapper

    typedef struct 
    {
        void (*dtor)(VehicleUnman*);
        void (*Move)(VehicleUnman*);    
    } __VTB;
public:
    char* id;
    static __VTB *vtb;    

    /// Perform all required imports. Use "ThisCall" calling convention to import 
    /// functions as class methods of this object (not "StdCall"). Note that we 
    /// pass this pointer to the imports. Use the "decorated name" retrieved from
    /// the DLL as the entry point.

    [DllImport("CppWin32Dll.dll", 
        EntryPoint="??0Vehicle@@QAE@PAD@Z", 
        CallingConvention=CallingConvention::ThisCall)]
    static void ctor(VehicleUnman*, char*);
    [DllImport("CppWin32Dll.dll", 
        EntryPoint="??1Vehicle@@UAE@XZ", 
        CallingConvention=CallingConvention::ThisCall)]
    static void dtor(VehicleUnman*);
    [DllImport("CppWin32Dll.dll", 
        EntryPoint="?GetId@Vehicle@@QBEPADXZ", 
        CallingConvention=CallingConvention::ThisCall)]
    static char* GetId(VehicleUnman*);
    [DllImport("CppWin32Dll.dll", 
        EntryPoint="?Move@Vehicle@@UAEXXZ", 
        CallingConvention=CallingConvention::ThisCall)]
    static void Move(VehicleUnman*);
        
    /// Delegates of imported virtual methods for the virtual table.
    /// This basically is hacking the limitation of function pointer (FP),
    /// as FP requires function address at compile time.

    static void Vdtor(VehicleUnman* w)
    {
        dtor(w);
    }
    static void VMove(VehicleUnman* w)
    {
        Move(w);
    }
    static void Ndtor(VehicleUnman* w)
    {
        ///Do nothing

    }
};


/// Create a unmanaged wrapper structure as the placeholder for unmanaged class 
/// members as exported by the DLL. This structure/class is not intended to be
/// instantiated by .NET applications directly.

public struct CarUnman
{
    /// Define the virtual table for the wrapper

    typedef struct 
    {
        void (*dtor)(CarUnman*);
        void (*Move)(CarUnman*);    
    } __VTB;
public:
    static __VTB *vtb;    

    /// Perform all required imports. Use "ThisCall" calling convention to import 
    /// functions as class methods of this object (not "StdCall"). Note that we 
    /// pass this pointer to the imports. Use the "decorated name" retrieved from
    /// the DLL as the entry point.

    [DllImport("CppWin32Dll.dll", 
        EntryPoint="??1Car@@UAE@XZ", 
        CallingConvention=CallingConvention::ThisCall)]
    static void dtor(CarUnman*);
    [DllImport("CppWin32Dll.dll", 
        EntryPoint="?Move@Car@@UAEXXZ", 
        CallingConvention=CallingConvention::ThisCall)]
    static void Move(CarUnman*);

    /// Delegates of imported virtual methods for the virtual table.
    /// This basically is hacking the limitation of function pointer (FP),
    /// as FP requires function address at compile time.

    static void Vdtor(CarUnman* w)
    {
        dtor(w);
    }
    static void VMove(CarUnman* w)
    {
        Move(w);
    }
};

Note the following:

  1. Import the exported public methods/members only.
  2. Don't import compiler-added members. They are mostly internals, and not all of them are accessible.
  3. Every imported function takes the current pointer as an input parameter, in addition to the original input parameter(s). The DLL uses this pointer to call the function properly via the decorated name "@Vehicle" or "@Car", which is how theC++compiler handlesclasses internally.
  4. I added a virtual table or VTB manually to handle virtual methods, as an emulation ofC++virtual members internal handling. The VTB contains function pointers for all virtual methods.

As you may notice, I defined two extra methods:VdtorandVMove, each to call its corresponding import. This actually is a hack/patch of function pointers in P/Invoke. As we know, a function pointer points to (the address of) a function. Here, it would point to an import, which doesn't have an address at compile time. It gets the address only through dynamical binding at run-time. The two delegates help to delay the binding between the function pointers and the actual functions.

Note that the source file should contain the initialization of the static VTB data:

/// Unmanaged wrapper static data initialization
VehicleUnman::__VTB *VehicleUnman::vtb = new VehicleUnman::__VTB;
CarUnman::__VTB *CarUnman::vtb = new CarUnman::__VTB;

5. Wrap all the Imports in ManagedClasses

Go to Top

Now, we are ready to write a new managedC++class, which will contain an object of each unmanagedclassdefined above. Here is the source:

/// Managed wrapper class which will actually be used by .NET applications.

public ref class VehicleWrap
{
public: 
    /// User-defined managed wrapper constructor. It will perform a few tasks:
    /// 1) Allocating memory for the unmanaged data
    /// 2) Assign the v-table
    /// 3) Marshall the parameters to and call the imported unmanaged class constructor

    VehicleWrap(String ^str)
    {
        tv = new VehicleUnman();
        VehicleUnman::vtb->dtor = VehicleUnman::Vdtor;
        VehicleUnman::vtb->Move = VehicleUnman::VMove;
            
        char* y = (char*)(void*)Marshal::StringToHGlobalAnsi(str);
        VehicleUnman::ctor(tv, y);
    }
    /// Let the v-table handle virtual destructor

    virtual ~VehicleWrap()
    {
        VehicleUnman::vtb->dtor(tv);
    }        
    /// Let the v-table handle method overriding

    String^ GetId()
    {
        char *str = VehicleUnman::GetId(tv);
        String ^s = gcnew String(str);
        return s;
    }
    virtual void Move()
    {
        VehicleUnman::vtb->Move(tv);
    }
private: 
    VehicleUnman *tv;
};

/// Managed wrapper class which will actually be used by .NET applications.
public ref class CarWrap : public VehicleWrap
{
public: 
    /// User-defined managed wrapper constructor. It will perform two tasks:
    /// 1) Allocating memory for the unmanaged data
    /// 2) Assign the v-table

    CarWrap(String ^str) : VehicleWrap(str)
    {
        tc = new CarUnman();
        CarUnman::vtb->dtor = CarUnman::Vdtor;
        CarUnman::vtb->Move = CarUnman::VMove;
    }
    /// Let the v-table handle virtual destructor

    ~CarWrap()
    {
        CarUnman::vtb->dtor(tc);
        /// After the DLL code handled virtual destructor, manually turn off
        /// the managed virtual destrctor capability.

        VehicleUnman::vtb->dtor = VehicleUnman::Ndtor;
    }    
    /// Let the v-table handle method overriding

    virtual void Move () override 
    {
        CarUnman::vtb->Move(tc);
    }

private:
    CarUnman *tc;
};

Several places in the source code are noticeable:

  1. Don't derive the managed "VehicleWrap" from the unmanaged "VehicleUnman". Unmanaged wrappers merely provide the storage for the originalclassmembers, including data and methods, whereas managed ones handle theclassrelationship. More importantly, you pass the unmanaged object to the DLL, not the managed one.
  2. Use managed data types in managedclasses whenever possible, particularly as input/output parameters and return types. This is more than just a good practice, rather a necessity, because other .NET developers do not have to marshal unmanaged data types at the application level.
  3. Derive "CarWrap" from "VehicleWrap" to recover the original inheritance between the two unmanagedclasses. This way, we don't have to handle the inheritance manually in the managedclasses.
  4. Assign the do-nothing function toVehicleUnman::vtb->dtorin the~Car()destructor. This is a hack to mitigate the conflict between the unmanaged DLL internals and the managedclassinheritance. I'll leave the detailed discussion of this issue to the next section.

Now, we put all theclasses in a DLL named "CppManagedDll.dll". "VehicleWrap" and "CarWrap" are two managedclasses, which are ready to be used by .NET applications. In order to test the "VehicleWrap" and the "CarWrap"classes, I created a .NETC++CLR console application project, with this source code:

// TestProgram.cpp : main project file.

#include "stdafx.h"

using namespace System;
using namespace CppManagedDll;

int main(array<System::String ^> ^args)
{
    /// Create an instance of Car and cast it differently to test polymorphism 

    CarWrap ^car1 = gcnew CarWrap("12345");

    String ^s = car1->GetId();

    Console::WriteLine(L"GetId() returned: {0:s}", s);

    car1->Move();

    /// Delete instances to test virtual destructor

    delete car1, s;

    return 0;
}

6. Inheritance, Polymorphism, and Virtual Destructor

Go to Top

As we saw earlier, I derived "CarWrap" from "VehicleWrap" to avoid the manual implementation of the original inheritance between the "Car" and "Vehicle"classes, with the assumption that theC++DLL breaks down all the relationship between the derivedclasses. This turned out not to be true. The tests revealed that it only breaks the binding between the twoMove()methods of "Vehicle" and "Car", but retains the virtual destructor binding. That is, whenever~Car()is called from outside the DLL,~Vehicle()gets called automatically. This has some adverse impact on our managedclasses, because~Vehicle()would be called twice, one by the managedclassvirtual destructor and the other by the original destructor inside the DLL. To test this, you can comment/uncomment the following line in~CarWrap():

VehicleUnman::vtb->dtor = VehicleUnman::Ndtor;

This line allows the managedclassto use its own binding, and meanwhile, to disable the unexpected binding in the DLL, which is achieved through the power of VTB and function pointer!

After we run "TestProgram.exe", we get the print-out as follows:

Called Vehicle constructor with ID: 12345
Called Vehicle::GetId()
GetId() returned: 12345
Called Car::Move()
Called Car destructor
Called Vehicle destructor

To verify the polymorphism, modify the second line in the main:

VehicleWrap ^car1 = gcnew CarWrap("12345");

You will get the same printout. If you change the line to:

VehicleWrap ^car1 = gcnew VehicleWrap ("12345");

You will get:

Called Vehicle constructor with ID: 12345
Called Vehicle::GetId()
GetId() returned: 12345
Called Vehicle::Move()
Called Vehicle destructor

As we discussed earlier, if you comment out the VTB assignment in~Car(), the "Vehicle" destructor in the DLL would be called twice:

Called Vehicle constructor with ID: 12345
Called Vehicle::GetId()
GetId() returned: 12345
Called Car::Move()
Called Car destructor
Called Vehicle destructor
Called Vehicle destructor

Surprisingly, although calling the same destructor twice is logically incorrect, it hasn't caused any crash. How could this happen? It did because the importing didn't create any object in the DLL. We will discuss this in more details in the next section.

Now, everything seems to work smoothly. We are ready to extend to multiple inheritance, another important unmanagedC++specification. Well, not quite. This extension is not feasible, not because we cannot mimic multiple inheritance, but because managedC++has abandoned this complicated concept completely. In order to comply with the managedC++standard, you should avoid legacy multiple inheritance in .NET applications.

7. Imported Resource Disposal

Go to Top

To fully understand why the two calls to the exported destructor in the DLL didn't cause any memory problems, let's first analyze where unmanaged resources are allocated:

  1. Created by your unmanaged wrapper. It's your responsibility to dispose all the resource allocated within the unmanaged wrapper.
  2. Created by the imports? No. When we import, we don't create any instance inside the DLL. Instead, the instance will be created within our managed assembly, with the unmanaged wrapper (structure) as the placeholder for all the imported functions and otherclassdata. The exported functions are allocated by the DLL on the stack. No dynamic allocation is performed during importing, as you don't "new" any objects inside the DLL. Thus, no disposing is necessary for the importing itself.
  3. Created and disposed by the DLL internally. We assume that the DLL has taken care of this properly already; in other words, the DLL is bug-free. One thing to note though. If a DLL destructor contains code to dispose any other resources, multiple calls to it may cause problems to the DLL.

8. Concluding Remarks

Go to Top

This tutorial provides an alternativeapproach to reusing unmanagedC++libraries, particularly when direct importing from unmanaged DLLs becomes necessary. I have demonstrated three steps to wrap unmanagedC++DLLs for use in .NET applications:

  1. Retrieveclassmember data from the DLL.
  2. Import requiredclassmethods.
  3. Wrap up all the imports in a managedclass.

The tutorial also shows that the implementation of the approach is not trivial, mainly because you must recover the original relationship between unmanagedclasses, such as inheritance, virtual functions, and polymorphism. ManagedC++can help, but when there are conflicts, you have to simulate someC++compiler internals. In working withC++internals, you will find virtual table and function pointer helpful.


下文讲述了如何使用IJW,下文转自:http://www.codeproject.com/Articles/2234/Using-IJW-in-Managed-C

Prologue

I have always loathed P/Invoke in an intense manner. I guess it's perhaps due to the fact that I am a very simple human being and thus I naturally disliked anything which was not simple. P/Invoke in my opinion was ugly and so pathetically unnatural. These two facets made it an utterly complicated entity. Then I came across these beautiful words by Nick Hodapp.

"IJWin C++ is syntactically easier than P/Invoke, and as I said, slightly more performant."- Nick Hodapp, Microsoft

Two things struck me immediately after I read those words. The first one, naturally was that there was no word called "performant" in the English dictionary, though I could actually understand very clearly what Nick Hodapp had meant by that word. The second more glaring point was that I didn't know whatIJWmeant. Later on when I realized thatIJWsimply meant, "It just works", I had this feeling for a few seconds that I was stuck in a world of lunacy. But after I tried it out, I simply said aloud, "It just works". Because, it really does work. And it's not ugly or unnatural like P/Invoke is. And as Nick Hodapp said, it's slightly moreperformant.

UsingIJW

All you do is to simply#includethe required C++ header file. Of course there is always a danger that there will be several name clashes between the definitions in the header file and the .NET framework classes and their member functions. I found this out the hard way when I got 100s of compilation errors. All of them simply said :- "error C2872: 'blahblahblah' : ambiguous symbol". Now as you can assume, this was a most distressing situation as far as I was concerned. It took my rather simple brain a couple of minutes to figure out that, I had to include the header file before all myusing namespacedirectives.

Unlike P/Invoke, where all the data marshalling between .NET types and native types is done by the compiler, here we must do it ourselves. It's not a complicated issue at all once you take a look at theSystem.Runtime.InteropServices.Marshalclass in the framework. Jolly nice class I tell ya, with jolly nice functions.

Without further tête-à-tête, let's see some sample code. In the tiny example program listed below I shall show you how to create a managed class, which can be instantiated from a managed block, and which usesIJWto call a native API call. You'll see how much more nicer this looks like when compared to the foul looking P/Invoke code.

Code Listing

#include "stdafx.h"

#using <mscorlib.dll>
#include <tchar.h>
#include <windows.h>

using namespace System;
using namespace System::Runtime::InteropServices;

public __gc class MsgBox
{
public:
    MsgBox(String *str)
    {
        IntPtr ptrtxt = Marshal::StringToCoTaskMemUni(str);
        MessageBoxW(0,(LPCWSTR)ptrtxt.ToPointer(),
            L"IJW is cool",0);
        Marshal::FreeCoTaskMem(ptrtxt);
    }
};

int _tmain(void)
{
 
    String *str;
    str = "Nish was here";
    MsgBox *m_msgbox = new MsgBox(str);
    return 0;
}

I have usedStringToCoTaskMemUniwhich copies the string to an unmanaged area in the heap. Once I have made my call, I must free the string that has been allocated in the unmanaged heap area, because this will not get garbage collected. Isn't it truly amazing that whenIJWexisted, a lot of us were wasting our time with P/Invoke! Of course this is available only for Managed C++ programmers. The poor C# and VB .NET guys will have to suffer the P/Invoke monster as that's their only option.

I guess this is one very good reason for the use of Managed C++ ahead of C#. I am also hoping that this is the firstIJWarticle on CP or perhaps on any non-Microsoft site. I guess I'll have to wait for Chris M to confirm that. I also do hope that it has served it's simple purpose. Thank you.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be foundhere



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值