Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Using .NET Classes/Modules from Native C++

4.97/5 (38 votes)
13 Sep 2009CPOL9 min read 106.3K   5.2K  
The goal of this article is to describe a more or less generic way to access .NET managed objects from a native C++ application.

Introduction

The goal of this article is to describe a more or less generic way to access .NET managed objects from a native C++ application. I will present a dynamic link library (DLL) which can be used, for example, to augment legacy C++ applications with the power of managed code. The library is written in C++/CLI, which is the only .NET language which can be used to accomplish such a task. All code was written with Visual C++ 2008; it’s also possible to do this with previous versions of the Microsoft C++ compilers, but Microsoft has done a lot of changes to C++/CLI for VS 2008, so it’s now much easier to use than older versions. The "more" generic in the first sentence means that the library can be used to call any function (with an unlimited amount of parameters) of any managed class. The "less" means that the parameter types are limited to the native C++ types and a few user defined types (string, date/time, …). It’s easy to provide support for your own types, but for that, you will have to extend the code for the DLL yourself.

Overview

Figure 1 shows a model of the idea behind the story:

figure1.png

A native C++ application holds an instance of a proxy class A’’ for the managed class A to use. This proxy passes all calls to a bridge DLL which holds a managed C++ proxy class A’ of A. The managed proxy class finally calls the managed class A. Return values and output parameters are passed back to the native proxy in the opposite direction. The proxy class A’’ presents the same public interface (transformed to C++) to the native C++ application as the managed class A presents to a managed application, so the managed class A can be transparently used from within the C++ application.

Code arrangement

The code is broken into three parts. The native to managed bridge library, the native adapter static link library, and the native tools static link library.

The native adapter static library

This library holds the base class NativeProxyBase of the native proxy classes. This base class handles the loading and freeing of the dynamic link library. It contains member functions to request the creation and freeing of instances of the managed class from the bridge library. It also contains functions to pass function calls and property access requests to the bridge library. Proxy classes in your applications should not use the NativeProxyBase class directly. There are two derivations of this class for easier access from your own proxy classes. The NativeProxy class should be used to access all non static members of the managed class. NativeProxyStatic is for use with static members.

The native to managed bridge dynamic library

This library exports a set of functions to operate on managed classes. It also holds the managed counterparts of the native proxy class in the native adapter library. The library uses .NET Reflection to invoke member functions of managed classes. It is able to load an assembly via the filename or via the full .NET assembly names.

The native tools static library

This library holds the types and conversion functions for parameter passing from C++ to managed C++/CLI.

Parameter passing

To pass any number of parameters of any type, we need a special mechanism which can handle this. Boost.Any is such a powerful facility (see the Links section). Parameters are passed as std::vector of Boost.Any (std::vector<boost::any>). Each parameter of a method call is wrapped in a Boost.Any object. On the managed C++/CLI side, the parameters are unwrapped and converted to an affiliated managed type. This unwrapping is the reason for the limited generalization, as mentioned in the introduction. Each type, and its managed affiliate, wrapped into a Boost.Any, must be known by the unwrapping code in the bridge library. To remove the direct dependency to Boost.Any, the alias AnyType (typedef boost::any AnyType) is defined in the native tools library. All your code should use AnyType instead of boost::any, so it’s easy to replace Boost.Any with a home brewed facility.

How to use

A simple example: suppose we have a managed class Hello in a .NET assembly (hello.dll).

C#
//C#
namespace Universe
{
    class Hello
    {
        public void HelloWorld()
        {
            Console.Writeline("Hello World!");
        }
    }
}

Now we need to define a native proxy class for Hello:

MC++
//C++
namespace Universe {
class Hello
{
public:
    Hello() : wrapper_("hello.dll", "Universe.Hello") {}
    void HelloWorld()
    {
        wrapper_("Hello");
    }
private:
    nativeAdapter::NativeProxy wrapper_;
};
}

In our main function, we can use the proxy as if it were the managed class:

MC++
//C++
int main(int, char **)
{
    Universe::Hello hello;
    hello.Hello();
    return 0;
}

If you run this little program, “Hello world!” will be printed to the console, as expected. The wrapper_ instance of the NativeProxy class loads the NativeToManagedBridge.dll in its constructor. It also loads the assembly hello.dll and creates an instance of the managed Hello class. The object is locked to avoid releasing it by the garbage collector, because there is no managed reference to the managed object. The lock is released by the destructor of the NativeProxy class, which also releases the handle to NativeToManagedBridge.dll. The destructor will be called from the destructor of the C++ Hello class destructor (called when the hello object goes out of scope). To call a method of the managed class, use one of the overloaded function call operators from NativeProxy, or call an executeManaged member function. The first parameter is always the method name of the managed class.

Parameters, return values, output parameters, and properties

A simple person representation in C#:

C#
//C#
namespace Universe
{
class Person
{
    public HelloWorld(string firstname, string lastname, int age)
    {
        firstname_ = firstname;
        lastname_ = lastname;
        age_ = age;
    }
    public string fullName()
    {
        Return firstname_ + " " + lastname_;
    }
    public string Age
    {
        get {return age_;}
        set {age_ = value;}
    }
    public int getAgeAndNames(out string first, out string last)
    {
        first = firstname_;
        last = lastname_;
        return age_;
    }
    private string firstname_, lastname_;
    private int age_;
}
}

The C++ proxy for Person:

MC++
//C++
namespace Universe {
class Person
{
public:
    Person(const AnyUnicodeString &firstname, 
           const AnyUnicodeString &lastname, int age)
    {
        AnyTypeArray params(3);
        params[0] = anyFromUnicodeString(firstname);
        params[1] = anyFromUnicodeString(lastname);
        params[2] = anyFromInt32(age);
        wrapper_ = new nativeAdapter::NativeProxy("hello.dll", 
                       "Universe.Person", params);
    }
    ~Person()
    {
        delete wrapper_;
    }
    int getAge() const
    {
        AnyType age;
        wrapper_->get("Age", age);
        return int32FromAny(age);
    }
    void setAge(int age)
    {
        wrapper_->set("Age", anyFromInt32(age));
    }
    int getAgeAndNames(AnyUnicodeString &first, AnyUnicodeString &last)
    {
        AnyTypeArray params(2);
        params[0] = anyFromUnicodeString(first);
        params[1] = anyFromUnicodeString(last);
        AnyType result;
        (*wrapper_)("getAgeAndNames", params, result);
        first = unicodeStringFromAny(params[0]);
        last = unicodeStringFromAny(params[1]);
        return int32FromAny(result);
    }
private:
    nativeAdapter::NativeProxy *wrapper_;
};
}

This sample shows how to pass parameters in constructors, and how to handle return values and .NET properties. To pass parameters, an AnyTypeArray has to be constructed (an alias for std::vector<:any>) and filled with parameter values from the constructor. To wrap the values into an Any object, use the wrapper functions provided by the native tools library. For the support of .NET properties, the NativeProxy offers two methods get and set. The string parameter is the property name. The get method returns the property value directly wrapped into an AnyType object. The set method takes the new property value as the second parameter. To retrieve a return value of a method call, just pass in a result object of type AnyType. To unwrap the Any object, use the unwrapper functions provided by the native tools library. Output and reference parameters are not handled differently in passing to the bridge library. They will be filled/overwritten by the bridge after returning from the managed member function.

What about static member functions?

C#
//C#
namespace Universe
{
class Planet
{
    public static void ShowVersion()
    {
        Console.Writeline("v1.1");
    }
}
}
MC++
//C++
namespace Universe {
class Planet
{
public:
    static void ShowVersion()
    {
        using namespace nativeAdapter;
        NativeProxyStatic wrapper("hello.dll", "Universe.Planet");
        wrapper("ShowVersion");
    }
};
}

As you can see, the NativeProxyStatic class works almost the same as the NativeProxy class. Except it doesn’t create an instance of the managed class. Parameters, return values, and properties are handled the same way as in NativeProxy.

Parameter types

Predefined

The following types can be used to pass as parameters or return values. They are defined in the anyType.h header file of the native tools library.

ConcerningAny type nameC++ typeC# type
BooleanAnyBooleanboolbool
Signed integersAnyInt64, AnyInt32, AnyInt16, AnyInt8__int64, __int32, __int16, __int8Int64, Int32, Int16, SByte
Unsigned integersAnyUInt64, AnyUInt32, AnyUInt16, AnyUInt8unsigned __int64, unsigned __int32, unsigned __int16, unsigned __int8UInt64, UInt32, UInt16, byte
Floating pointAnyFloat128, AnyFloat64, AnyFloat32long double, double, float-, Double, Single
Date/TimeAnyDateTimetmDateTime
CharactersAnyAsciiChar, AnyUnicodeChar, AnyTCharchar, wchar_t, TCHARchar
StringsAnyAsciiString, AnyUnicodeString, AnyTStringstd::string, std::wstring, std::basic_string<TCHAR>string
ArraysAnyByteArray, AnyAsciiStringArray, AnyUnicodeStringArray, AnyTStringArraystd::vector<unsigned __int8>, std::vector<std::string>, std::vector<std::wstring>, std::vector<std::basic_string <TCHAR> >byte[], string[], string[], string[]
Table 1: Predefined data types

For the types in the table 1, wrapper and unwrapper functions are provided by the native tools library.

MFC/Qt types

Some of the MFC and Qt data types are easily converted to the predefined types from table 1; look at mfc_any_conversions.h of the testMFC project and qt_any_conversions.h in the testQt project for wrapper and unwrapper functions.

MFC TypeAnyTypeC++ typeC# type
CStringAnyAsciiString or AnyUnicodeStringstd::string or std::wstringstring
CByteArrayAnyByteArraystd::vectory<unsigned __int8>byte[]
COleDateTimeAnyDateTimetmDateTime
CStringArrayAnyTStringArraystd::vector<std::string> or std::vector<std::wstring>string[]
Table 2: MFC types
Qt TypeAnyTypeC++ typeC# type
QCharAnyUnicodeCharwchar_tchar
QStringAnyUnicodeStringstd::wstringstring
QByteArrayAnyByteArraystd::vector<unsigned __int8>byte[]
QDateTimeAnyDateTimetmDateTime
QStringListAnyUnicodeStringArraystd::vector<std::wstring>string[]
Table 3: Qt types

Passing your own types to managed classes

If the predefined types are not sufficient, some additional work has to be done. Keep in mind that the type must be compatible with C++/CLI.

  1. Define an AnyType… alias for it (anyType.h in the native tools library).
  2. Provide a wrapper function anyFrom… () (anyType.h in the native tools library).
  3. Provide an unwrap function …FromAny() (anyType.h in the native tools library)
  4. Extend the toObject(…) function in anyHelper.cpp of the native to managed bridge library, to convert your type (unwrap it from an any object) to a managed System::Object.
  5. Extend the toAny(…) function in anyHelper.cpp of the native to managed bridge library, to convert a System::Object to your type and wrap it to an Any object.

Locating .NET assemblies

Locating assemblies by filename

If a filename of a .NET assembly is passed to the constructor of the NativeProxy class, the native to managed bridge library tries to load this assembly file the following way:

  1. Try to load the assembly without modification of the path (e.g., if the full path is specified).
  2. If 1.) fails, the loader looks in the sub directory netmodules of the application directory.
  3. If 1.) and 2.) fails, the assembly is searched for in the application directory.

Locating assemblies by full assembly name

If a fully qualified assembly name is passed to the constructor of the NativeProxy class, the .NET Framework tries to load the assembly as described in the MSDN Library (How the runtime locates assemblies). The bridge library loads the assembly via the Assembly.Load method.

The following sample shows how to specify an assembly via its full name:
MC++
//C++
NativeProxy form("System.Windows.Forms, "
                 "Version=2.0.0.0, "
                 "Culture=neutral, "
                 "PublicKeyToken=b77a5c561934e089, "
                 "processorArchitecture=MSIL"
                 , System.Windows.Forms.Form);
form("show");

Error handling

To pass exceptions raised in the .NET code over to the native C++ code, all .NET exceptions are caught within the bridge library. The full exception text is returned to the native proxy classes. In the NativeProxyBase class, a C++ exception of type ManagedException is raised again with the same text. The nativeAdapter library holds the declaration of the exception class ManagedException which is derived from std::exception.

C++
//C++
try
{
    Universe::Hello hello;
    hello.Hello();
}
catch(std::exception &ex)
{
    std::cout << ex.what() << std::endl;
}

Links

License

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