Interop 101 - Part 1

It's funny how often the people within our team (myself included) take certain things for granted. We have provided a great way to bridge the gap between native and managed code with C++/CLI yet I am continually surprised by how little information has been successfully conveyed. I posted slides from the talk I gave last month on this topic, however it lacks the most basic examples. To this effect, I intend to write up a few posts with some simple samples to boil down the basic concepts behind what we have dubbed C++ Interop.

 

Let's start with "legacy" native Win32 code. The following piece of code defines a simple type that is exported from a dll.

 

   // HelloWorld.h

   #pragma once

   class __declspec(dllexport) HelloWorld

   {

   public:

      HelloWorld();

      ~HelloWorld();

      void SayThis(wchar_t *phrase);

   };

 

The implementation of the SayThis method just brings up a message box as follows:

 

   void HelloWorld::SayThis(wchar_t *phrase)

   {

      MessageBox(NULL, phrase, L"Hello World Says", MB_OK);

   }

 

Once this code is compiled into a dll, our goal is to instantiate HelloWorld and invoke its one and only method. A traditional native client would do it by including the header we defined above as follows:

 

   #include "..\interop101\helloworld.h"

   int wmain()

   {

      HelloWorld hw;

      hw.SayThis(L"I'm a native client");

      return 0;

   }

 

Alright, now that I've reminded what native code looks like, we can talk interop. In this little educational series, I intend to demonstrate 4 major scenarios.

  1. Client in C++/CLI calls the native code
  2. Client in C# calls the native code using its built-in interop functionality (a.k.a. P/Invoke)
  3. Client in C# calls the native code via a COM interface
  4. Client in C# calls the native code via a C++/CLI wrapper

Today let's look at the first and simplest scenario: using C++/CLI.

 

   #include "..\interop101\helloworld.h"

   using namespace System;

   int main(array<System::String ^> ^args)

   {

      HelloWorld hw;

      hw.SayThis(L"I'm a managed C++ client");

      return 0;

   }

 

Hmmm… Looks virtually identical… This what we refer to when we talk about IJW or "It Just Works". In other words, all we had to do here was throw the /clr switch on the original code and the compiler generated MSIL instead of x86 assembly. If we delve into the MSIL, we see that transitions to native code (there are three in this case, can you spot them?) are automagically handled by the compiler.

 

.method assembly static int32 main(string[] args) cil managed

{

  // Code size 51 (0x33)

  .maxstack 2

  .locals ([0] int32 V_0,

     [1] int32 V_1,

           [2] valuetype HelloWorld hw)

  IL_0000: ldc.i4.0

  IL_0001: stloc.0

  IL_0002: ldloca.s hw

  IL_0004: call valuetype HelloWorld* modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) 'HelloWorld.{ctor}'(valuetype HelloWorld* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst))

  IL_0009: pop

  .try

  {

    IL_000a: ldloca.s hw

    IL_000c: ldsflda valuetype '<CppImplementationDetails>'.$ArrayType$$$BY0BJ@$$CB_W modopt([mscorlib]System.Runtime.CompilerServices.IsConst) '?A0x783d98d5.unnamed-global-0'

    IL_0011: call void modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) HelloWorld.SayThis(valuetype HelloWorld* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst),

                                                                                                                    char*)

    IL_0016: ldc.i4.0

    IL_0017: stloc.1

    IL_0018: leave.s IL_0028

  } // end .try

  fault

  {

    IL_001a: ldftn void modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) 'HelloWorld.{dtor}'(valuetype HelloWorld* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst))

    IL_0020: ldloca.s hw

    IL_0022: call void ___CxxCallUnwindDtor(method void *(void*),

                                                   void*)

    IL_0027: endfinally

  } // end handler

  IL_0028: ldloca.s hw

  IL_002a: call void modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) 'HelloWorld.{dtor}'(valuetype HelloWorld* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst))

  IL_002f: ldloc.1

  IL_0030: stloc.0

  IL_0031: ldloc.0

  IL_0032: ret

} // end of method 'Global Functions'::main

 

Voila. Short and sweet this time. Next example will be far more useful for C# clients :)