//
you're reading...
.NET/COM/ActiveX Interop, Managed Structures

Marshaling a SAFEARRAY of Managed Structures by COM Interop Part 1

1. Introduction.

1.1 There are two basic ways to pass an array of managed structures to unmanaged code. These include :

  • By C-style array.
  • By SAFEARRAY.

1.2 In this series of blogs I shall expound on the use of SAFEARRAYs to pass an array of managed structures to unmanaged code.

1.3 In this part 1, I shall provide background information on SAFEARRAYs that contain structures and co-relate this with managed structures. This should serve as a solid foundation for the remaining parts of the series. I shall also build up a sample managed structure that will be used throughout this series.

2. SAFEARRAYs and User-Defined Types.

2.1 When a structure is to be contained inside a SAFEARRAY, the type of the structure must be a COM User-Defined Type (UDT). In order to qualify as a COM UDT, the following rules apply :

  • It must be associated with a GUID.
  • It must internally contain OLE-Automation compatible types.
  • It must be associated with an IRecordInfo object.

A typical UDT defined in an IDL file looks like the following :

typedef
[
        uuid(B4A16864-42FF-48ea-973B-E0BE5922719E),
        version(1.0)
]
struct TestStruct
{
  long m_integer;
  double m_double;
  BSTR m_string;
} TestStruct;

2.2 With reference to the above IDL struct declaration, note the following :

  • The declared GUID will be indelibly associated with the structure.
  • The types of its members are those that can be found in a VARIANT (e.g. long, double, BSTR).
  • The IRecordInfo object is created at runtime by the COM sub-system with reference to the type library generated for the IDL from which the structure was defined.

2.3 Hence when used to contain an array of structures which originate from managed code, the managed version of the structure must be coded such that the above-mentioned criteria can be met. We shall discuss this in greater detail in the next section

3. Transforming a Managed Structure into a UDT.

3.1 Throughout the remainder of this blog, we shall use the following C# managed structure :

    public struct TestStruct
    {
        public Int32 m_integer;
        public double m_double;
        public string m_string;
    }

3.2 To ensure that such a structure can be transformed into a UDT, we look at the requirements listed in point 2.1 : assigning a GUID to it, ensuring that its equivalent unmanaged types are OLE-Automation compatible (i.e. types that can be found inside a VARIANT), and ensuring that it can be associated with an IRecordInfo object.

3.3 To associate a managed structure with a GUID, we use the GuidAttribute, e.g. :

[Guid("B4A16864-42FF-48ea-973B-E0BE5922719E")]

3.4 To ensure that the types of the struct members have unmanaged equivalents that are OLE-Automation compatible, we use compatible managed types and, if necessary decorate the member with the MarshalAsAttribute with an appropriate UnmanagedType enumeration argument.

3.5 In our example TestStruct, the “m_integer” member is of type Int32. This directly maps to the VT_I4 variant type. The “m_double” member is of type double. This maps to the VT_R8 type. Finally, the “m_string” member, a string, maps to the VT_BSTR type.

3.6 To ensure that our managed structure can be associated with a COM IRecordInfo object, we must use the ComVisibleAttribute. This, in fact, is the most important attribute to use. The GuidAttribute may be omitted if this attribute is used because once the structure is declared as being COM visible, a GUID will be automatically generated for it if one has not been declared.

3.7 Note that the IRecordInfo object is useful only in unmanaged code. It serves no purpose in managed code. It is created by the COM sub-system and each UDT will have a unique customized IRecordInfo object associated with it. To create such an object, the COM sub-system requires the COM type info of the UDT.

3.8 We shall return to this subject again later when we examine low-level interop marshaler activities (see point 6.2 below).

3.9 With the above considerations, we emerge the following C# declaraion for the TestStruct structure :

[ComVisible(true)]
[Guid("B4A16864-42FF-48ea-973B-E0BE5922719E")]
public struct TestStruct
{
  [MarshalAs(UnmanagedType.I4)]
  public Int32 m_integer;
  [MarshalAs(UnmanagedType.R8)]
  public double m_double;
  [MarshalAs(UnmanagedType.BStr)]
  public string m_string;
}

4. Passing a SAFEARRAY of TestStruct to Unmanaged Code.

4.1 There are two techniques by which we exchange a SAFEARRAY of TestStruct structures with unmanaged code :

  • Via COM Interop.
  • Via PInvoke.

4.2 In this series of blogs, we will study such exchanges by way of COM Interop. We shall end part 1 with a set of example codes demonstrating how to pass a SAFEARRAY of TestStruct to an unmanaged COM-client application.

4.3 The part 2 of this series will examine how to receive a SAFEARRAY of TestStructs from unmanaged code. Then in part 3, we will study how to exchange a SAFEARRAY 2 ways, i.e. as an “in” and “out” parameter (or by reference parameter).

4.4 In other blog series, we will study such exchanges by way of PInvoke.

5. The COM Interface and Implementation.

5.1 Throughout the rest of the blog, we shall use the following interface defined in managed code but declared COM-visible :

