Click here to Skip to main content
15,867,756 members
Articles / Programming Languages / C
Article

Using Unmanaged C++ Libraries (DLLs) in .NET Applications

Rate me:
Please Sign up or sign in to vote.
4.81/5 (42 votes)
2 Jun 2006CPOL11 min read 587.3K   7.9K   209   60
An article on how to use unmanaged C++ classes exported from pre-built libraries (DLLs) with no source code.

Contents

1. Introduction

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:

  1. IJW or 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 the C++ 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 unmanaged classes are not COM-ready, you probably won't rewrite all the old code to support COM.
  3. P/Invoke or Platform Invoke. This mechanism allows you to import a class as functions at the attribute level. Basically, you import class methods one by one as individual functions, as you do with Win32 APIs.

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 unmanaged classes 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.

2. Sample Unmanaged C++ Library

Go to Top

The following segment is the definition of a base class "Vehicle" and its derived class "Car":

C++
// 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:

  1. The base class contains a virtual destructor.
  2. The derived class overrides a virtual method of the base class.

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":

C++
#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 a class 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#.

3. Retrieve Exported Information from the DLL

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:

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 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

Vehicle::Vehicle(char *)

??1Vehicle@@UAE@XZ

Vehicle::~Vehicle()

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

Class default 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

Class default 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/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.

4. Perform Platform Invoke

Go to Top

I have imported four methods: the constructor, the destructor, GetId, and Move, and put them in another unmanaged class called "VehicleUnman":

MC++
/// 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 the C++ compiler handles classes internally.
  4. I added a virtual table or VTB manually to handle virtual methods, as an emulation of C++ virtual members internal handling. The VTB contains function pointers for all virtual methods.

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:

MC++
/// 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 Managed Classes

Go to Top

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

MC++
/// 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 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.
  2. Use managed data types in managed classes 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 unmanaged classes. This way, we don't have to handle the inheritance manually in the managed classes.
  4. Assign the do-nothing function to 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 managed classes, 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:

MC++
// 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 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():

MC++
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:

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

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

MC++
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.

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 other class data. 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 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:

  1. Retrieve class member data from the DLL.
  2. Import required class methods.
  3. Wrap up all the imports in a managed class.

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. Managed C++ 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.

9. Revision History

Go to Top

  • 21 May 2006: First revision of the article and source code.
  • 28 May 2006: Revised with the following updates:
    • Added section: "7. Imported Resource Disposal".
    • Added comments to the source code.
    • Changed unmanaged wrapper from class to structure.

    Thanks to vmihalj for the inspiring question leading to this update.

  • 2 June 2006: Revised with the following additions:
    • Added multiple inheritance discussion to section 3.
    • Added "name mangling" discussion to section 6.

    Thanks to lsanil for the inspiring question leading to this update.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect GuestLogix Inc.
Canada Canada
Jun is an experienced software architect. He wrote his first computer code on the tape machine for a "super computer". The tape machine reads holes on the black pape tape as source code. When manually fixing code, you need a punch and tranparent tape. To delete code, you block holes or cut off a segment and glue two ends together. To change code, you block old holes and punch new holes. You already know how to add new code, don't you? Anyway, that was his programming story in early 1980's.

Jun completed university with the specialty in oceanography, and graduate study in meteorology. He obtained his Ph.D. in physics. Jun has worked in a number of different areas. Since mid-90's, he has been working as a software professional in both military & commercial industries, including Visual Defence, Atlantis Systems International and Array Systems Computing.

Currently, Jun is an architect at GuestLogix, the global leader in providing onboard retail solutions for airlines and other travel industries. He is also the founder of Intribute Dynamics, a consulting firm specialized in software development. He has a personal blog site, although he is hardly able to keep it up to date.

In his spare time, Jun loves classic music, table tennis, and NBA games. During the summer, he enjoyes camping out to the north and fishing on wild lakes.

Comments and Discussions

 
GeneralI have the C++ headers Pin
maihem28-May-06 15:11
maihem28-May-06 15:11 
GeneralRe: I have the C++ headers Pin
Jun Du28-May-06 16:16
Jun Du28-May-06 16:16 
GeneralRe: I have the C++ headers Pin
maihem29-May-06 2:07
maihem29-May-06 2:07 
GeneralRe: I have the C++ headers Pin
Jun Du29-May-06 4:39
Jun Du29-May-06 4:39 
GeneralRe: I have the C++ headers Pin
maihem29-May-06 11:14
maihem29-May-06 11:14 
GeneralRe: I have the C++ headers Pin
Jun Du30-May-06 13:37
Jun Du30-May-06 13:37 
QuestionDisposing? Pin
vmihalj24-May-06 0:27
vmihalj24-May-06 0:27 
AnswerRe: Disposing? Pin
Jun Du24-May-06 5:21
Jun Du24-May-06 5:21 
GeneralTo expeceive in terms of development Pin
olegxxx23-May-06 21:42
olegxxx23-May-06 21:42 
GeneralRe: To expeceive in terms of development Pin
Jun Du24-May-06 4:56
Jun Du24-May-06 4:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.