Click here to Skip to main content
13,903,802 members
Click here to Skip to main content
Add your own
alternative version

Stats

9.3K views
12 downloads
40 bookmarked
Posted 11 Jul 2017
Licenced CPOL

Interoping .NET and C++ through COM

, 11 Jul 2017
Rate this:
Please Sign up or sign in to vote.
Create COM visible types in C# and consume them from C++

It is not uncommon that we have to put to work together native and .NET managed components. When you have to consume a managed component in native code there are basically two options: through a mixed-mode component written in C++/CLI or through COM. This article will discuss the later and walk you through some of the key parts to help you understand the mechanism and get going with it. In this article you will learn to:

  • Write COM visible interface and classes in C# and expose them through COM
  • Import a type library in C++
  • Use COM smart pointers to consume the COM components
  • Understand the various type library files created in the process
  • Understand the marshalling of types between C# and C++
  • Handle marshaled arrays
  • Handle marshaled interfaces

Creating a .NET in-proc COM server

The first thing to start with is creating a .NET class library project that will represent an in-proc COM server. To this library we will add, for the start, an interface called ITest and a class that implements it called Test. To a bare minimum these will look as below:

namespace ManagedLib
{
   [Guid("D3CE54A2-9C8D-4EA0-AB31-2A97970F469A")]
   [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
   [ComVisible(true)]
   public interface ITest
   {
      [DispId(1)]
      void TestBool(bool b);
      
      [DispId(4)]
      void TestSignedInteger(sbyte b, short s, int i, long l);
   }
   
   [Guid("A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04")]
   [ClassInterface(ClassInterfaceType.None)]
   [ComVisible(true)]
   [ProgId("ManagedLib.Test")]
   public class Test : ITest
   {
      public void TestBool(bool b)
      {
         Console.WriteLine($"bool:   {b}");
      }
   
      public void TestSignedInteger(sbyte b, short s, int i, long l)
      {
         Console.WriteLine($"sbyte:  {b}");
         Console.WriteLine($"short:  {s}");
         Console.WriteLine($"int:    {i}");
         Console.WriteLine($"long:   {l}");
      }   
   }   
}

There is nothing special in defining the interface and implementing the class except for the several attributes used on the interface/class and methods that control how these entities are exposed to COM.

GuidAttributeSpecifies the GUID that defines the interface or class uniqueue identifier.
ComVisibleAttributeControls the accessibility of a type or member to COM. By setting the visibility parameter to true we indicate that the type or member is visible to COM.
InterfaceTypeAttributeSpecifies the type of COM interface a managed interface is when exposed to COM. The options specified in this sample is InterfaceIsIDispatch, which means the interface is dispinterface. This enables late binding only, with the methods and properties of the interface not part of the VTBL of the interface, and accessible through IDispatch::Invoke() only.
ClassInterfaceAttributeSpecifies what kind of interface should be generated for the COM class. The options None specified above indicates that the class provides late-bind access through the IDispatch interface and no class interface is generated for the class.
DispIdAttributeSpecifies the COM dispatch ID for a method, property or field.
ProgIdAttributeAllows to specify a programmable ID, which is a human friendly name for the COM class and must be uniqueue within the system (just like the class ID).

The next thing to do is registering the class library for COM interop from Project properties > Build. This can be done manually with regasm.exe, but by checking this option in the project settings, Visual Studio will run this tool with the /tlb and /codebase options. regasm.exe does all the registration necessary for the COM library to work. With the /tlb obtion it also generates a Type Library file (.tlb) that contains definitions of the assembly types that are COM visible. With the /codebase option it performs the registration from your project directory and not from GAC.

Importing a type library in C++

A type library is a binary file that contains informations about COM interfaces, methods, and properties. This information is accessible to other applications at runtime. In VC++ it is possible to generate C++ classes based on this information and therefore provide early binding to the COM components. This is possible by using the #import directive.

#import "ManagedLib.tlb"

The general form is #import filename [attributes], where the filename can be a type library (.tlb, .olb, .dll), an executable, a library containing a type library resource (.ocx), the programmatic ID of a control in a type library, the library ID of a type library, or any other format that can be understood by LoadTypeLib. The attributes are optional and control the content of the resulting headers. For details about the way the import directive works check its MSDN documentation.

The result of importing a type library are two header files, with the same name as the type library file, but different extensions:

  • .TLH (Type Library Header) contains a header and a footer, forward references and typedefs, smart pointer declarations, typeinfo declarations, #include statement or the secondary header, and other parts.
  • .TLI (Type Library Implementation) contains implementation for the compiler generated member functions and properties.

The result of the above import directive for the C# code shown earlier are the following header files:

