Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#

Interoping .NET and C++ through COM

Rate me:
Please Sign up or sign in to vote.
4.97/5 (24 votes)
11 Jul 2017CPOL15 min read 34.5K   334   50   7
Create COM visible types in C# and consume them from C++

Introduction

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:

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

GuidAttribute Specifies the GUID that defines the interface or class unique identifier.
ComVisibleAttribute Controls 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.
InterfaceTypeAttribute Specifies 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.
ClassInterfaceAttribute Specifies 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.
DispIdAttribute Specifies the COM dispatch ID for a method, property or field.
ProgIdAttribute Allows to specify a programmable ID, which is a human friendly name for the COM class and must be unique 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 option, 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.

Image 1

Importing a Type Library in C++

A type library is a binary file that contains information 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.

C++
#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
    C++
    // 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
    C++
    // 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 most importance in our case:

  • The declaration of a smart pointer in the form:
    C++
    _COM_SMARTPTR_TYPEDEF(ITest, __uuidof(ITest));
    _COM_SMARTPTR_TYPEDEF is a macro that expands to the following:
    C++
    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:

C++
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 type From example Comments
IDispatch* this Pointer to an IDispatch interface
DISPID 0x4 Dispatch identifier of the interface member
WORD DISPATCH_METHOD Flags describing the context of the Invoke() call
VARTYPE VT_EMPTY Type of the return value
void* NULL Pointer 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 decimal 2 that is VT_I2 (i.e. signed 16-bit integer).
... (ellipsis) b, s, i, l A 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 uninitialize 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.

C++
#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 Framework Size in bits COM/C++ Size in bits VARENUM
bool System.Boolean 8 VARIANT_BOOL 16 VT_BOOL
char System.Char 8 unsigned short 16 VT_UI2
sbyte System.SByte 8 char 8 VT_UI1
byte System.Byte 8 unsigned char 8 VT_UI1
short System.I16 16 short 16 VT_I2
ushort System.UInt16 16 unsigned short 16 VT_UI2
int System.Int32 32 long 32 VT_I4
uint System.UInt32 32 unsigned long 32 VT_UI4
long System.Int64 64 __int64 64 VT_I8
ulong System.UInt64 64 unsigned __int64 64 VT_UI8
float System.Single 32 float 32 VT_R4
double System.Double 64 double 64 VT_R8
decimal System.Decimal 128 DECIMAL 128 VT_DECIMAL
string System.String   _bstr_t (BSTR)   VT_BSTR
object System.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.
    C++
    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 a 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.

    C++
    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 of 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().
    C++
    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:
    C++
    [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:
    C++
    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:
    C++
    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.

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

C++
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*.

C++
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 dimension and a pointer to the actual data.

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

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

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

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

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

  • Passing the array as an input parameter:
    C++
    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 dimensional 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:
    C++
    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:
    C++
    SAFEARRAYBOUND sab{ 0, 0 };                      // define unspecified array bounds
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab); // create an one dimensional 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 shown so far used single dimensional 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-dimensional arrays of 32-bit signed integers:

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

Their implementation in the Test class is shown below:

C++
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-dimensional arrays.

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

C++
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).

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

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

C++
_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.

C++
[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 class that implement the IBar interface.

C++
[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.

C++
[DispId(46)] IBar TestInterfaceReturn();
[DispId(47)] void TestInterface(IBar bar);
C++
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:

C++
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 looks like this:

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

An example of using the two methods is shown below:

C++
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 accompanying source code.

See Also

History

  • 11th July, 2017: Initial version

License

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


Written By
Architect Visma Software
Romania Romania
Marius Bancila is the author of Modern C++ Programming Cookbook and The Modern C++ Challenge. He has been a Microsoft MVP since 2006, initially for VC++ and nowadays for Development technologies. He works as a system architect for Visma, a Norwegian-based company. He works with various technologies, both managed and unmanaged, for desktop, cloud, and mobile, mainly developing with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. You can follow Marius on Twitter at @mariusbancila.

Comments and Discussions

 
QuestionMarius to the Rescue Again. Pin
TheCPUWizard7-Nov-23 12:49
TheCPUWizard7-Nov-23 12:49 
QuestionDefault Parameter value in interface Pin
Member 1060975921-Apr-22 20:52
Member 1060975921-Apr-22 20:52 
GeneralMy vote of 5 Pin
Ensamblador31-Jan-21 12:12
Ensamblador31-Jan-21 12:12 
Very usefull example and easy to implement in contrast with one similar I found.
GeneralMy vote of 5 Pin
germanredcat5-Feb-19 10:43
germanredcat5-Feb-19 10:43 
PraiseGreat Article Pin
Daniel Kamisnki19-Sep-18 15:30
Daniel Kamisnki19-Sep-18 15:30 
PraiseMy 5 Pin
Igor Ladnik15-Sep-17 18:55
professionalIgor Ladnik15-Sep-17 18:55 
Questionit is possible without a DLL to install in one EXE? Pin
Erhy16-Aug-17 11:31
Erhy16-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.