Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / Win32

Mixing .NET and native code

Rate me:
Please Sign up or sign in to vote.
4.85/5 (49 votes)
7 Apr 2014CPOL9 min read 163K   7.2K   139   20
A first approach to mixing .NET and native code, using the C++/CLI gateway.

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:

  1. How to call managed code from native code
  2. 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

mixnetnative/Fig1_-_Native_calling_managed.png

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:

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

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:

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

C++
#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:
   // Initialization
   CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate);
   virtual ~CPerson();

   // Accessors
   unsigned int get_Age() const;
   tstring get_BirthDateStr() const;
   SYSTEMTIME get_BirthDate() const;

private:
   // Embedded wrapper of an instance of a CLR class
   // Goal: completely hide CLR to pure unmanaged C/C++ code
   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”:

C#
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:

C++
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);
   // Managed type conversion into unmanaged pointer is not
   // allowed unless we use "gcroot<>" wrapper.
   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:

C++
CPerson::~CPerson()
{
   if (this->m_pPersonClr)
   {
      // Get the CLR handle wrapper
      gcroot<Person^> *pp =  static_cast<gcroot<Person^>*>(this->m_pPersonClr);
      // Delete the wrapper; this will release the underlying CLR instance
      delete pp;
      // Set to null
      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:

C++
unsigned int CPerson::get_Age() const
{
   if (this->m_pPersonClr != 0)
   {
      // Get the CLR handle wrapper
      gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);
      // Get the attribute
      return ((Person^)*pp)->Age;
   }

   return 0;
}

But things are much more complex when we must return complex types as with this class member:

C++
tstring CPerson::get_BirthDateStr() const
{
   tstring strAge;
   if (this->m_pPersonClr != 0)
   {
      // Get the CLR handle wrapper
      gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr);

      // Convert to std::string
      // Note:
      // - Marshaling is mandatory
      // - Do not forget to get the string pointer...
      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:

  1. Get the System::String object.
  2. 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.
  3. 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

mixnetnative/Fig2_-_Managed_calling_native.png

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

C++
class NATIVEDLL_API CPerson {
public:
   // Initialization
   CPerson(LPCTSTR pszName, SYSTEMTIME birthDate);
   // Accessors
   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

C++
int fnNativeDLL(void);

The exported C variable

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

C++
CPerson* _pPerson;

Not:

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

C++
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 'name' memory before calling unmanaged code
pin_ptr<const TCHAR> psz = PtrToStringChars(name);

// Allocate the unmanaged object
_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:

  1. Using a wrapper method/attribute
  2. 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.

License

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


Written By
Architect
France France
I started working in 1991 as an engineer. My pecialization: development in robotics.

After two years in this job, I began a new career leaving robotics, and coming to standard development as C++ expert.

This enabled me to diversify my knowledge by putting one foot on the side of the systems. Then began my double competence.

In 2000 I started a new job as consultant. Many experiences came with that new job, continuing with both development and systems subjects.

I reinforced my knowledge in technical architectures, IT security, and IT production management.

2011: a new experience. I founded the company Net-InB, a computer services company specialized in the fields of infrastructures, systems and software.

Now I am both technical and software architect specialized in Microsoft products and technologies.

Comments and Discussions

 
QuestionIn btnDirectCallClick Exception thrown Pin
Krischu29-Oct-17 5:42
Krischu29-Oct-17 5:42 
GeneralMy vote of 4 Pin
KarstenK8-Dec-16 21:59
mveKarstenK8-Dec-16 21:59 
QuestionNative Splash and .NET application Pin
Robert Kindl15-Apr-14 1:38
Robert Kindl15-Apr-14 1:38 
QuestionDose not released allocated memory from clr Pin
Aydin Homay8-Mar-14 1:54
Aydin Homay8-Mar-14 1:54 
AnswerRe: Dose not released allocated memory from clr Pin
Alain DOS REIS1-Apr-14 11:06
Alain DOS REIS1-Apr-14 11:06 
GeneralRe: Dose not released allocated memory from clr Pin
Aydin Homay2-Apr-14 2:03
Aydin Homay2-Apr-14 2:03 
GeneralRe: Dose not released allocated memory from clr Pin
Alain DOS REIS4-Apr-14 17:45
Alain DOS REIS4-Apr-14 17:45 
GeneralRe: Dose not released allocated memory from clr Pin
Aydin Homay4-Apr-14 19:00
Aydin Homay4-Apr-14 19:00 
GeneralRe: Dose not released allocated memory from clr Pin
Alain DOS REIS4-Apr-14 21:24
Alain DOS REIS4-Apr-14 21:24 
GeneralRe: Dose not released allocated memory from clr Pin
Yet Another XCoder15-May-14 6:53
Yet Another XCoder15-May-14 6:53 
QuestionCalling managed code from native code Pin
Member 1045308312-Dec-13 6:57
Member 1045308312-Dec-13 6:57 
AnswerRe: Calling managed code from native code Pin
Alain DOS REIS1-Apr-14 11:15
Alain DOS REIS1-Apr-14 11:15 
SuggestionError: Could not load file or assembly. Pin
peto224227-Oct-11 3:51
peto224227-Oct-11 3:51 
GeneralRe: Error: Could not load file or assembly. Pin
lazy30317-Nov-12 9:38
lazy30317-Nov-12 9:38 
AnswerRe: Error: Could not load file or assembly. Pin
Alain DOS REIS1-Apr-14 10:56
Alain DOS REIS1-Apr-14 10:56 
GeneralMy vote of 5 Pin
LaxmikantYadav31-Jan-11 19:26
LaxmikantYadav31-Jan-11 19:26 
GeneralMarshaling strings Pin
TobiasP28-Apr-09 1:41
TobiasP28-Apr-09 1:41 
GeneralReturning managed string to unmanged exported function Pin
shadowlocke24-Apr-09 3:37
shadowlocke24-Apr-09 3:37 
GeneralKudos! Pin
shadowlocke23-Apr-09 11:15
shadowlocke23-Apr-09 11:15 
General#define NATIVEDLL_API __declspec(dllexport) Pin
Dmitri Nеstеruk6-Apr-09 4:13
Dmitri Nеstеruk6-Apr-09 4:13 

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.