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:
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.
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.
#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
#pragma once
#pragma pack(push, 8)
#include <comdef.h>
namespace ManagedLib {
struct __declspec(uuid("56418cab-6e5e-41c7-b477-e3b5c250d879"))
__ManagedLib;
struct __declspec(uuid("d3ce54a2-9c8d-4ea0-ab31-2a97970f469a"))
ITest;
struct Test;
_COM_SMARTPTR_TYPEDEF(ITest, __uuidof(ITest));
struct __declspec(uuid("d3ce54a2-9c8d-4ea0-ab31-2a97970f469a"))
ITest : IDispatch
{
HRESULT TestBool (
VARIANT_BOOL b );
HRESULT TestSignedInteger (
char b,
short s,
long i,
__int64 l );
};
struct __declspec(uuid("a7a5c4c9-f4da-4cd3-8d01-f7f42512ed04"))
Test;
#include "c:\comdemo\nativeclient\debug\managedlib.tli"
}
#pragma pack(pop)
- managedlib.tli
#pragma once
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:
_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 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.
#include <iostream>
#import "ManagedLib.tlb"
struct COMRuntime
{
COMRuntime() { CoInitialize(NULL); }
~COMRuntime() { CoUninitialize(); }
};
int main()
{
COMRuntime runtime;
ManagedLib::ITestPtr ptr;
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 typedef
s 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 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.
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 BSTR
s 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. BSTR
s are allocated with SysAllocString()
and destroyed with SysFreeString()
.
BSTR str = SysAllocString(L"sample");
SysFreeString(str);
_bstr_t str(L"sample");
- 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 dimension 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 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:
[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 and output arrays using SAFEARRAY
for these functions can be done in the following manner:
- Passing the array as an input parameter:
SAFEARRAYBOUND sab;
sab.cElements = 3;
sab.lLbound = 0;
SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab);
for(LONG i = 0; i < 3; ++i)
{
int value = i + 1;
SafeArrayPutElement(sa, &i, &value);
}
ptr->TestIntArray(sa);
SafeArrayDestroy(sa);
- Receiving an array as a return value:
SAFEARRAY* sa = ptr->TestIntArrayReturn();
VARTYPE vt;
SafeArrayGetVartype(sa, &vt);
if (vt == VT_I4)
{
LONG begin{ 0 };
LONG end{ 0 };
SafeArrayGetLBound(sa, 1, &begin);
SafeArrayGetUBound(sa, 1, &end);
for (LONG i = begin; i <= end; ++i)
{
int value;
SafeArrayGetElement(sa, &i, &v);
}
}
SafeArrayDestroy(sa);
- Passing an array as an output parameter:
SAFEARRAYBOUND sab{ 0, 0 };
SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab);
ptr->TestIntOutArray(&sa);
LONG begin{ 0 };
LONG end{ 0 };
SafeArrayGetLBound(sa, 1, &begin);
SafeArrayGetUBound(sa, 1, &end);
for (LONG i = begin; i <= end; ++i)
{
int value;
SafeArrayGetElement(sa, &i, &value);
}
SafeArrayDestroy(sa);
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:
[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-dimensional arrays.
HRESULT TestInt2DArray (SAFEARRAY * arr);
SAFEARRAY * TestInt2DArrayReturn ( );
The way these functions are consumed from C++ and how the SAFEARRAY
s 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 string
s 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 class 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
{
__declspec(property(get=GetId,put=PutId))
long Id;
__declspec(property(get=GetName,put=PutName))
_bstr_t Name;
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:
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 accompanying source code.
See Also
History
- 11th July, 2017: Initial version