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:
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).
namespace Universe
{
class Hello
{
public void HelloWorld()
{
Console.Writeline("Hello World!");
}
}
}
Now we need to define a native proxy class for Hello
:
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:
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#:
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
:
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?
namespace Universe
{
class Planet
{
public static void ShowVersion()
{
Console.Writeline("v1.1");
}
}
}
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.
Concerning | Any type name | C++ type | C# type |
Boolean | AnyBoolean | bool | bool |
Signed integers | AnyInt64 , AnyInt32 , AnyInt16 , AnyInt8 | __int64 , __int32 , __int16 , __int8 | Int64 , Int32 , Int16 , SByte |
Unsigned integers | AnyUInt64 , AnyUInt32 , AnyUInt16 , AnyUInt8 | unsigned __int64 , unsigned __int32 , unsigned __int16 , unsigned __int8 | UInt64 , UInt32 , UInt16 , byte |
Floating point | AnyFloat128 , AnyFloat64 , AnyFloat32 | long double , double , float | -, Double , Single |
Date/Time | AnyDateTime | tm | DateTime |
Characters | AnyAsciiChar , AnyUnicodeChar , AnyTChar | char , wchar_t , TCHAR | char |
Strings | AnyAsciiString , AnyUnicodeString , AnyTString | std::string , std::wstring , std::basic_string<TCHAR> | string |
Arrays | AnyByteArray , AnyAsciiStringArray , AnyUnicodeStringArray , AnyTStringArray | std::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 Type | AnyType | C++ type | C# type |
CString | AnyAsciiString or AnyUnicodeString | std::string or std::wstring | string |
CByteArray | AnyByteArray | std::vectory<unsigned __int8> | byte[] |
COleDateTime | AnyDateTime | tm | DateTime |
CStringArray | AnyTStringArray | std::vector<std::string> or std::vector<std::wstring> | string[] |
Table 2: MFC types
Qt Type | AnyType | C++ type | C# type |
QChar | AnyUnicodeChar | wchar_t | char |
QString | AnyUnicodeString | std::wstring | string |
QByteArray | AnyByteArray | std::vector<unsigned __int8> | byte[] |
QDateTime | AnyDateTime | tm | DateTime |
QStringList | AnyUnicodeStringArray | std::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.
- Define an
AnyType…
alias for it (anyType.h in the native tools library). - Provide a wrapper function
anyFrom… ()
(anyType.h in the native tools library). - Provide an unwrap function
…FromAny()
(anyType.h in the native tools library) - 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
. - 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:
- Try to load the assembly without modification of the path (e.g., if the full path is specified).
- If 1.) fails, the loader looks in the sub directory netmodules of the application directory.
- 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:
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
.
try
{
Universe::Hello hello;
hello.Hello();
}
catch(std::exception &ex)
{
std::cout << ex.what() << std::endl;
}
Links