Introduction
Save the work done in the past is a guideline in many enterprises; and they are right! The investment to save generally represents, for a programmer, several thousand men-days. Why throw a code that has proved its worth?
One option open to the programmer is to gradually switch to the new technology. For .NET, the solution is to mix managed code and native code. The approach can be done either in a top-down issue (from UI to low-level layers) or bottom-up (from low-level to UI).
The objective of this document is to present, through two simple examples, how to use the two technologies together, as well as the first “traps” to avoid, or restrictions to be taken into consideration.
Two approaches will be presented:
- How to call managed code from native code
- How to call native code from managed code
This article is not intended to cover all mixed environment aspects, traps, and tips. It is dedicated to mixed CLR beginners for a “first touch”. For a complete view of development issues, I can’t do anything but advise you to read books as the one from Stephen Phraser: “Pro Visual C++/CLI and the .NET 2.0 Platform” (Apress editor), and specially, part 3: “Unsafe/Unmanaged C++/CLI”.
Calling managed code from native code
This sample shows how a native code (C++) can use a managed class library written in C#, by using an intermediate “mixed code” DLL that exports an API using managed code.
This could seem to be a bit heavy, but this is the only way in some situations:
- If the native client is compiled with Visual Studio 2005/2008, some new compiler options allow changing how a native module can use managed code, and the intermediate C++/CLI DLL is useless. For example, since Visual Studio 2008 we have the “/clr” option.
- If a native client is compiled with a “legacy compiler” (i.e., Visual C++ 6), previous specific compiler options are not available; the application designer will have to design an intermediate module as shown above.
The pure native client
Here is the code of the console client:
#include "stdafx.h"
#include <iostream>
using namespace std;
#ifdef _UNICODE
#define cout wcout
#define cint wcin
#endif
int _tmain(int argc, TCHAR* argv[])
{
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(argv);
SYSTEMTIME st = {0};
const TCHAR* pszName = _T("John SMITH");
st.wYear = 1975;
st.wMonth = 8;
st.wDay = 15;
CPerson person(pszName, &st);
cout << pszName << _T(" born ")
<< person.get_BirthDateStr().c_str()
<< _T(" age is ") << person.get_Age()
<< _T(" years old today.")
<< endl;
cout << _T("Press ENTER to terminate...");
cin.get();
#ifdef _DEBUG
_CrtDumpMemoryLeaks();
#endif
return 0;
}
There is nothing extraordinary here… This is classical native C++ code.
It imports the header and the LIB files (in the StdAfx.h file used for the precompiled headers).
The pure managed assembly
This is a classic assembly written in C#:
using System;
namespace AdR.Samples.NativeCallingCLR.ClrAssembly
{
public class Person
{
private string _name;
private DateTime _birthDate;
public Person(string name, DateTime birthDate)
{
this._name = name;
this._birthDate = birthDate;
}
public uint Age
{
get
{
DateTime now = DateTime.Now;
int age = now.Year - this._birthDate.Year;
if ((this._birthDate.Month > now.Month) ||
((this._birthDate.Month == now.Month) &&
(this._birthDate.Day > now.Day)))
{
--age;
}
return (uint)age;
}
}
public string BirthDateStr
{
get
{
return this._birthDate.ToShortDateString();
}
}
public DateTime BirthDate
{
get
{
return this._birthDate;
}
}
}
}
As you can see, this is pure CLR.
The mixed native/CLI module
All difficulties are concentrated here. The Visual Studio environment provides a set of include files that helps the developer to make the junction with both worlds:
#include <vcclr.h>
But, the story does not stop here. We will see that there are other traps to avoid, especially while marshalling strings between the CLR and the native worlds.
Here is the class header exported to pure native modules:
#pragma once
#ifdef NATIVEDLL_EXPORTS
#define NATIVEDLL_API __declspec(dllexport)
#else
#define NATIVEDLL_API __declspec(dllimport)
#endif
#include <string>
using namespace std;
#ifdef _UNICODE
typedef wstring tstring;
#else
typedef string tstring;
#endif
class NATIVEDLL_API CPerson
{
public:
CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate);
virtual ~CPerson();
unsigned int get_Age() const;
tstring get_BirthDateStr() const;
SYSTEMTIME get_BirthDate() const;
private:
void* m_pPersonClr;
};
We made here the effort to present anything to the native caller of the CLR environment. For example, in order to avoid seeing what is exported into the vcclr.h file. That’s why we are using a void
pointer as the wrapped CLR object. Then, the caller thinks that it’s a classical C++ class.
Open the door of a strange world…
As I already said, things begin with including the vcclr.h file. But, as we will internally use CLR code and need to marshal complex types (like strings, arrays, etc.), here are the .NET “includes”:
using namespace System;
using namespace Runtime::InteropServices;
using namespace AdR::Samples::NativeCallingCLR::ClrAssembly;
Of course, we need to declare the use of the pure native assembly.
First, let’s have a look at the constructor:
CPerson::CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate)
{
DateTime^ dateTime = gcnew DateTime((int)birthDate->wYear,
(int)birthDate->wMonth,
(int)birthDate->wDay);
String^ str = gcnew String(pszName);
Person^ person = gcnew Person(str, *dateTime);
gcroot<Person^> *pp = new gcroot<Person^>(person);
this->m_pPersonClr = static_cast<void*>(pp);
}
This native class is allowed to store reference pointers on managed classes, but that’s not our goal as we don’t want to show managed code to the user code.
Moreover, as we use a void
pointer to mask the object, a new problem appears: we are not allowed to convert a managed type into an unmanaged pointer. That’s why we use the gcroot<>
template helper class.
Notice also how we write “pointers” to managed objects with the ^
character; this means we are using “reference pointers” to a managed class. Remember that class
objects in .NET are considered as references when used as function parameters.
Notice also the keyword for .NET allocations: gcnew
. This means we are allocating on the garbage collector protected environment, not on the process heap.
Be aware of that at any time, the process heap is completely different from the garbage collector protected environment. We will see that marshaling tasks will have to be done, with huge consequences on the code and performance.
Like all heap allocated objects, we will have to free the allocated memory when it is no more needed; this is done in the class destructor:
CPerson::~CPerson()
{
if (this->m_pPersonClr)
{
gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
delete pp;
this->m_pPersonClr = 0;
}
}
We use here a standard C++ type-cast through the keyword static_cast
. The deletion of the object will release the underlying wrapped CLR object, allowing it to be garbage collected.
Reminder: declaring a destructor causes when compiling the implementation of IDisposable
interface and its Dispose()
method.
Consequence: don't forget to call Dispose()
or use the C# keyword using on such CPerson
instance. Forgetting this will cause severe memory leaks, as the C++ won't be destroyed (destructor not called).
Calling simple CLR class members is easy and quite the same:
unsigned int CPerson::get_Age() const
{
if (this->m_pPersonClr != 0)
{
gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
return ((Person^)*pp)->Age;
}
return 0;
}
But things are much more complex when we must return complex types as with this class member:
tstring CPerson::get_BirthDateStr() const
{
tstring strAge;
if (this->m_pPersonClr != 0)
{
gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
strAge = (const TCHAR*)Marshal::StringToHGlobalAuto(
((Person^)*pp)->BirthDateStr
).ToPointer();
}
return strAge;
}
We cannot return a System::String
object directly into a native string. It must be decomposed into several steps:
- Get the
System::String
object. - Get a global handle on it with the help of
Marshal::StringToHGlobalAuto()
. Note that we are here using the “auto” version that gets the Unicode returned string, and convert it as necessary into an ANSI string. - Finally, get the pointer on the underlying object of the handle.
We have here three steps instead of one!
Reading reference books on C++/CLI, you will meet other specific keywords as pin_ptr<>
and internal_ptr<>
that allow you to get the underlying pointer of the object in a short time. See documentations for more details.
The big mix
This standalone sample shows how to build a native console application with MFC and CLR! Except the particularity of how to initialize MFC from a console application, this sample uses concepts that have been seen before. This sample is presented only “for the fun”.
Conclusion (native code calling managed code)
Using managed code in native code is one of the most complex things to do. The sample shown here is very simple. As simple as it is, you have seen some complex considerations. Hope that you will meet many others in your experience on mixed code.
Calling native code from managed code
This sample shows how a CLR code (C#) can use a native class library written in C++, by using an intermediate “mixed code” DLL that exports an API using unmanaged code.
If the .NET client is written in C++/CLI, it can be transformed to call pure native C++ code; but as writing mixed C++/CLI is quite hard, this could be an expensive experience. Minimizing the intermediate mixed DLL is the fastest way to incorporate native code.
The native C++ DLL
The DLL simply exports:
- A C++ class
- A C-style function
- A C-style variable
This paragraph presents object declarations. As they are simplest as possible, comments are unnecessary.
The module is compiled as a regular DLL without any particular option for future use by a .NET module.
The C++ class
class NATIVEDLL_API CPerson {
public:
CPerson(LPCTSTR pszName, SYSTEMTIME birthDate);
unsigned int get_Age();
private:
TCHAR m_sName[64];
SYSTEMTIME m_birthDate;
CPerson();
};
The get_Age()
accessor simply computes a duration between the current date and the birth date.
The exported C function
int fnNativeDLL(void);
The exported C variable
int nNativeDLL;
The.NET client
There is nothing to say about this module. Everything is classical.
The mixed native/managed C++ DLL
Here begins the hard work…
Note 1:
C++ .NET classes (managed) cannot inherit from native C++ classes. Writing a C++ managed class compels us to internally embed an instance of any native C++ object. Moreover, in order to be used by other managed code, a C++ managed class cannot use unmanaged types as parameters or attributes.
Note 2:
Declaring a member CPerson _person2;
would generate a C4368 compiler error (cannot define 'member' as a member of managed 'type': mixed types are not supported).
That's why a pointer (seen as 'unsafe
' in C#) is used internally.
What says the documentation:
You cannot embed a native data member in a CLR type. You can, however, declare a pointer to a native type and control its lifetime in the constructor and destructor and the finalizer of your managed class (see Destructors and Finalizers in Visual C++ for more information).
That’s why the embedded object is:
CPerson* _pPerson;
Not:
CPerson person;
Special information on the constructor
The public constructor takes a System::String
string (managed type) and a SYSTEMTIME
structure (Win32 API type but only numeric; marshalling is obvious).
As the native C++ CPerson
constructor takes a LPCTSTR
string pointer, the managed string cannot be transmitted directly to the unmanaged object.
Here is the code for the constructor:
SYSTEMTIME st = { (WORD)birthDate.Year,
(WORD)birthDate.Month,
(WORD)birthDate.DayOfWeek,
(WORD)birthDate.Day,
(WORD)birthDate.Hour,
(WORD)birthDate.Minute,
(WORD)birthDate.Second,
(WORD)birthDate.Millisecond };
pin_ptr<const TCHAR> psz = PtrToStringChars(name);
_pPerson = new CPerson(psz, st);
Notice the use of the pin_ptr
keyword in order to protect the string against CLR operations.
A pinning pointer is an interior pointer that prevents the object pointed into from moving on to the garbage-collected heap (the value of a pinning pointer is not changed by the common language runtime). This is necessary when passing the address of a managed class to an unmanaged function because the address will not change unexpectedly during the resolution of the unmanaged function call.
The object is no longer pinned when its pinning pointer goes out of scope, or is set to nullptr
.
C-style APIs
C-style APIs can be used in two ways:
- Using a wrapper method/attribute
- Using the
[DllImport]
attribute as method decoration
Note that the second way can only be used on functions. It cannot be used with a variable export. In order to call variable exports, the developer must use the first way.
Conclusion (managed code calling native code)
If we can see that importing native code into managed code is simpler than the opposite, consider that writing the “intermediate assembly” is not so easy.
You will have to make sure that the investment is really less than that for a complete code migration. Consider redesigning a complete application taking into account an ISO-functional rewriting to managed code (C# is so similar to C++) could be less expensive than a migration. Moreover, the final application architecture is often cleaner.
History
- Monday, April 6th, 2009: Article published; initial release.
- Saturday, April 5th, 2014: Fixed memory leaks, migration to VS2013 and Framework .Net 4.0, added x64 target.