Interop: A Look at Managed C++

by Jason Clark of Wintellect

There are two forms of interop built into the .NET Framework Common Language Runtime. These are Platform Invoke and COM Interop. The purpose of these interop features is to allow your managed .NET code to interface with legacy unmanaged code. Platform Invoke, or P/Invoke, is a low-level means of calling into system functions, such as DLL entry point functions. COM Interop is built on top of P/Invoke, and manages higher-level details related to COM, such as COM object lifetime, method signature translation, etc. It is also possible for language compilers to offer their own interop features built on top of P/Invoke. Microsoft's Managed C++ compiler does this.

IL and C++

We will look at Managed C++ or MC++ more in a bit, but first let's talk about managed code in general. Managed code is code that runs on the Common Language Runtime (CLR). Managed code is a combination of Intermediate Language and Metadata. Metadata describes managed code in detail, so that runtime features such as garbage collection and type safety are enforceable by the system. Intermediate Language or IL is a .NET p-code. IL makes up the logic of managed methods, and is Just-in-Time (JIT) compiled into native machine language when the code is executed. IL can access managed primitive types such as System.Int32, as well as more high-level class library types such as System.Form.

Even though IL is managed, by nature, it does include instructions for accessing memory directly through pointers. This ability, in conjunction with P/Invoke features of the CLR, is what makes MC++ possible.

Managed C++ is regular C++, but with the ability to access features of the Common Language Runtime. In brief, the MC++ compiler lets you link-together modules that are compiled into x86 machine language, with modules that are compiled down to IL and metadata. In fact, using a #pragma managed, and #pragma unmanaged, you can coerce the compiler into producing both x86 and IL from functions defined in the same source code module. The only required step is to include the /CLR switch when building your module, and MC++ will produce IL from your C++ logic!

This ability to build C++ logic down to IL and metadata is the foundation of the features of managed C++, but it alone doesn't really affect much. All of the memory allocation and API calls are still unmanaged, they are just happening through IL, that's all. However, once you are compiling to IL, rather than directly to machine language, it is a small leap to begin accessing features of the .NET Framework. This is where the benefits begin to show.

In keeping with the Microsoft tradition of extending C++, MC++ extends the language with a number of extended keywords that start with two underbars, such as __gc or __box. These extended keywords are called Managed Extensions, and they provide access to features of managed code, such as garbage collection and use of managed types.

MC++ and Interop

Personally, I like C# when writing managed code. Even when doing interop, C# is fine, up to a point. The problem with C# and interop, however, is that C# really only provides the interop features of the CLR. It, as a language, does not have any built-in interop knowledge of its own. The bottom line is that interop with C# puts a fair amount of work on the developer. Additionally, there are certain things that C# just can't do at all. Where C# let's you call DLL functions and use certain COM objects, Managed C++ continues to know everything that C++ ever knew about unmanaged code! MC++ just can't be beat in terms of interop flexibility.

Here are some things that you can do from Managed C++:

  • You can read header files and call unmanaged functions directly without having to explicitly design extern P/Invoke methods. In contrast, C# knows nothing about header files, and makes you define extern methods that match the signatures of unmanaged methods. This can become tedious when you consider the many #define and typedef'd values that are passed to many unmanaged API functions.


  • You can use #import, DEFINE_GUID, CoCreateInstanceEx, etc. to access COM objects directly. You manage lifetime, etc. just like you always did with C++, rather than let the CLR do this.
    In contrast, C# only lets you access COM through the COM interop features of the CLR. From C# you are likely to find many COM objects largely unusable, because of this.


  • You can design a single assembly DLL that exposes any mix of COM objects, exported DLL functions, and public managed types. Wow!
    In contrast, C# can't export DLL functions at all.

With all of this power, why doesn't everyone use MC++ for all their managed needs? There are two reasons for this: the code is ugly, and you can't define verifiable assemblies using Managed C++. And did I mention that the code is ugly? Bottom line is that version one of Managed C++ is great for migration and cool for interop, but its practical uses start dropping off quickly from there. Microsoft will eventually release a version of Managed C++ that is of more general use.

Managed DLL Export Sample

Ok, so for the rest of this article, let's look at a code example that uses MC++ to implement one of the more unlikely interop scenarios. This code exposes an arbitrary managed type to the unmanaged world as a flat handle-style DLL API.

The code in Figure 2 is the complete source for a DLL that exports four functions that wrap the managed class library type System.Random. The code in Figure 1 is the supporting header file.

#ifndef NETWRAPPER_LINKAGE
#define NETWRAPPER_LINKAGE __declspec(dllimport)
#endif

#define HRAND int
extern "C"{
   NETWRAPPER_LINKAGE HRAND RandomCreate();
   NETWRAPPER_LINKAGE int RandomNext(HRAND handle, int maxValue);
   NETWRAPPER_LINKAGE
      void RandomNextBytes(HRAND handle, PBYTE buffer, DWORD size);
   NETWRAPPER_LINKAGE void RandomClose(HRAND handle);
}

Figure 1 NetWrapper.h

To the outside world, this DLL looks as though it is just a regular unmanaged DLL with some exported functions. But on the inside is a mix of managed and unmanaged code! If an unmanaged application loads the DLL and uses the functions, the CLR is automatically loaded into the process to provide runtime support for the managed portions of the DLL.

// Include windows.h
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

// .NET Framework goo
#using <mscorlib.dll>
using namespace System;
using namespace System::Collections;

// DLL Project specific goo
#define NETWRAPPER_LINKAGE __declspec(dllexport)
#include "NetWrapper.h"

// Just one of many ways to keep track of managed objects
__gc class ManagedObjects{
public:
   static Hashtable* table;
};

// Good ol' DllMain()
BOOL APIENTRY
   DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
{
   Hashtable* pTable = NULL; // temp reference

   if(dwReason == DLL_PROCESS_ATTACH){
      pTable = new Hashtable (); // Create a Hashtable on DLL load

      // Get a thread synchronized wrapper around the object
      ManagedObjects::table = Hashtable::Synchronized(pTable);
   }
   return TRUE;
}

// Helper function to translate an HRAND into an object reference
inline Random* GetRandomObj(HRAND handle){
   Random* obj = dynamic_cast<Random*>
      (ManagedObjects::table->get_Item(__box (handle)));
   return obj;
}

// Exported DLL Functions
extern "C"{

// Create a Random object
__declspec(dllexport) HRAND RandomCreate(){
   Random* rand = new Random (); // Managed object

   // Get the hashcode that we will use as a "handle"
   int handle = rand->GetHashCode();

   // Add the object to the hashtable
   ManagedObjects::table->Add(__box (handle), rand);
   return (HRAND)handle; // return "handle"
}

// Wrapper around the Random::Next(int maxValue) method
__declspec(dllexport) int RandomNext(HRAND handle, int maxValue){
   Random* rand = GetRandomObj(handle); // Get the object
   return rand->Next(maxValue); // call managed method
}

__declspec(dllexport)
   void RandomNextBytes(HRAND handle, PBYTE buffer, DWORD size){
   Random* rand = GetRandomObj(handle); // Get object

   // Create managed array of Byte's
   Byte tempBytes __gc[] = new Byte __gc [size];
   rand->NextBytes(tempBytes); // Call managed method to fill array

   // Pin managed array and copy memory
   BYTE __pin *pinBuffer = &(tempBytes[0]);
   CopyMemory(buffer, pinBuffer, size);
   pinBuffer = NULL; // null out pinned reference
}

__declspec(dllexport) void RandomClose(HRAND handle){
   // Unreference the managed object so it can be garbage collected
   ManagedObjects::table->Remove(__box (handle));
}

}

Figure 2 NetWrapper.cpp

Shortly I will also show you a simple unmanaged application that uses this DLL. But first let's run down some of the highlights of the code in Figure 2.

  • I marked managed keywords in red, and references to managed types in green.


  • You will notice that much of the source is just your regular unmanaged DLL stuff. It has DllMain() and some __declspec(dllexport) functions.


  • But in DllMain() on process attach, I allocate a managed object of type System.Collections.Hashtable. Notice that I have marked the new keyword there in red. This is because the new operator in C++ becomes a managed new operator if the type you are using is managed. This means that the allocation is on the managed heap, and the object does not need to be deleted, because it will be garbage collected by the CLR.


  • But why the Hashtable in the first place? The answer is that I am returning, to the unmanaged caller, a "handle" to a managed object. I can't use a pointer to do this, because the garbage collector moves managed objects around in memory. So I am using a managed Hashtable object to keep track of the managed objects. I return the key to the unmanaged caller as the "handle". This could have been handled (no pun intended) in a variety of ways.


  • Another comment about DllMain(): did you notice that the pTable variable is defined as Hashtable*? Is this a pointer or a managed reference? It is a managed reference, and the way that you know this is that it is referring to a managed type. The garbage collector will keep track of this local variable, and if a collection occurs it will fix-up the pointer in the reference so that it refers to the new location of the object.


  • I defined a helper function called GetRandomObj() that converts one of the HRAND handle values into a reference to a System.Random object stored in the Hashtable. Notice that the dynamic_cast became a managed cast automatically because I am casting to a managed type.


  • Oh yes, and I can't forget boxing. If I want to treat an integer like an object, then it needs to be boxed. That is what the __box keyword is doing in the GetRandomObj() helper function. It is boxing an int and returning a System.Object reference. If you are unfamiliar with the term boxing, it is a CLR feature that allows your code to automatically copy primitive value type instances, such as integer values, into a heap-allocated object-derived object.


  • Ok, so now let's look at one of the exported functions: RandomCreate(). This method instantiates a managed Random object, and then adds it to the global Hashtable. Finally, it returns the key to the table to the unmanaged caller as a sort of handle.


  • The RandomNext() exported function is little more than a pass through to the Next() method of the Random type. However, it does accept an HRAND handle, and get a managed object from the handle, before passing the call through.


  • The RandomNextBytes() exported function is a little more complex. It fills a buffer with an arbitrary number of random bytes. To do this, it needs to allocate a managed array of bytes, and then call the managed Random.NextBytes() method that does the actual work. Then, it has to copy the resulting bytes over to the unmanaged buffer. MC++ provides a lot of flexibility, but the compromise is that sometimes you have to marshal parameters and such yourself, and that is what is going on here.


  • The RandomClose() exported function ]K"sn't really close or cleanup anything. All it does is remove the reference to the object from the Hashtable. Once the managed object is no longer referenced, it is now subject for garbage collection by the CLR. If this DLL was exposing some other managed type, it might want to call a cleanup method on the object itself, such as Close() or Dispose().


So there it is. This is a pretty short example, but it does include a fair amount of complexity. By the way, this DLL could have just as effectively flattened out any arbitrary custom C# or VB.Net class. I just used System.Random to keep things simple. Either way, it is a cool example of interop that just isn't possible with C# or VB.Net alone.

// Common includes
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>

// Include and link for custom DLL functions
#include "NetWrapper.h"

void main()
{
   // Get "handle" to managed object
   HRAND rand = RandomCreate();

   printf("*** 10 Random Integers ***\n");
   // Call managed method on object 10 times
    for(int index=0; index<10; index++){
      int num = RandomNext(rand, 10)+1;
      printf("%d\n", num);
   }

   printf("\n\n");
   printf("*** 10 Random Bytes ***\n");
   // Call one managed method to get block of bytes
   BYTE bytes[10];
   int size = sizeof(bytes)
   RandomNextBytes(rand, bytes, size);
   // Print the byte's values
   for(int index=0; index<size; index++){
      int num = bytes[index];
   printf("%d\n", num);
   }

   // Make managed object subject to garbage
   // collection
   RandomClose(rand);
}

Figure 3 Win32App.cpp

The preceding code, in Figure 3, is an example of an unmanaged C++ application that uses the DLL like any other flat handle-based API.

Summing Up

My experience with Managed C++ has not moved me to abandon C# for my purely managed projects. However, in terms of interop flexibility, MC++ is the current king. I hope this example has given you an idea of some of the power that is there for those who need it.

Have fun!

Download Source Code

Download source code: MCPP.zip

About the Author...

Jason Clark has been banging code since he fell in love with computers way back in sixth grade. Since the early nineties, Jason professional life has been devoted to Windows development. His most recent full-time employment was with Microsoft, where he wrote security protocols for the Windows operating systems.

Jason now does software consulting and writes about a variety of topics ranging from developing secure software to writing software that runs on Microsoft's new .NET platform. Jason coauthored Programming Server-Side Applications for Microsoft Windows 2000, and he writes articles for Dr. Dobbs Journal, MSDN Magazine (formerly MSJ), Windows Developers Journal, and other leading developer magazines. Jason's commercial software credits include work he has performed for Microsoft, IBM, Sony, HP, and other companies, and involve everything from writing printer drivers to helping develop the Windows 2000 and "Whistler" operating systems.

# # #



Comments

  • Creating from scratch on vs.net 2003

    Posted by Legacy on 07/01/2003 12:00am

    Originally posted by: Allen


    great article, I really appreciate the info. I've run into a problem where I need to do something very similar to support some legacy applications where I work.

    One problem I'm having though is how to go from scratch to your type of project? I tried creating a managed dll and droping your code into the source files and removing everything else (much like your sample), however I get

    antest.obj : error LNK2001: unresolved external symbol "void * __cdecl memcpy(void *,void const *,unsigned int)" (?memcpy@@$$J0YAPAXPAXPBXI@Z)

    every time. I went back and tried adding/changing everything to be identical to your project but nothing seemed to work. So could you please provide a roadmap to go from 0 to your project in vs.net?

    thanks!

    Reply
  • i not looking for NetWrapper.lib ,where is it?

    Posted by Legacy on 02/20/2003 12:00am

    Originally posted by: jang kyoungchip

    hello

    That's complied , but error.
    error message's NetWrapper.lib not find

    how can do that???

    can you help me !!!!


    thank you..

    from jang kyoungchip.

    Reply
  • ruudkrank@hotmail.com

    Posted by Legacy on 12/11/2002 12:00am

    Originally posted by: RUUD KRANK

    Nice.
    There hasn't been much emphasis on MC++ so far.
    ruud

    Reply
  • Thanks...

    Posted by Legacy on 12/08/2002 12:00am

    Originally posted by: Paul Selormey

    Currently porting an application to .NET using MC++. Your article provides some additional insights and tricks.

    Best regards,
    Paul.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The proliferation of cloud computing options has begun to change the way storage is thought about, procured, and used. IT managers and departments need to think through how cloud options might fit into and complement their onsite data infrastructures. This white paper explains cloud storage and backup, providing advice about the tools and best practices for its implementation and use. Read this white paper for some useful takeaways about how to take advantage of cloud storage for high availability, backup and …

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds