对于在C#中调用C++类的情况比较复杂,至少有三种情况,见下文的Introduction部分,并详细讲述了P/Invoke
This article has been revised. Check here for 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, and classes 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:
If your unmanaged C++ 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 requires C++ 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 unmanaged C++ classes exported from a DLL. No source code for the unmanaged C++ 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 Visual C++ 6.0, which is not required if you don't recompile the unmanaged source.
Go to Top
The following segment is the definition of a base class "Vehicle
" and its derived class "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 two classes are very simple. However, they bear two most important characteristics:
To demonstrate the invoke sequence, I've inserted a printf
statement 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 two classes into a Win32 DLL called "CppWin32Dll.dll" on Visual C++ 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 import classes. What we can do is import all the methods in aclass and wrap them in a managed class, which then can be used by .NET applications written in any .NET compatible language, C++, C#, VB, or J#.
Go to Top
As the first step, we are going to import class methods 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:
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 the class definitions in the header. Mapping of the mangled names to the class members is listed in the following table:
C++ Decorated Name |
Class Member |
Note |
---|---|---|
??0Vehicle@@QAE@ABV0@@Z |
Default constructor |
Added by compiler |
??0Vehicle@@QAE@PAD@Z |
|
|
??1Vehicle@@UAE@XZ |
|
|
??4Vehicle@@QAEAAV0@ABV0@@Z |
Class default structure |
Added by compiler |
??_7Vehicle@@6B@ |
Virtual table (VTB) |
Added by compiler |
?GetId@Vehicle@@QBEPADXZ |
|
|
?Move@Vehicle@@UAEXXZ |
|
|
??0Car@@QAE@ABV0@@Z |
Default constructor |
Added by compiler |
??1Car@@UAE@XZ |
|
|
??4Car@@QAEAAV0@ABV0@@Z |
Class default structure |
Added by compiler |
??_7Car@@6B@ |
Virtual table (VTB) |
Added by compiler |
?Move@Car@@UAEXXZ |
|
|
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/change class members to the Win32 project, you will notice that the new DLL may have different "mangled names" for the constructor or other class members. This is because the "mangled name" contains all the information about the class member and its relationship with the rest of the class. 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.
Go to Top
I have imported four methods: the constructor, the destructor, GetId
, and Move
, and put them in another unmanaged class called "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:
@Vehicle
" or "@Car
", which is how the C++ compiler handles classes internally.As you may notice, I defined two extra methods: Vdtor
and VMove
, 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;
Go to Top
Now, we are ready to write a new managed C++ class, which will contain an object of each unmanaged classdefined 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:
VehicleWrap
" from the unmanaged "VehicleUnman
". Unmanaged wrappers merely provide the storage for the original class members, including data and methods, whereas managed ones handle the class relationship. More importantly, you pass the unmanaged object to the DLL, not the managed one.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 managed classes.VehicleUnman::vtb->dtor
in the ~Car()
destructor. This is a hack to mitigate the conflict between the unmanaged DLL internals and the managed class inheritance. I'll leave the detailed discussion of this issue to the next section.Now, we put all the classes 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 .NET C++ 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; }
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 the C++ DLL breaks down all the relationship between the derived classes. This turned out not to be true. The tests revealed that it only breaks the binding between the two Move()
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 managed classes, because ~Vehicle()
would be called twice, one by the managed class virtual 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 managed class to 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 unmanaged C++ specification. Well, not quite. This extension is not feasible, not because we cannot mimic multiple inheritance, but because managed C++ has abandoned this complicated concept completely. In order to comply with the managed C++ standard, you should avoid legacy multiple inheritance in .NET applications.
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:
new
" any objects inside the DLL. Thus, no disposing is necessary for the importing itself.Go to Top
This tutorial provides an alternative approach to reusing unmanaged C++ libraries, particularly when direct importing from unmanaged DLLs becomes necessary. I have demonstrated three steps to wrap unmanaged C++ DLLs for use in .NET applications:
The tutorial also shows that the implementation of the approach is not trivial, mainly because you must recover the original relationship between unmanaged classes, such as inheritance, virtual functions, and polymorphism. ManagedC++ can help, but when there are conflicts, you have to simulate some C++ compiler internals. In working with C++internals, you will find virtual table and function pointer helpful.
下文讲述了如何使用IJW,下文转自:http://www.codeproject.com/Articles/2234/Using-IJW-in-Managed-C
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.
"IJW in 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 what IJW meant. Later on when I realized that IJWsimply 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 more performant.
All you do is to simply #include
the 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 my using namespace
directives.
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.Marshal
class 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 uses IJW to call a native API call. You'll see how much more nicer this looks like when compared to the foul looking P/Invoke code.
#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 used StringToCoTaskMemUni
which 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 when IJW existed, 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 firstIJW article 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.
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 found here