  • managedlib.tlh
    // Created by Microsoft (R) C/C++ Compiler Version 14.00.24215.1 (d73829de).
    //
    // c:\comdemo\nativeclient\debug\managedlib.tlh
    //
    // C++ source equivalent of Win32 type library ManagedLib.tlb
    // compiler-generated file created 07/04/17 at 19:04:17 - DO NOT EDIT!
    
    #pragma once
    #pragma pack(push, 8)
    
    #include <comdef.h>
    
    namespace ManagedLib {
    
    //
    // Forward references and typedefs
    //
    
    struct __declspec(uuid("56418cab-6e5e-41c7-b477-e3b5c250d879"))
    /* LIBID */ __ManagedLib;
    struct __declspec(uuid("d3ce54a2-9c8d-4ea0-ab31-2a97970f469a"))
    /* dispinterface */ ITest;
    struct /* coclass */ Test;
    
    //
    // Smart pointer typedef declarations
    //
    
    _COM_SMARTPTR_TYPEDEF(ITest, __uuidof(ITest));
    
    //
    // Type library items
    //
    
    struct __declspec(uuid("d3ce54a2-9c8d-4ea0-ab31-2a97970f469a"))
    ITest : IDispatch
    {
        //
        // Wrapper methods for error-handling
        //
    
        // Methods:
        HRESULT TestBool (
            VARIANT_BOOL b );
        HRESULT TestSignedInteger (
            char b,
            short s,
            long i,
            __int64 l );
    };
    
    struct __declspec(uuid("a7a5c4c9-f4da-4cd3-8d01-f7f42512ed04"))
    Test;
        // interface _Object
        // [ default ] dispinterface ITest
    
    //
    // Wrapper method implementations
    //
    
    #include "c:\comdemo\nativeclient\debug\managedlib.tli"
    
    } // namespace ManagedLib
    
    #pragma pack(pop)
  • managedlib.tli
    // Created by Microsoft (R) C/C++ Compiler Version 14.00.24215.1 (d73829de).
    //
    // c:\comdemo\nativeclient\debug\managedlib.tli
    //
    // Wrapper implementations for Win32 type library ManagedLib.tlb
    // compiler-generated file created 07/04/17 at 19:04:17 - DO NOT EDIT!
    
    #pragma once
    
    //
    // dispinterface ITest wrapper method implementations
    //
    
    inline HRESULT ITest::TestBool ( VARIANT_BOOL b ) {
        return _com_dispatch_method(this, 0x1, DISPATCH_METHOD, VT_EMPTY, NULL, 
            L"\x000b", b);
    }
    
    inline HRESULT ITest::TestSignedInteger ( char b, short s, long i, __int64 l ) {
        return _com_dispatch_method(this, 0x4, DISPATCH_METHOD, VT_EMPTY, NULL, 
            L"\x0011\x0002\x0003\x0014", b, s, i, l);
    }

From the .tlh header two things are of most importance in our case:

  • The declaration of a smart pointer in the form:
    _COM_SMARTPTR_TYPEDEF(ITest, __uuidof(ITest));
    _COM_SMARTPTR_TYPEDEF is a macro that expands to the following:
    typedef _com_ptr_t<_com_IIID<ITest, __uuidof(ITest)> > ITestPtr;  
    _com_ptr_t is a smart-pointer implementation that hides the call to CoCreateInstance() for creating a COM object, encapsulates interface pointers and eliminates the need to call AddRef(), Release(), and QueryInterface() functions.
  • The ITest class, that is a C++ class that emulates the ITest COM interface. ITestPtr is a smart pointer that should be used instead of ITest*.

On the other hand, the .tli header contains the implementation of all the COM interface methods. These use the _com_dispatch_method(), _com_dispatch_propget(), _com_dispatch_method(), that internally call IDispath::Invoke() and, possibly, other functions from comdef.h. The _com_dispatch_method() function that we can see in this example has the following signature:

HRESULT __cdecl
    _com_dispatch_method(IDispatch*, DISPID, WORD, VARTYPE, void*,
                         const wchar_t*, ...) ;

The parameters are as listed in the table below. The example considered is the function TestSignedInteger().

Parameter typeFrom exampleComments
IDispatch*thisPointer to an IDispatch interface.
DISPID0x4Dispatch identifier of the interface member.
WORDDISPATCH_METHODFlags describing the context of the Invoke() call.
VARTYPEVT_EMPTYType of the return value.
void*NULLPointer to the location where the result is to be stored, or NULL if no result is expected.
const wchar_t*L"\x0011\x0002\x0003\x0014"Pointer to an array of wide characters representing the type of each input parameter. Each value has a string representation of a hexadecimal value introduced with \x. For instance \x0011 is decimal 17, which is VT_UI1 (i.e. unsigned 8-bit integer) and 0x0002 is decinal 2 that is VT_I2 (i.e. signed 16-bit integer).
... (ellipsis)b, s, i, lA variable list of input parameters for the COM interface function.

Consuming the COM components from C++

With the helper code created by importing the type library it is relatively simple to consume the COM components from C++. What we have to do is:

  • Initialize the COM library for the current thread (and properly uninitializ it when no longer needed).
  • Create an smart pointer instance.
  • Instantiate the COM coclass through the COM pointer. For this we can either use the class ID (both in the form of a GUID or a string delimited with {}, such as L"{A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04}") or the programmatic ID.
  • Call the methods from the COM interface.
  • Properly handle possible COM errors propagated to the client wrapped in a _com_error exception.

The following example shows all these steps by instantiating the Test coclass and calling methods through the ITest COM interface.

#include <iostream>
#import "ManagedLib.tlb"

struct COMRuntime
{
   COMRuntime() { CoInitialize(NULL); }
   ~COMRuntime() { CoUninitialize(); }
};

int main()
{
   COMRuntime runtime;
   ManagedLib::ITestPtr ptr;
   //ptr.CreateInstance(L"{A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04}");
   ptr.CreateInstance(L"ManagedLib.Test");
   if (ptr != nullptr)
   {
      try
      {   
         ptr->TestBool(true);
         ptr->TestSignedInteger(CHAR_MAX, SHRT_MAX, INT_MAX, MAXLONGLONG);
      }
      catch (_com_error const & e)
      {
         std::wcout << (wchar_t*)e.ErrorMessage() << std::endl;
      }      
   }
   
   return 0;
}

Notice that the calls ptr.CreateInstance(L"{A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04}") and ptr.CreateInstance(L"ManagedLib.Test") are in this case equivalent.

The output from the program above is as follows:

bool:   True
sbyte:  127
short:  32767
int:    2147483647
long:   9223372036854775807

Mapping .NET and C++ types

The following table shows the C# types, with their equivalent .NET framework type, and the mapping to COM and C++ types.

C#.NET FrameworkSize in bitsCOM/C++Size in bitsVARENUM
boolSystem.Boolean8VARIANT_BOOL16VT_BOOL
charSystem.Char8unsigned short16VT_UI2
sbyteSystem.SByte8char8VT_UI1
byteSystem.Byte8unsigned char8VT_UI1
shortSystem.I1616short16VT_I2
ushortSystem.UInt1616unsigned short16VT_UI2
intSystem.Int3232long32VT_I4
uintSystem.UInt3232unsigned long32VT_UI4
longSystem.Int6464__int6464VT_I8
ulongSystem.UInt6464unsigned __int6464VT_UI8
floatSystem.Single32float32VT_R4
doubleSystem.Double64double64VT_R8
decimalSystem.Decimal128DECIMAL128VT_DECIMAL
stringSystem.String _bstr_t (BSTR) VT_BSTR
objectSystem.Object _variant_t (VARIANT) VT_VARIANT
 System.DateTime DATE VT_DATE
 System.Array SAFEARRAY VT_ARRAY

Marshaling of integer and floating point types is straight forward and should not require additional comments. However, there are several other built-in types that need to be discussed further:

  • The bool (System.Bool) C# type is not mapped to the C++ bool type, but instead to the Microsoft Automation specific type VARIANT_BOOL. This is actually a typedef for short, and therefore has 16 bits (unlike the .NET Boolean type that is represented on 8 bits). There are two typedefs for the possible values of a VARIANT_BOOL variable: VARIANT_TRUE (0xFFFF) and VARIANT_FALSE (0). This type is available in the wtypes.h header.
  • The char (System.Char) C# type is not mapped to the C++ char type, but instead to unsigned short. The reason for this is that characters in .NET represent 16-bit UNICODE characters, while in C++ char represents an 8-bit ANSI character.
  • The decimal (System.Decimal) C# char type does not have built-in C++ type equivalent. This type is marshaled as the Microsoft specific DECIMAL type, defined in wtypes.h.
    typedef struct tagDEC {
      USHORT wReserved;
      union {
        struct {
          BYTE scale;
          BYTE sign;
        };
        USHORT signscale;
      };
      ULONG  Hi32;
      union {
        struct {
          ULONG Lo32;
          ULONG Mid32;
        };
        ULONGLONG Lo64;
      };
    } DECIMAL;
    This is a compound type that stores an 96-bit unsigned integer value and a scale representing a power of 10. This is actually the number of digits to the right of the decimal point and can have a value between 0 an 28. For instance, the decimal 42.12345 is stored as integer 4212345 with a scale of 5.
    DECIMAL dm{0};
    dm.scale = 5;
    dm.Lo32 = 4212345;
  • The string (System.String) C# type is marshaled to _bstr_t (available in comutil.h), a COM utility class that is a wrapper for BSTR that manages the allocation and release of BSTRs and other functionalities. The BSTR type (also from the wtypes.h header) represents a pointer to a string a wide characters. However, the BSTR type is actually a composite type that consists of a prefixed length, the data string and two terminating null characters. The data string is represented by 16-bit UNICODE characters and may contain multiple embedded null characters. The length of the data string (which does not include the terminating null characters) is represented by a 32-bit integer appearing in memory immediately before the first character of the data string. BSTR is a pointer that points to the first character of the data string, and not the length. BSTRs are allocated with SysAllocString() and destroyed with SysFreeString().
    BSTR str = SysAllocString(L"sample");
    // use str
    SysFreeString(str);
    
    _bstr_t str(L"sample");
    // use str
  • The System.DateTime type is marshaled to the Microsoft specific DATE type (from wtypes.h), which is a typedef for double. The date information is represented by whole-number increments, starting with December 30, 1899 midnight as time zero. The time information is represented by the fraction of a day since the preceding midnight. For example, 3:00 P.M. on January 7, 1900 would be represented by the value 8.625. The integer part, 8, represents the numbers of days since the sbase date, and the fraction part, .625, is the is the part of the 24-hours day since midnight (15 hours / 24 hours = 0.625).
  • The object (System.Object) C# type is marshaled as the Microsoft specific _variant_t type. This is a wrapper class for VARIANT data type, available in the header comutil.h. VARIANT is a container for a union that can hold many types of data (hence the name), and _variant_t is a wrapper class that manages initialization, cleanup, resource allocation and deallocation.
  • The array data type is marshaled to the Microsoft specific SAFEARRAY type. This is basically a structure that describes a multi-dimentional array and has a pointer to the memory location where the actual data is stored. This will be further discussed later on.

Below are some snippets from the attached source code where you can find more and complete examples.

  • C# ITest interface members
    [DispId(1)] void TestBool(bool b);
    [DispId(2)] void TestChar(char c);
    [DispId(3)] void TestString(string s);
    [DispId(4)] void TestSignedInteger(sbyte b, short s, int i, long l);
    [DispId(5)] void TestUnsignedInteger(byte b, ushort s, uint i, ulong l);
    [DispId(6)] void TestReal(float f, double d);
    [DispId(7)] void TestDate(DateTime dt); 
    [DispId(8)] void TestDecimal(decimal d);
  • C# Test class member implementation
    public void TestBool(bool b)
    {
       Console.WriteLine($"bool:   {b}");
    }
    
    public void TestChar(char c)
    {
       Console.WriteLine($"char:   {c}");
    }
    
    public void TestDate(DateTime dt)
    {
       Console.WriteLine($"date:   {dt}");
    }
    
    public void TestSignedInteger(sbyte b, short s, int i, long l)
    {
       Console.WriteLine($"sbyte:  {b}");
       Console.WriteLine($"short:  {s}");
       Console.WriteLine($"int:    {i}");
       Console.WriteLine($"long:   {l}");
    }
    
    public void TestUnsignedInteger(byte b, ushort s, uint i, ulong l)
    {
       Console.WriteLine($"byte:   {b}");
       Console.WriteLine($"ushort: {s}");
       Console.WriteLine($"uint:   {i}");
       Console.WriteLine($"ulong:  {l}");
    }
    
    public void TestReal(float f, double d)
    {
       Console.WriteLine($"float:  {f}");
       Console.WriteLine($"double: {d}");
    }
    
    public void TestString(string s)
    {
       Console.WriteLine($"string: {s}");
    }
    
    public void TestDecimal(decimal d)
    {
       Console.WriteLine($"decimal:{d}");
    }      
  • C++ client code
    void TestInputParams(ManagedLib::ITestPtr ptr)
    {
       std::cout << "test input parameters..." << std::endl;
    
       ptr->TestBool(true);
       ptr->TestChar('A');
       ptr->TestString(L"test");
       ptr->TestSignedInteger(CHAR_MAX, SHRT_MAX, INT_MAX, MAXLONGLONG);
       ptr->TestUnsignedInteger(UCHAR_MAX, USHRT_MAX, UINT_MAX, MAXULONGLONG);
       ptr->TestReal(FLT_MAX, DBL_MAX);
       DECIMAL dm{0};
       dm.scale = 5;
       dm.Lo32 = 4212345;
       ptr->TestDecimal(dm);
       COleDateTime dt = COleDateTime::GetCurrentTime();
       ptr->TestDate(dt.m_dt);
    }
  • Program output
    test input parameters...
    bool:   True
    char:   A
    string: test
    sbyte:  127
    short:  32767
    int:    2147483647
    long:   9223372036854775807
    byte:   255
    ushort: 65535
    uint:   4294967295
    ulong:  18446744073709551615
    float:  3.402823E+38
    double: 1.79769313486232E+308
    decimal:42.12345
    date:   2017-07-07 09:55:52

In C#, function parameters can be declared with the ref or out modifier. ref indicates that a value is already set and the function can read and write it. On the other hand, out indicates that the value is not set and the function must do so before returning. When used on COM interfaces, these two are marshaled identically.

Let’s consider the following methods from the ITest interface.

[DispId(52)]
void TestRefParams(ref int a, ref double d);

[DispId(53)]
void TestOutParams(out int a, out double d);

The actual implementation is not that important. However, these two functions get identical COM interface methods. The C++ implementation of the wrapper functions from the .tli file is shown below.

inline HRESULT ITest::TestRefParams ( long * a, double * d ) {
    return _com_dispatch_method(this, 0x34, DISPATCH_METHOD, VT_EMPTY, NULL, 
        L"\x4003\x4005", a, d);
}

inline HRESULT ITest::TestOutParams ( long * a, double * d ) {
    return _com_dispatch_method(this, 0x35, DISPATCH_METHOD, VT_EMPTY, NULL, 
        L"\x4003\x4005", a, d);
}

Both ref and out params become pointers. \x4003 means VT_BYREF|VT_I4 and \x4005 means VT_BYREF|VT_DOUBLE. Both ref int and out int are marshaled to long*, and both ref double and out double are marshaled to double*.

long i;
double d;
ptr->TestRefParams(&i, &d);
ptr->TestOutParams(&i, &d);

Handling arrays

C# arrays are marshaled to SAFEARRAY. As mentioned earlier, SAFEARRAY is not a container class, but rather a descriptor on an array, that contains information about the dimensions of the array, the bounds on each dimention and a pointer to the actual data.

typedef struct tagSAFEARRAY
{
   USHORT cDims;
   USHORT fFeatures;
   ULONG cbElements;
   ULONG cLocks;
   PVOID pvData;
   SAFEARRAYBOUND rgsabound[ 1 ];
} SAFEARRAY;

Input and returned arrays are marshaled as SAFEARRAY* and ref and out arrays are marshaled as SAFEARRAY**.

When passing an array to a COM function you have to:

  • Define the dimensions of the array with the number of elements and base index (for each dimension) using SAFEARRAYBOUND.
  • Create the array with SafeArrayCreate(), specifying the type of elements and the dimensions.
  • Put elements in the array with SafeArrayPutElement().
  • After the array is no longer needed destroy it with SafeArrayDestroy().

When getting an array from a COM function (either as a return value or output parameter) you have to:

 

  • Define the dimension of the array using SAFEARRAYBOUND, without having to specify the number of elements and base index.
  • Create the array with SafeArrayCreate(), specifying the type of elements and the dimensions.
  • After receiving the array, you could check the type of the elements to make sure it is what you expect.
  • Get the bounds of the elements on each dimention with SafeArrayGetLBound() and SafeArrayGetUBound().
  • Iterate through the elements retrieving them with SafeArrayGetElement().
  • After the array is no longer needed destroy it with SafeArrayDestroy().

To show how it works, let’s consider the following ITest interface methods:

[DispId(27)] void TestIntArray(int[] i);
[DispId(36)] int[] TestIntArrayReturn();
[DispId(45)] void TestIntOutArray(out int[] o);

The implementation in the Test class is as follows:

public void TestIntArray(int[] i)
{
   Console.Write("int arr:    ");
   foreach (var e in i) Console.Write($"{e} ");
   Console.WriteLine();
}

public int[] TestIntArrayReturn()
{
   return new int[] { 1, 2, 3 };
}

public void TestIntOutArray(out int[] o)
{
   o = new int[] { 1, 2, 3 };
}

The wrapper functions in the C++ ITest class are declared as follows:

HRESULT TestIntArray (SAFEARRAY * i);
SAFEARRAY * TestIntArrayReturn ( );
HRESULT TestIntOutArray (SAFEARRAY ** o);

Handling the input an output arrays using SAFEARRAY for these functions can be done in the following manner:

  • Passing the array as an input parameter
    SAFEARRAYBOUND sab;                              // define an array bound for one dimension with
    sab.cElements = 3;                               // 3 elements
    sab.lLbound = 0;                                 // starting from index 0
    
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab); // create a one dimenional array of 32-bit signed integers
    
    for(LONG i = 0; i < 3; ++i)
    {
      int value = i + 1;
      SafeArrayPutElement(sa, &i, &value);           // put elements in the array {1, 2, 3}
    }
    
    ptr->TestIntArray(sa);                           // use the array
    
    SafeArrayDestroy(sa);                            // destroy the array
  • Receiving an array as a return value:
    SAFEARRAY* sa = ptr->TestIntArrayReturn();       // get the array
    VARTYPE vt;
    SafeArrayGetVartype(sa, &vt);                    // check the type of its elements
    
    if (vt == VT_I4)                                 // make sure it's the expected type
    {
       LONG begin{ 0 };
       LONG end{ 0 };
       SafeArrayGetLBound(sa, 1, &begin);            // fetch the lower bound of the array index
       SafeArrayGetUBound(sa, 1, &end);              // fetch the upper bound of the array index
       for (LONG i = begin; i <= end; ++i)
       {
          int value;
          SafeArrayGetElement(sa, &i, &v);           // fetch the elements from the array
          
          // ...                                     // use value
       }
    }
    
    SafeArrayDestroy(sa);                            // destroy the array
  • Passing an array as an output parameter
    SAFEARRAYBOUND sab{ 0, 0 };                      // define unspecified array bounds
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab); // create an one dimentional array of 32-bit signed integers with defined bounds
    
    ptr->TestIntOutArray(&sa);                       // retrieve the array as an output parameter
    
    LONG begin{ 0 };
    LONG end{ 0 };
    SafeArrayGetLBound(sa, 1, &begin);               // fetch the lower bound of the array index
    SafeArrayGetUBound(sa, 1, &end);                 // fetch the upper bound of the array index
    for (LONG i = begin; i <= end; ++i)
    {
       int value;
       SafeArrayGetElement(sa, &i, &value);          // fetch the elements from the array
       
       // ...                                        // use value
    }
    
    SafeArrayDestroy(sa);                            // destroy the array

The later two examples are very similar. However, in the case the array is an output parameter (marshaled from a ref or out function parameter in C#), the array must be first created on the caller side. However, you only need to specify the type of the elements and the number of dimensions, but not the dimension bounds (the number of elements and the base index). These will be filled-in in the SAFEARRAY when the array is marshaled back from managed to native.

All the examples show so far used single dimentional arrays. However, SAFEARRAY can be used for multi-dimentional arrays. The only thing that is different is that you have to specify the bounds for each dimension when you create the array, or read the bounds when you receive the array.

Again, to exemplify, let us consider the following ITest functions that handle two-dimentional arrays of 32-bit signed integers:

[DispId(42)] void TestInt2DArray(int [,] arr);
[DispId(43)] int[,] TestInt2DArrayReturn();

Their implementation in the Test class is shown below:

public void TestInt2DArray(int[,] arr)
{
   for(int i=0; i < arr.GetLength(0); ++i)
   {
      for(int j = 0; j < arr.GetLength(1); ++j)
      {
         Console.Write($"{arr[i,j]} ");
      }
      Console.WriteLine();
   }
}

public int[,] TestInt2DArrayReturn()
{
   return new int[3, 2] { { 1,2}, {3,4}, { 5,6} };
}

The signature of the wrapper functions in the C++ ITest class is no different than of the functions shown earlier that worked with one-dimentional arrays.

HRESULT TestInt2DArray (SAFEARRAY * arr);
SAFEARRAY * TestInt2DArrayReturn ( );

The way these functions are consumed from C++ and how the SAFEARRAYs are handled is listed below:

  • 2d input array
    SAFEARRAYBOUND sab[2];
    sab[0].cElements = 3;
    sab[0].lLbound = 0;
    
    sab[1].cElements = 2;
    sab[1].lLbound = 0;
    
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 2, sab);
    for (int i = 0; i < 3; i++)
    {
      for (int j = 0; j < 2; j++)
      {
         LONG index[2] = { i,j };
         int value = 1 + i * 2 + j;
         SafeArrayPutElement(sa, index, &value);
      }
    }
    
    ptr->TestInt2DArray(sa);
    
    SafeArrayDestroy(sa);
  • 2d returned array
    SAFEARRAY* sa = ptr->TestInt2DArrayReturn();
    VARTYPE vt;
    SafeArrayGetVartype(sa, &vt);
    
    if (vt == VT_I4)
    {
       LONG begin[2]{ 0 };
       LONG end[2]{ 0 };
    
       SafeArrayGetLBound(sa, 1, &begin[0]);
       SafeArrayGetLBound(sa, 2, &begin[1]);
       SafeArrayGetUBound(sa, 1, &end[0]);
       SafeArrayGetUBound(sa, 2, &end[1]);
    
       for (LONG i = begin[0]; i <= end[0]; ++i)
       {
          for (LONG j = begin[1]; j <= end[1]; ++j)
          {
             LONG index[2]{ i,j };
             int value;
             SafeArrayGetElement(sa, index, &value);
          }
       }
    }
    
    SafeArrayDestroy(sa);

Handling objects

In .NET, System.Object (object in C#) is the base class for all built-in and user defined types. Every reference or value type is implicitly derived from this type. The type object can be used to pass any object, whether of a reference or value type. When used in COM visible interfaces, the type is marshaled as VARIANT, which is a structure that contains a union that can hold values of many built-in types, such as integers, floating-point, decimal, date, string, arrays, etc. The code that is generated when importing a type library uses however a wrapper classed instead of VARIANT, called _variant_t. This handles the initialization and clean-up of a VARIANT variable and provides useful other functionalities.

struct tagVARIANT
    {
    union 
        {
        struct __tagVARIANT
            {
            VARTYPE vt;
            WORD wReserved1;
            WORD wReserved2;
            WORD wReserved3;
            union 
                {
                LONGLONG llVal;
                LONG lVal;
                BYTE bVal;
                SHORT iVal;
                FLOAT fltVal;
                DOUBLE dblVal;
                VARIANT_BOOL boolVal;
                _VARIANT_BOOL bool;
                SCODE scode;
                CY cyVal;
                DATE date;
                BSTR bstrVal;
                IUnknown *punkVal;
                IDispatch *pdispVal;
                SAFEARRAY *parray;
                BYTE *pbVal;
                SHORT *piVal;
                LONG *plVal;
                LONGLONG *pllVal;
                FLOAT *pfltVal;
                DOUBLE *pdblVal;
                VARIANT_BOOL *pboolVal;
                _VARIANT_BOOL *pbool;
                SCODE *pscode;
                CY *pcyVal;
                DATE *pdate;
                BSTR *pbstrVal;
                IUnknown **ppunkVal;
                IDispatch **ppdispVal;
                SAFEARRAY **pparray;
                VARIANT *pvarVal;
                PVOID byref;
                CHAR cVal;
                USHORT uiVal;
                ULONG ulVal;
                ULONGLONG ullVal;
                INT intVal;
                UINT uintVal;
                DECIMAL *pdecVal;
                CHAR *pcVal;
                USHORT *puiVal;
                ULONG *pulVal;
                ULONGLONG *pullVal;
                INT *pintVal;
                UINT *puintVal;
                struct __tagBRECORD
                    {
                    PVOID pvRecord;
                    IRecordInfo *pRecInfo;
                    } 	__VARIANT_NAME_4;
                } 	__VARIANT_NAME_3;
            } 	__VARIANT_NAME_2;
        DECIMAL decVal;
        } 	__VARIANT_NAME_1;
    } ;
typedef VARIANT *LPVARIANT;

To show how this works we will consider a function that takes an object parameter and one that returns an object (the actual implementation returning a string as an object).

[DispId(50)] void TestObject(object o);
[DispId(51)] object TestObjectReturn();

public void TestObject(object o)
{
   Console.WriteLine($"object: {o}");
}

public object TestObjectReturn()
{
   return "demo";
}

The C++ methods in the wrapper ITest class look like this:

HRESULT TestObject (const _variant_t & o);
_variant_t TestObjectReturn ();

The client code in the example below passes a string to the TestObject() function, and also handles strings returned from the TestObjectReturn() function.

_variant_t vi(L"demo");
ptr->TestObject(vi);

_variant_t vr = ptr->TestObjectReturn();
if (vr.vt == VT_BSTR)
{
   std::wcout << "object: " << (wchar_t*)vr.bstrVal << std::endl;
}

Handling COM visible interfaces

COM visible interfaces can also be marshaled back and forth from managed to native or the other way around through COM. COM visible interfaces can be of one of four possible types: IUnknown, IDispatch, dual, or IInspectable (these are interfaces exposed to COM as Windows Runtime interfaces). IUnknown and IInspectable interfaces are marshaled as IUnknown* (the VARENUM type VT_UNKNOWN), and IDispatch and dual interfaces as IDispatch* (the VARENUM type is VT_DISPATCH).

In the following example, IBar is a COM visible interface of type IDispatch. It has a couple properties (and integer ID and a string name) and method that returns an array of bytes.

[Guid("7FA115C0-C1D3-49B8-B0B7-B7155CE307C5")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IBar
{
  [DispId(1)]
  int Id { get; set; }

  [DispId(2)]
  string Name { get; set; }

  [DispId(3)]
  byte[] GetData();
}

Bar is a clas that implement the IBar interface.

[Guid("564ADB07-434F-4ED3-A138-B5E41976F099")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
class Bar : IBar
{
  public int Id { get; set; }

  public string Name { get; set; }

  public byte [] GetData()
  {
     return new byte[] { 1, 2, 3 };
  }
}

In the ITest interface there is a method that returns a reference to an IBar interface, and a method that takes an IBar reference as argument.

[DispId(46)] IBar TestInterfaceReturn();
[DispId(47)] void TestInterface(IBar bar);
public IBar TestInterfaceReturn()
{
   return new Bar() { Id = 1, Name = "Test" };
}

public void TestInterface(IBar bar)
{
   Console.Write($"Bar({bar.Id}, {bar.Name})=");
   foreach (var e in bar.GetData()) Console.Write($"{e} ");
   Console.WriteLine();
}

In the ManagedLib.tlh header, the IBar wrapper class has the following definition:

struct __declspec(uuid("7fa115c0-c1d3-49b8-b0b7-b7155ce307c5"))
IBar : IDispatch
{
    //
    // Property data
    //

    __declspec(property(get=GetId,put=PutId))
    long Id;
    __declspec(property(get=GetName,put=PutName))
    _bstr_t Name;

    //
    // Wrapper methods for error-handling
    //

    // Methods:
    long GetId ( );
    void PutId (
        long _arg1 );
    _bstr_t GetName ( );
    void PutName (
        _bstr_t _arg1 );
    SAFEARRAY * GetData ( );
};

The corresponding methods in the ITest wrapper class look like this:

IBarPtr TestInterfaceReturn ( );
HRESULT TestInterface (struct IBar * bar );

An example of using the two methods is shown below:

ManagedLib::IBarPtr bar = ptr->TestInterfaceReturn();
if (bar != nullptr)
{
   std::wcout << "Bar(" << bar->Id << ", " << (wchar_t*)bar->Name << ")=";

   SAFEARRAY* data = bar->GetData();
   LONG begin{ 0 };
   LONG end{ 0 };
   SafeArrayGetLBound(data, 1, &begin);
   SafeArrayGetUBound(data, 1, &end);
   for (LONG i = begin; i <= end; ++i)
   {
      unsigned char v;
      SafeArrayGetElement(data, &i, &v);
      std::cout << (int)v << ' ';
   }
   std::cout << std::endl;
}

bar->Name = "Test2";
ptr->TestInterface(bar);

Additional examples for marshaling interface references are available in the accompaning source code.

See Also

License

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

Share

About the Author

Marius Bancila
Architect Visma Software
Romania Romania
Marius Bancila is the author of Modern C++ Programming Cookbook and The Modern C++ Challenge. He used to be a Microsoft MVP for VC++ and later Visual Studio and Development Technologies for 11 years. He works as a system architect for Visma, a Norwegian-based company. He is mainly focused on building desktop applications with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++ programmers. You can follow Marius on Twitter at @mariusbancila.

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
germanredcat5-Feb-19 10:43
membergermanredcat5-Feb-19 10:43 
PraiseGreat Article Pin
Daniel Kamisnki19-Sep-18 15:30
memberDaniel Kamisnki19-Sep-18 15:30 
PraiseMy 5 Pin
Igor Ladnik15-Sep-17 18:55
mvaIgor Ladnik15-Sep-17 18:55 
Questionit is possible without a DLL to install in one EXE? Pin
Erhy16-Aug-17 11:31
memberErhy16-Aug-17 11:31 

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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web06 | 2.8.190306.1 | Last Updated 11 Jul 2017
Article Copyright 2017 by Marius Bancila
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid