Last Modification: September 13, 2001

HOWTO: Use structs in Automation-compatible interfaces

Structs, also known as User Defined Types (UDTs), can be used in Automation- compatible interfaces since Win NT4 SP4 and DCOM 1.2 for Win9x. An Automation- compatible struct may contain only primitive Automation types as its members. Nesting structs is not allowed, but VARIANT is allowed thus enabling nested structs (you can store a struct in a VARIANT). In order for a struct to be usable for Automation-compatible interfaces, the struct must be described in a type library and it must be declared with its own GUID:

struct MyStruct
	[helpstring("A long value")]
	long nLongValue;

	[helpstring("A string")]
	BSTR bstrStringValue;

// Later in the IDL file
[uuid(...), version(...), helpstring(...)]
library MyLib
	struct MyStruct;

For unknown reason, the MIDL compiler bundled with VC6 has not been updated even in SP4. Therefore, you need the latest Platform SDK installed on your computer (or at least no older than Platform SDK Jul'99) in order to compile the above IDL code and any IDL using structs with Automation.

NOTE: The current release of MIDL at the time of writing (5.03.0280) does not support the helpstring() attribute on the struct itself. helpstring() is supported on the struct fields, though.

Once your struct is declared as above, you can pass it via Automation- compatible interfaces. Keep in mind, however, that only struct pointers can be passed:

HRESULT SendStruct([in] struct MyStruct *pData);
HRESULT GetStruct([out, retval] struct MyStruct *pData);

Structs can also be passed in safe arrays and VARIANTs:

HRESULT SendManyStructs([in] SAFEARRAY(struct MyStruct) *psaData);

Neither IDL nor Automation define byte alignment for a struct. VB assumes 4-byte alignment, while #import in VC assumes 8-byte alignment. For most Automation structs this isn't an issue. However, if you use 8-byte types like double, this may make a difference:

[uuid(...), helpstring(...)]
struct BadAlign
	long   nLongValue;
	double dblDoubleValue;

For VB, the double field starts at the fourth byte of the struct, while for VC's #import it starts at the eight byte. This poses a significant problem. It can be solved by adding an inject_statement clause to #import:

#import "My.tlb" inject_statement("#pragma pack(4)")

IMPORTANT: Structs cannot be used by scripting clients!

Implementing structs in code is usually trivial - just use the struct directly. There are two specific struct uses when the code is fairly non-trivial - when using a safe array of structs and when stuffing the struct into a VARIANT. Let's note first that in Automation any struct is represented via an IRecordInfo interface pointer. The object exposing this interface is implemented by the Automation runtime and is acquired via the following two API functions:


When structs are used in say dual interfaces, this detail is hidden from the implementer and the user of the interface. Only the marshaler knows and uses it. However, when we need to pass structs in a VARIANT or a safe array, we have to delve into these details ourselves. Here's the C++ code for populating a VARIANT with a struct (error checking omitted):

IRecordInfo *pRI;
MyStruct data;

data.nLongValue = 5;
data.bstrStringValue = SysAllocString(L"Something");
hr = GetRecordInfoFromGuids(LIBID_MyLib, 1, 0, 0x409, uuidof(MyStruct), &pRI);
vnt.vt = VT_RECORD;
vnt.pvRecord = &data;
vnt.pRecInfo = pRI;

Note that the VARIANT doesn't own the storage for the embedded struct, but VariantClear will free the content of the struct by calling IRecordInfo::RecordClear. Note also that the common VARIANT wrapper classes like CComVariant and _variant_t don't support structs. Finally, note that the mechanism for storing struct data in a VARIANT is not fully documented. For example it isn't clear how a struct can be returned in an [out] VARIANT argument without producing memory leaks.

Now let's see how to populate a safe array with structs. The following code is a trimmed down version of the MSDN example code (error checking omitted):

SAFEARRAYBOUND sab = {2, 0};
MyStruct *pData;
IRecordInfo *pRI;

hr = GetRecordInfoFromGuids(LIBID_MyLib, 1, 0, 0x409, uuidof(MyStruct), &pRI);
psa = SafeArrayCreateEx(VT_RECORD, 1, &sab, pRI);
hr = SafeArrayAccessData(psa, (void**)&pData);
pData[0].nLongValue = 1;
pData[0].bstrStringValue = SysAllocString(L"First");
pData[1].nLongValue = 2;
pData[1].bstrStringValue = SysAllocString(L"Second");
hr = SafeArrayUnaccessData(psa);

Unlike a VARIANT, a safe array owns the storage of its structs.

The UDT documentation is located at: mk:@MSITStore:D:\MSDN\Automat.Chm::/htm/chap12_3rcj.htm (Copy the link and paste it in the "Jump to URL..." box. Substitute the path with the path to your MSDN files. This link is obtained from MSDN Library Oct'2000.)