[ComVisible(true)]
[Guid("FE37FC87-1E1B-4262-B720-1E6503B3E964")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITestInterface
{
  void GetTestStructArray([Out] out TestStruct[] test_struct_array);
  void SetTestStructArray([In] TestStruct[] test_struct_array);
  void ReferenceTestStructArray([In][Out] ref TestStruct[] test_struct_array);
}

5.2 This interface is to be defined in a C# Class Library project. After compilation and registered to COM via regasm, it will generate the following COM interface :

[
  odl,
  uuid(FE37FC87-1E1B-4262-B720-1E6503B3E964),
  version(1.0),
  oleautomation
]
interface ITestInterface : IUnknown
{
  HRESULT _stdcall GetTestStructArray([out] SAFEARRAY(TestStruct)* test_struct_array);
  HRESULT _stdcall SetTestStructArray([in] SAFEARRAY(TestStruct) test_struct_array);
  HRESULT _stdcall ReferenceTestStructArray([in, out] SAFEARRAY(TestStruct)* test_struct_array);
}

Note that for each of the 3 methods, the managed array parameters are transformed by the type library exporter as SAFEARRAYs. This is the default matching type for managed arrays. Note the following pertinent points related to this observation :

  • This is so even if the managed class is exported to COM as IUnknown-based (as is our case) The SAFEARRAY type is used because it allows for the use of the Type Library Marshaler (contained in oleaut32.dll).
  • The alternative (i.e. C-style arrays) will require the use of size_is() IDL attribute. This is undesireable because the Type Library Marshaler cannot be used and a custom proxy/stub DLL will have to be produced somehow.
  • Custom proxy/stub DLLs can only be used in the situation where the type library is generated via IDL (instead of via C# as is the case in our current example).

5.3 The implementation of ITestInterface shall be in C#. The following is a skeleton of this implementation :

[ComVisible(true)]
[Guid("61470868-479E-49b9-8B69-72E2693608EE")]
[ProgId("TestCSCOMServer01.TestClass")]
[ClassInterface(ClassInterfaceType.None)]
public class TestClass : ITestInterface
{
        public void GetTestStructArray([Out] out TestStruct[] test_struct_array)
        {
		...
        }

        public void SetTestStructArray([In] TestStruct[] test_struct_array)
        {
		...
        }

        public void ReferenceTestStructArray([In][Out] ref TestStruct[] test_struct_array)
        {
		...
        }
}

5.4 Throughout this blog, the test client application shall be written in C++.

6. Returning a SAFEARRAY of TestStruct via COM Interface Method.

6.1 With reference to ITestInterface, the relevant COM method is

void GetTestStructArray([Out] out TestStruct[] test_struct_array);

6.2 The following is an implementation of this method :

public void GetTestStructArray([Out] out TestStruct[] test_struct_array)
{
  test_struct_array = new TestStruct[10];

  for (int i = 0; i < test_struct_array.Length; i++)
  {
    test_struct_array[i].m_integer = i;
    test_struct_array[i].m_double = 0.123 + (double)i;
    test_struct_array[i].m_string = string.Format("Hello World {0:D}", i);
  }
}

The basic idea is for an unmanaged C++ client to call this method from an instance of the “TestClass” class in order to obtain a SAFEARRAY of TestStructs.

The following describes what happens at a low-level :

  • At the end of the GetTestStructArray() method, a COM SAFEARRAY will be created from the newly created and initialized test_struct_array managed array.
  • This SAFEARRAY (that will contain the TestStruct structures) is created using the SafeArrayAllocDescriptorEx() API.
  • The interop marshaler then internally generates COM Type Info for the TestStruct structure.
  • It will then call the GetRecordInfoFromTypeInfo() API to generate an unmanaged IRecordInfo object that is to be associated with the TestStruct structure in unmanaged code.
  • The interop marshaler then inserts the IRecordInfo object into the SAFEARRAY via the SafeArraySetRecordInfo() API.
  • The SAFEARRAY will then be returned to the unmanaged client application.

6.3 A sample C++ code that creates an instance of the C# TestClass class is listed below :

#include "stdafx.h"
#import "..\TestCSCOMServer01\bin\Debug\TestCSCOMServer01.tlb" raw_interface_only no_implementation
using namespace TestCSCOMServer01;

void GetTestStructArray(/*[in]*/ ITestInterfacePtr& spITestInterface);

int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(NULL);

	if (1)
	{
		ITestInterfacePtr spITestInterface = NULL;

		spITestInterface.CreateInstance(__uuidof(TestClass));

		GetTestStructArray(spITestInterface);
	}

	::CoUninitialize();

	return 0;
}

The following is a sample C++ function which accesses the GetTestStructArray() method :

void GetTestStructArray(/*[in]*/ ITestInterfacePtr& spITestInterface)
{
	SAFEARRAY* pSafeArrayOfTestStruct = NULL;
	// Call the GetTestStructArray() method to obtain
	// a SAFEARRAY of TestStruct structures.
	spITestInterface -> GetTestStructArray(&pSafeArrayOfTestStruct);

	if (pSafeArrayOfTestStruct)
	{
	    // Call the SafeArrayGetVartype() to determine
	    // the VARTYPE of the contained elements.
		VARTYPE vt;	

		// The value is VT_RECORD (i.e. 36).
		SafeArrayGetVartype(pSafeArrayOfTestStruct, &vt);

		// Get the IRecordInfo object associated with the structure.
		IRecordInfoPtr spIRecordInfo = NULL;
		SafeArrayGetRecordInfo(pSafeArrayOfTestStruct, &spIRecordInfo);

		// Get the GUID associated with the IRecordInfo object.
		// This is the same GUID associated with the structure,
		// i.e. B4A16864-42FF-48ea-973B-E0BE5922719E.
		GUID guid;
		spIRecordInfo -> GetGuid(&guid);			

		// Obtain bounds information of the SAFEARRAY.
		long lLbound = 0;
		long lUbound = 0;

		SafeArrayGetLBound(pSafeArrayOfTestStruct, 1, &lLbound);
		SafeArrayGetUBound(pSafeArrayOfTestStruct, 1, &lUbound);
		long lDimSize = lUbound - lLbound + 1;		

		// Obtain a copy of each structure contained
		// in the SAFEARRAY.
		for (int i = 0; i < lDimSize; i++)
                  {
                    long rgIndices[1];
                    TestStruct value;
                    // Note that we must set all fields
                    // inside value to zero.
                    // This is important because
                    // SafeArrayGetElement() will first
                    // clear all members of value.
                    // For the "m_string" BSTR member,
                    // it will call the ::SysFreeString()
                    // API. Now if "m_string" contains
                    // random data, it will result in
                    // a crash. If it is NULL, it is OK.
                    memset(&value, 0, sizeof(value));
                    // SafeArrayGetElement() will return
                    // a copy of the relevant element in
                    // the structure. Hence it is important
                    // that when "value" is no longer required,
                    // its members be cleared. This is especially
                    // so for members which point to allocated
                    // memory (e.g. BSTRs).
                    rgIndices[0] = i;
                    SafeArrayGetElement
                    (
                      pSafeArrayOfTestStruct,
                      rgIndices,
                      (void FAR*)&value
                    );
                    printf ("TestStruct[%d].m_integer : [%d]\r\n", i, value.m_integer);
                    printf ("TestStruct[%d].m_double  : [%f]\r\n", i, value.m_double);
                    printf ("TestStruct[%d].m_string  : [%S]\r\n", i, value.m_string);
                    // We clear the members of "value"
                    // which includes the "m_string" BSTR member.
                    spIRecordInfo -> RecordClear((PVOID)&value);
		}

		::SafeArrayDestroy(pSafeArrayOfTestStruct);
		pSafeArrayOfTestStruct = NULL;
	}
}

Note the following pertinent points about the function :

  • The SafeArrayGetVartype() API can be used to determine what is the VARTYPE of the elements contained in the SAFEARRAY. This will be VT_RECORD (value 36).
  • The IRecordInfo object associated with the structure is obtainable via the SafeArrayGetRecordInfo() API.
  • The IRecordInfo::GetGuid() method can be used to obtain the GUID associated with the structure.
  • SafeArrayGetLBound() and SafeArrayGetUBound() can be used on the SAFEARRAY as per normal.
  •  As we obtain each element of the SAFEARRAY via SafeArrayGetElement(), note that each returned structure is actually a copy of the corresponding element. This is because COM uses copy-semantics. Hence the calling code remains the owner of the memory occupied by “value” (including its “m_string” member). The “value” structure must later be released using IRecordInfo::RecordClear().
  • The other important matter is that before we call on SafeArrayGetElement(), the “out” parameter “value” must first be completely cleared (in our example, we used memset() to set all member fields to zero values). This is important because as SafeArrayGetElement() fills “value” with field values, it will first clear its existing field values. For its “m_string” field, it will call SysFreeString(). Now if “m_string” is filled with random data, SysFreeString() will crash. If it contains zero, SysFreeString() will do nothing.

7. In Conclusion.

7.1 In this part 1, we have studied in some depth the background of using SAFEARRAYs to contain UDTs. We have also discussed how a managed structure may be exposed to COM as a UDT.

7.2 We ended this part with a concrete example demonstrating how to pass a SAFEARRAY of UDTs (which originate from managed code) to an unmanaged client.

7.3 In the next installment, we shall be looking at how to pass a SAFEARRAY of TestStruct from unmanaged code to the managed TestClass server.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

One thought on “Marshaling a SAFEARRAY of Managed Structures by COM Interop Part 1

  1. Would there be a problem if m_string, in the managed struct, were mapped to VT_LPWSTR instead of VT_BSTR?
    The unmanaged struct would then be:

    struct TestStruct
    {
    long m_integer;
    double m_double;
    wchar_t *m_string;
    } TestStruct;

    Posted by Germán | October 5, 2012, 5:33 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: