The reason I got into this is that I've rarely used any help from newsgroups or similar communities. On the other hand since I've used code provided by other developers/programmers on CodeProject and CodeGuru it seemed reasonable to join a couple of them and just have a look.

Early in May 2000 I noticed several posts about UDTs and their interaction with VB and ATL. At this point I may say I had not any real experience on the subject. As a matter of fact I've never developed professionally in COM with C++ or ATL. In addition I've learned the hard way that one cannot apply the same coding techniques one uses with C or C++ to VB. Still I consider myself novice in the COM environment.

It is true that there is very little help in implementing UDTs in COM and even less in implementing arrays of UDTs. In the past it was not even thinkable to use UDTs in COM. Nowadays there is support for UDTs in COM but there are no real example projects on how to use this feature. So a personal mail by a fellow developer inspired me to go onto this.

I am going to present a step by step approach on creating an ATL project which using UDTs to communicate with a VB Client. Using it with a C++ Client will be easy as well.

This document will proceed along with the project. I assume you are familiar with ATL, COM and VB. On the way I may present practices I use myself, which may be irrelevant to the cause of this example, but on the other hand you may have also used these practices as well or beginners may benefit from these.

Create the ATL project.

As a starting point create an ATL DLL project using the wizard. Set the name of the project to UDTDemo and accept the defaults. Now let's have a look at the generated "IDL" file.

//UDTDemo.IDL

import "oaidl.idl";
import "ocidl.idl";

[
    uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F),
    version(1.0),
    helpstring("UDTDemo 1.0 Type Library")
]
library UDTDEMOLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

};

Modify the type library name

As you expected this, there is nothing unknown in this file so far. Well, the fact is that I do not really like the "Lib" portion added to the name of the projects I create, and I always change it before any object is being inserted into the project. This is very easy.

As a first step edit the "IDL" file and set the library name to what you like. You have only to remember that this is case sensitive when the MIDL generated code is used. The modified file is shown bellow.

//UDTDemo.IDL

import "oaidl.idl";
import "ocidl.idl";

[
    uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F),
    version(1.0),
    helpstring("UDTDemo 1.0 Type Library")
]
library UDTDemo   //UDTDEMOLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

};

The second step is to replace any occurrence of the previous library name with the new one. The only file apart the "IDL" one, where the library name is found is the main project implementation file, "UDTDemo.cpp", where DllMain is called and the _module is initialized. You may also use the "Find in Files" command from the toolbar and search for the "UDTDEMOLib" string.

What ever way we use we have to replace the "LIBID_UDTDEMOLib" string with the "LIBID_UDTDemo" one. Mind the case of the strings. It is case sensitive.

Now you are ready to change the name of your type library to what you really like. Again keep in mind that this is not going to be trivial unless it is done before any object is added into the project, or before any early compilation of the project.

Bellow is the modified DllMain function of our project.

//UDTDemo.cpp

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        //_Module.Init(ObjectMap, hInstance, &LIBID_UDTDEMOLib);
        _Module.Init(ObjectMap, hInstance, &LIBID_UDTDemo);
        DisableThreadLibraryCalls(hInstance);
    }
    else if (dwReason == DLL_PROCESS_DETACH)
        _Module.Term();
    return TRUE;    // ok
}

You may Compile the project now. Be sure everything is done right. In case something goes wrong you should make sure all occurrences of "UDTDEMOLib" are replaced with "UDTDemo".

Defining the structure.

An empty project is not of any use. Our purpose is to define a UDT, or struct respectively, and this is what I am going to do right now.

The demo project will handle named variables. This means we need a structure for holding the Name, and theValue of a variable. Although I haven't tested it yet, we may add a VARIANT to hold some other Special data.

The above types where chosen so as you may see the whole story, and not take any homework at all. :)

So open the UDTDemo.idl file and add these lines before the library block.

//UDTDemo.idl

    typedef
    [
        uuid(C21871A0-33EB-11D4-A13A-BE2573A1120F),
        version(1.0),
        helpstring("A Demo UDT variable for VB projects")
    ]
    struct UDTVariable {
        [helpstring("Special case variant")]    VARIANT  Special;
        [helpstring("Name of the variable")]    BSTR     Name;
        [helpstring("Value of the variable")]   long     Value;
    } UDTVariable;

Save and build again. Everything should compile without any problems. Well you have to follow this pace in this demo project. :)

User Defined data Types. The Theory.

Whenever a structure is created in IDL we need to specify a UUID for it so as the type library manager interfaces can get information about the UDT and access it. (I also realized why on this project :) ).

UUIDs

How is the UUID for the structure acquired? No, we do not execute the guidgen utility. My next free hack is this. It may not be approved, but it works. Go to the library section, copy the UUID of the library, and paste it after thetypedef keyword of the structure inside the angle brackets. Then go to the 8th digit and subtract (one) 1.

The library     uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F)
                            |
                           \./
The UDTVariable uuid(C21871A0-33EB-11D4-A13A-BE2573A1120F)

As it is documented the UUIDs are created using the current date, the current time and the unique number of the network card of the computer. Well, the time and date part resides in the first eight-digit part of the UUID. The next four-digit part is not known to me so far. The rest of it is unique and, we may say, identifies your PC. So, subtracting one (1) from the time part takes us to the past. Finally this UUID is still unique!!!

As a rule of thumb, after the library UUID is created, I add one (1) to the time part of the UUID for every interface andcoclass I insert into the project. Subtract one (1) for the structures or enumerations I use. Basically the interface UUIDs are replaced and will be demonstrated later.

The only reason I get into this kind of trouble is because it is said that Windows handle consequent UUIDs in the registry faster!

More type attributes.

After the definition of the UUID for our structure we define its version number. This is a hack discovered after checking the type library of a VB created object. VB adds a version number to everything it adds to a type library. This will never be used in this project but why not use it?

Then add a help string. This brief description is very useful to everyone using our library. I recommend using it all the time.

We could also add the public keyword to make the structure visible from the library clients. This is not necessary as it will finally be implicitly visible to the clients. Clients should not be able to create any structure which might not be used in the interfaces exposed by our object.

The UDT Data members.

Let's proceed to the data members now. First every data member of our UDT must be an automation compatible type. In the simpler form, as I've conclude, in a UDT we are allowed to use only the types defined in the VARIANT union, if you have checked the code, or whatever VB allows us to use inside a variant type.

This is only for our sake to avoid creating marshaling code for our structure. Otherwise you are free to pass even a bit array with a structure :).

The data types of our UDT members were chosen so as we can expect some difficulty and make the demonstration as complete as possible.

  • long Value : a member of type long was chosen because it behaves like any other built in type. There are no extra considerations to be taken for built in types. (long, byte, double, float, DATE, VT_BOOL).

  • BSTR Name : Although strings are easily handled in VB, here we have some considerations to take into account. Creation, Initialization and Destruction of the string are good reasons to take care of and use a string in the demo.

  • VARIANT Special : This came up just now. Since we are going to do it, variants are more difficult to use than BSTR's, Not only in terms of initialization and termination, but also in checking what is the real type of the actual variants. This is not so bad!

Arrays as structure members.

At this point you should know how to declare a structure of simple types in IDL. Finally, now that you know how to declare a UDT structure to be used in VB we have to take the exercise 1 and create a UDT which holds an array of UDTs. The reason is, that arrays are also special cases, and since we haven't put an array in our structure in the first place, lets make a case with an array. Using an array of longs or what ever other type would be the same at this point of the demonstration.

//UDTDemo.idl

    typedef
    [
        uuid(C218719F-33EB-11D4-A13A-BE2573A1120F), 
        version(1.0),
        helpstring("A Demo UDT Holding an Array of Named Variables")
    ]
    struct UDTArray {

        [helpstring("array of named variables")]    
                   SAFEARRAY(UDTVariable) NamedVars;

    } UDTArray;

As you have noticed, the only difference is that we used the SAFEARRAY declaration in the first place, but we also included the name of our newly declared UDT. This is the right way to give the COM mechanism the knowledge of what the array will be holding. At this point we have declared a UDT holding a typed array.

Declaring an array of longs it would be as simple, as declaring the following.

SAFEARRAY(long)

Performing a test.

We may compile our project once more. At this point it would be nice to create a test VB project , and add our library into this client project through the references in the project menu. Now press F2 to check what VB may see in our library. Well, nothing but the globals appears in the window.

This is due to the fact that we have declared our UDT's outside the library block in the IDL file. Well, if any declared item, (enum, UDT, Interface) outside the library block, is not explicitly or implicitly imported inside the library block, then this item is unknown (not known) to the clients of the type library.

Lets make a simple test. Save the VB project, and then close it. Otherwise the UDTDemo project will not pass the link step. Inside the "UDTDemo.idl" file go inside the library block and add the following lines.

//UDTDemo.IDL

library UDTDemo   //UDTDEMOLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");
    
    struct UDTVariable;
    struct UDTArray;
    
};

Build the UDTDemo project once more and open the VB demo project. Open the references dialog, uncheck the UDTDemo line close it and then register our UDTDemo again with the VB project through the references.

Opening the object browser now, will show both the UDT's we have defined in our library. Close the VB project, andcomment out the previous lines in the "UDTDemo.idl" file. These structures will be added implicitly into the library through the interfaces we are going to define.

End of the test.

The big secret for our UDT is that the MIDL compiler attaches enough information about it with the library, so as it may be described with the IRecordInfo interface. So, Ole Automation marshaling knows how to use our UDT type as a VT_RECORD type. Items identified as records may be wired. So do arrays of records.

One more thing. The SAFEARRAY(UDTVariable) declaration is a typedef for LPSAFEARRAY. This means that the structure is really declared as

    struct UDTArray
    {
        LPSAFEARRAY NamedVars;
    }

This leads us to the conclusion that there is no information provided for us about the type of data the array holds inside our code. Only type library compitible clients know the type information.

The Demo UDT Object

So far we have some really useless structures. We may not use these anywhere, except in VB internally only if we change the "UDTDemo.idl" file.

So to make our demo project a bit useful, lets add an object to our project. Use the hopefully well known insert "new ATL Object" menu item. In the Atl Object Wizard select "simple object" and press "next".


 

Then type "UDTDemoOb" as a short name in the "ATL object wizard properties". We may use what ever name we like, but we have to avoid using "UDTDemo" as it collides with the library name.

Then as I may always suggest, in the attributes tab, check the "support IsupportErrorInfo" choice, leave it apartment threaded, but as it dawned on me right now, check the "Support Connection points" on the dialog as well.


 


 

Pressing "ok" now the wizard will create two interfaces and a coclass object for as in the IDL file, and a class to implement the UDTDemoOb interface.

We checked the support for connection points, because when we use the proxy code generator for connection point interfaces, the code is far from right in the first place, when any of the parameters is of array type. It gives a slight warning about booleans, and compiles the wrong code. So we have to see it as well.

At this point, as It is mentioned at the beginning of this document, I am going to replace the wizard generated UUIDs. the rest of you may compile the project or check this with me.

You may skip this if you like

Do not compile the project yet.

First copy the library UUID and paste it above every UUID defined for a) the IUDTDemoOb interface, b) the _IUDTDemoObEvents events interface and c) the UDTDemo coclass. While you copy the UUID, you may comment out the wizard generated ones. Then starting with the above stated order increase by one the first part of the library interface, for each new occurrence. Parts of the code will look like this.

//UDTDemo.idl

    [
        object,
        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F), //previous one
        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F), //library one, modified
        dual,
        helpstring("IUDTDemoOb Interface"),
        pointer_default(unique)
    ]
    interface IUDTDemoOb : IDispatch
    {
    };

    [
        //uuid(9117A523-34C3-11D4-A13A-AAA07458B90F), //previous one
        uuid(C21871A3-33EB-11D4-A13A-BE2573A1120F), //library one, modified
        helpstring("_IUDTDemoObEvents Interface")
    ]
    dispinterface _IUDTDemoObEvents
    {
        properties:
        methods:
    };

    [
        //uuid(9117A522-34C3-11D4-A13A-AAA07458B90F), //previous one
        uuid(C21871A4-33EB-11D4-A13A-BE2573A1120F), //library one, modified
        helpstring("UDTDemoOb Class")
    ]
    coclass UDTDemoOb
    {
        [default] interface IUDTDemoOb;
        [default, source] dispinterface _IUDTDemoObEvents;
    };

In the above items you may notice that the newly created UUIDs defer in the first part, and they are consequent. But these defer in both the first and second part with the UUID of the library. The fact is that these UUIDs are created one day later, than the one created for the library.

Since the newly created uuid's are consequent we know we are not mistaken to replace them with others consequent also, which should have been created in the past.

At this moment there are three more occurrences of the UUID of the coclass UDTDemo object. These are in the"UDTDemo.rgs" file. So copy the new UUID of the object, open the ".rgs" file in the editor, and replace the old UUID with the new one.

The above are performed for all objects created by the wizard.

// UDTDemoOb.rgs

HKCR
{
    UDTDemo.UDTDemoOb.1 = s 'UDTDemoOb Class'
    {
        CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}'
    }
    UDTDemo.UDTDemoOb = s 'UDTDemoOb Class'
    {
        CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}'
        CurVer = s 'UDTDemo.UDTDemoOb.1'
    }
    NoRemove CLSID
    {
        ForceRemove { CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F } = s 'UDTDemoOb Class'
        {
            ProgID = s 'UDTDemo.UDTDemoOb.1'
            VersionIndependentProgID = s 'UDTDemo.UDTDemoOb'
            ForceRemove 'Programmable'
            InprocServer32 = s '%MODULE%'
            {
                val ThreadingModel = s 'Apartment'
            }
            'TypeLib' = s '{C21871A1-33EB-11D4-A13A-BE2573A1120F}'
        }
    }
}

End of skip area

Compile the project. Make sure everything is ok. If we check the project with the VB client at this point, we will only see the UDTDemo object appear in the object browser. This is correct.

So lets go on and add a property to our object. Using the property wizard add a property named UdtVar, accepting a pointer to a UDTVariable. We'll get later to the pointer thing. The UDTVariable is not in the type list of the dialog, so we have to manually add it. Check the picture bellow.


 

This is how our interface looks like after pressing the [Ok] button.

    [
        object,
        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F),
        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),
        dual,
        helpstring("IUDTDemoOb Interface"),
        pointer_default(unique)
    ]
    interface IUDTDemoOb : IDispatch
    {
        [propget, id(1), helpstring("Returns a UDT Variable in the UDTObject")] 
                HRESULT UdtVar([out, retval] UDTVariable * *pVal);
        [propput, id(1), helpstring("Sets a UDT Variable in the UDTObject")]
                HRESULT UdtVar([in] UDTVariable * newVal);   
    };

Lets check the put property first. Most of us know that in the put property we have to pass variables "by value". Here we defined a [in] variable as pointer to UDTVariable. So we pass the variable "by reference". In the C and C++ field we know that this is faster to do so. The same applies to VB and COM. In VB when dealing with types and structures we are forsed to use the byref declaration, no matter which direction the data goes to. It is up to the callee to enforce the integrity of the incoming data, so that when the method returns the input parameter is unchanged.

On the other hand the get property takes an argument of type pointer to pointer. In the beginning it looks right, since a "pointer to pointer" is a reference to a "pointer", and the get property argument type is always declared as the pointer to the put property argument type.

As always when the argument is an out one, the callee is responsible for allocating the memory. This means that we have to call "new UDTVariable" in our get_ method. But VB does not understand pointers. Does it?.


 

The above VB error says that VB can not accept a pointer to pointer in a get method returning a UDT. So we have to alter the get property of our object to accept only a pointer to UDTVariable. Still our method handles the memory allocation for the extra string in the UDT. Let's see it.

VB dimension a UDTVariable
	Allocate memory for the UDT.
	The memory is sizeof( UDTVariable ).
	Pass the variables address to the object.
		Object allocates memory for UDT.Name
		Object initializes the string
		If object.special is not an integral type 
			allocates memory for the type
		set the value of Object.Special.

So our get method is still responsible for allocating memory for the UDTVariable. It just does not allocate the UDTVariable body memory.

So after this we may go to the get method of our interface, and remove one of the "*" characters. Alongside with this modification change the argument names from pVal and newVal to "pUDT". This is a bit more clear for the VB, C++ client app developer since the beginning of autocompletion in the studio environment.

We also want this property to be the default one. Go and replace the id(1) with id(0) in both methods. Our interface now looks like this.

    [
        object,
        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F),
        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),
        dual,
        helpstring("IUDTDemoOb Interface"),
        pointer_default(unique)
    ]
    interface IUDTDemoOb : IDispatch
    {
        [propget, id(0), helpstring("Returns a UDT Variable in the UDTObject")] 
                HRESULT UdtVar([out, retval]        
                   UDTVariable *pUDT);
        [propput, id(0), helpstring("Sets a UDT Variable in the UDTObject")]
                HRESULT UdtVar([in]       
                  UDTVariable *pUDT);
    };

This is not enough though. We have to inform the CUDTDemoOb class for the change in the interface. So go to the header file, remove the "*" from the get_UdtVar method, and since we are there change the name of the argument to "pUDT". Do the same for the ".cpp" file.

Here are the modifications in the CUDTDemoOb class

//CUDTDemoOb.h

    STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT);
    STDMETHOD(put_UdtVar)(/*[in]*/  UDTVariable *pUDT);

//CUDTDemoOb.cpp

    STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT)
    STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT)

We are now ready to compile the project.

So what are these warnings about incompatible automation interface?. ("warning MIDL2039 : interface does not conform to [oleautomation] attribute") You may safely ignore this warning. It is stated in several articles. When the MIDL compiler is upgraded the warning will go away. (well, this may not be right in case the interface is defined insidethe Library block).

We may open the VB project again, and check the object browser. The property is there, declared for our object. There is also a reference to the UDTVariable. This is correct, since now the UDT is implicitly inserted into the UDTDemo library through the IUDTDemoOb interface.

Using the UDTVariable

Lets go back to the UDTDemo library and make it do something useful. First we need a UDTVariable member in the CUDTDemoOb class. So open the header file and add a declaration for a variable.

//CUDTDemoOb.h

protected:
    UDTVariable m_pUDT;

We also have to modify the constructor of our class to initialize the m_pUDT structure. We also need to add a destructor to the class.

//CUDTDemoOb.h

CUDTDemoOb()
{
    CComBSTR str = _T("Unnamed");
    m_pUDT.Value = 0; //default value zero (0)
    m_pUDT.Name = ::SysAllocString( str ); //default name "Unnamed" 
    ::VariantInit( &m_pUDT.Special ); //default special value "Empty"
}

virtual ~CUDTDemoOb()
{
    m_pUDT.Value = 0; //just in case
    ::SysFreeString( m_pUDT.Name ); //free the string memory
    ::VariantClear( &m_pUDT.Special ); // free the variant memory
}

Now it is time we added some functionality into the properties of our class.

Always check for an incoming NULL pointer, when there is a pointer involved. So go into both the get_ and put_properties implementation and add the following.

//CUDTDemoOb.cpp 
If( !pUDT )
    return( E_POINTER );

Now get into the put_UdtVar property method. What we have to do, is assign the members of the incoming variable into the one our object holds. This is easy for the Value member but for the other two, we have to free their allocated memory before assigning the new values. That is why we have selected a string and a variant. So the code will now look like the following.

//CUDTDemoOb.cpp

STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT)
{
    if( !pUDT ) 
        return( E_POINTER );
    if( !pUDT->Name )
        return( E_POINTER );

    m_pUDT.Value = pUDT->Value;  //easy assignment

    ::SysFreeString( m_pUDT.Name );  //free the previous string first
    m_pUDT.Name = ::SysAllocString( pUDT->Name );   //make a copy of the incoming

    ::VariantClear( &m_pUDT.Special );   //free the previous variant first
    ::VariantCopy( &m_pUDT.Special, &pUDT->Special );   //make a copy

    return S_OK;
}

As every great writer says, we remove error checks for clarity :).

You may have noticed that we also check the string Name for null value. We have to. BSTRs are declared as pointers so this field might be NULL. The point is that a NULL pointer is not an empty COM string. An Empty com string is one with zero length.

After the method returns, our object has a copy of the incoming structure, and that is what we wanted to do.

Now forward to the get_UdtVar method. This is the opposite of the previous one. We have to fill in the incoming structure with the values of the internal UDT structure of the object.

We may check the code.

//CUDTDemoOb.cpp

STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT)
{
    if( !pUDT ) 
        return( E_POINTER );

    pUDT->Value = m_pUDT.Value;  //return value

    ::SysFreeString( pUDT->Name );   //free old (previous) name
     pUDT->Name = ::SysAllocString( m_pUDT.Name );  //copy new name

    ::VariantClear( &pUDT->Special );  //free old special value
    ::VariantCopy( &pUDT->Special, &m_pUDT.Special ); //copy new special value

    return  S_OK;
}

The main difference is now that the Name and Special members of the incoming UDT may be NULL and Empty respectively. This is allowed because our object is obliged to fill in the structure. The callee is only responsible for allocating the memory for the UDT itself alone and not its members.

Why do we free the incoming string ?. well, because the callee may pass in an already initialized UDT. TheSysFreeString and VariantClear system methods may handle NULL string pointers and empty variants respectively. Freeing the string may give us errors. In case the method is not called from VB the Name BSTR pointer, may hold anot NULL but invalid pointer (trash). So this would have been

HRESULT hr = ::SysFreeString( pUDT->Name ); //free old (previous) name
if( FAILED( hr ) )
    return( hr );  //if for any reason there is error FAIL

Compile the project, open the VB client project, add a button to the form, and do some checks with assignments there.

Private Sub cmdFirstTest_Click()

    Dim a_udt As UDTVariable  ''define a couple UDTVariables
    Dim b_udt As UDTVariable
    
    Dim ob_udt As New UDTDemoOb ''declare and create a UDEDemoOb object    
    
    a_udt.Name = "Ioannis"  ''initialize one of the UDTS
    a_udt.Value = 10
    a_udt.Special = 15.5
    
    ob_udt.UdtVar = a_udt  ''assign the initialized UDT to the object    
    b_udt = ob_udt.UdtVar   ''assign the UDT of the object to the second UDT
    ''put a breakpoint here and check the result in the immediate window
    
 End Sub              
Now try this.
    b_udt = ob_udt.UdtVar   ''assign the UDT of the object to the second UDT
        ''put a breakpoint here and check the result in the immediate window

    b_udt.Special = b_udt ''it actually makes a full copy of the b_udt
    b_udt.Special.Special.Name = "kostas" ''vb does not use references

ARRAYS OF UDTs

So, we have not seen any arrays so far you may say. It is our next step. We are going to add a method to our interface, which will return an array of UDTs. It will take two numbers as input, start and length, and will return an array of UDTVariables with length items, holding consequent values.

So go to the UDTDemo project, right click on the IUDTDemoOb interface, and select "add method".

In the Dialog, type "UDTSequence" as the name of the method, and add the following as the parameters. "[in] long start, [in] long length, [out, retval] SAFEARRAY(UDTVariable) *SequenceArr". Press [Ok] and lets see what the wizard added into the project for us.

Do not compile now !

Well the definition of the new method has been inserted into the IUDTDemoOb interface.

//udtdemo.idl

    [
        object,
        //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F),
        uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),
        dual,
        helpstring("IUDTDemoOb Interface"),
        pointer_default(unique)
    ]
    interface IUDTDemoOb : IDispatch
    {
        [propget, id(0), helpstring("Returns a UDT Variable in the UDTObject")] 
                HRESULT UdtVar([out, retval] UDTVariable *pUDT);
        [propput, id(0), helpstring("Sets a UDT Variable in the UDTObject")]
                HRESULT UdtVar([in] UDTVariable *pUDT);
        [id(1), helpstring("Return successive named values")] 
            HRESULT UDTSequence([in] long start
                                [in] long length, 
                                [out, retval] SAFEARRAY(UDTVariable) *SequenceArr);
                               
    };

The above is edited a bit so it may be visible here at once. There should not be something we do not know so far. We saw earlier what SAFEARRAY(UDTVariable) is. This is the declaration of a pointer to a SAFEARRAY structure holding UDTVariables. So SequenceArr is really a reference to a SAFEARRAY pointer. Everything is fine so far.

Now lets check the header file of the CUDTDemoOb class.

//udtdemoob.h

public:
    STDMETHOD(UDTSequence)(/*[in]*/ long start, 
                           /*[in]*/ long length, 
                           /*[out, retval]*/ SAFEARRAY(UDTVariable) *SequenceArr);

STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT);
STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT);

At first it looks right. It is not. There is not any macro or something visible to the compiler to understand theSAFEARRAY(UDTVariable) declaration. As we said at the beginning of this document, our code will never have enough type information about the SAFEARRAY structure. The type information for arrays should be checked at run time. So we have to modify the code. Replace SAFEARRAY(UDTVariable) with SAFEARRAY *.

This is how the code should look like.

//udtdemoob.h

public:
    STDMETHOD(UDTSequence)(/*[in]*/ long start, 
                           /*[in]*/ long length, 
                           /*[out, retval]*/ SAFEARRAY **SequenceArr);

STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT);
STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT);

You've probably realized that we have to modify the implementation file of CUDTDemoOb class to correct this problem. Well I was surprised to see that for the first time, the wizard had not even added the declaration of the SequenceArr.

//udtdemoob.cpp

STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length )  //Where is the SafeArray ?
{
    return S_OK;
}

As you see, we have to add the SAFEARRAY **SequenceArr declaration. On the other hand if the SequenceArr was declared just replace is as we did in the header.

//udtdemoob.cpp

STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length, SAFAARRAY **SequenceArr )
{
    return S_OK;
}

Now we may compile the project. Check again the VB client project, in the object browser to see the new method, and that it returns an array of UDTVarables.


  

So return to the implementation of UDTSequence to start adding checking code. First we have to test that the outgoing array variable is not null. The second check is the length variable. It may not be less than or equal to zero.

//udtdemoob.cpp
 
STDMETHODIMP CUDTDemoOb::UDTSequence(long start, longlength, 
                                     SAFEARRAY **SequenceArr)
{
    if( !SequenceArr ) 
        return( E_POINTER );

    if( length <= 0 ) {
        HRESULT hr=Error(_T("Length must be greater than zero") );
        return( hr );
    } 
    
    return S_OK;
}

You may notice the usage of the Error method. This is provided by ATL and is very easy to notify clients for errors without getting into much trouble.

The next step is to check the actual array pointer. The dereferenced one. This is the "*SequenceArr". There are two possibilities at this point. Ether this is NULL, which is Ok since we return the array, or holds some non zero value, where supposing it is an array we clear it and create a new one.

So the method goes on.

//udtdemoob.cpp
 
STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length,
                                     SAFEARRAY **SequenceArr)
{
    if( !SequenceArr ) 
        return( E_POINTER );

    if( length <= 0 ) { 
        HRESULT hr = Error( _T("Length must be greater than zero") );
        return( hr ); 
    }

    if( *SequenceArr != NULL ) {
        ::SafeArrayDestroy( *SequenceArr );
        *SequenceArr = NULL;
    } 
    
    return S_OK;
}

Create The Array

Now we may create a new array to hold the sequence of named variables. Our first thought here is to use the::SafeArrayCreate method, since we do not know what we exactly need. Search the MSDN library and in the documentation we find nothing about UDTs. On the other hand the ::SafeArrayCreateEx method implies it may create an array of Records (UDTs).

As the normal version, this method needs access to a SAFEARRAYBOUND structure, the number of dimensions, the data type, and a pointer to IRecordInfo interface. So, go by the book. a) we need an array of "records" useVT_RECORDb) we need only one (1) dimension, c) we need a zero based array (lbound) with length (cbElements). Ok. This is what we have so far.

SAFEARRAYBOUND rgsabound[1];
rgsabound[0].lLbound = 0;
rgsabound[0].cElements = length;
*SequenceArr = ::SafeArrayCreateEx(VT_RECORD, 1, rgsabound, /*what next ?*/ );

Searching in the MSDN once more, reveals the "::GetRecordInfoFromGuids" method. Actually there are two of them, but this one seemed easier to use for this tutorial. The arguments to this method are,:

  • rGuidTypeLib : The GUID of the type library containing the UDT. In our case UDTDemo library, LIBIID_UDTDemo

  • uVerMajor : The major version number of the type library of the UDT. The version of this library is (1.0). so major version is 1.

  • uVerMinor : The minor version number of the type library of the UDT. Zero (0) in our case

  • lcid : The locale ID of the caller. Usually zero is a default value. Use zero.

  • rGuidTypeInfo : The GUID of the typeinfo that describes the UDT. This is the GUID of UDTVariable, but it is not found anywhere.

  • ppRecInfo : Points to the pointer of the IRecordInfo interface on a RecordInfo object. This pointer we pass to the "::SafeArrayCreateEx" method.

So, go into the IDL file, copy the uuid of the UDTVariable structure and paste it at the beginning of the implementation file. Then make it a formal UUID structure.

So this "C21871A0-33EB-11D4-A13A-BE2573A1120F" becomes

//udtdemoob.cpp

const IID UDTVariable_IID = { 0xC21871A0, 
                              0x33EB,
                              0x11D4, {
                                       0xA1,
                                       0x3A,
                                       0xBE,
                                       0x25,
                                       0x73,
                                       0xA1,
                                       0x12,
                                       0x0F
                                      }
                            };   

now we are ready, to create an uninitialized array of UDTVariable structures. inside the UDTSequence function

    //////
    //here starts the actual creation of the array
    //////
    IRecordInfo *pUdtRecordInfo = NULL;
    HRESULT hr = GetRecordInfoFromGuids( LIBID_UDTDemo,  
                                         1, 0, 
                                         0,
                                         UDTVariable_IID,
                                         &pUdtRecordInfo );
    if( FAILED( hr ) ) {
        HRESULT hr2 = Error( _T("Can not create RecordInfo interface for"
                                "UDTVariable") );
        return( hr ); //Return original HRESULT hr2 is for debug only
    }
    
    SAFEARRAYBOUND rgsabound[1];
    rgsabound[0].lLbound = 0;
    rgsabound[0].cElements =length;

    *SequenceArr = ::SafeArrayCreateEx( VT_RECORD, 1, rgsabound, pUdtRecordInfo );

    pUdtRecordInfo->Release(); //do not forget to release the interface
    if( *SequenceArr == NULL ) {
        HRESULT hr = Error( _T("Can not create array of UDTVariable "
                               "structures") );
        return( hr );
    }
    //////
    //the array has been created
    //////

Now we have created an uninitialized array, and have to put data on it. You may also make tests with VB at this point, to check that the method returns arrays with the expected size. Even without data.

If you get an the HRESULT error code "Element not found" make sure you have typed the UDTVariable_IID correctly.

At this point you should also know that the memory which has been allocated by the system for the array is zero initialized. This means that the Value and Name members are initialized to zero (0) and the Special member is initialized to VT_EMPTY. This is helpful in case we'd like to distinguish between an initialized or not slot in the array.

Add Data into the Array

There are two ways to fill in an array with data. One is to add it one by one, using the ::SafeArrayPutElementmethod, and the other is to use the ::SafeArrayAccessData to manipulate the data a bit faster. In my experience we are going to use the first one when we want to access a single element and the second one when we need to perform calculation in the whole range of the data the array holds.

Safe arrays of structures appear in memory as normal arrays of structures. At first there might be a misunderstanding that in the SAFEARRAY there is record information kept with every item in the array. This is not true. There is only one IRecordInfo or ITypeInfo pointer for the whole array. SAFEARRAYs use a simple old trick. They allocate the memory to hold the SAFEARRAY structure but there is also some more memory allocated to hold the extra pointer if necessary at the begining. This is stated in the MSDN library.

So now we are going to create two internal methods for demonstrating both ways of entering data into the array.

First we'll use the ::SafeArrayPutElement method. In the CUDTDemoOb class declaration, insert the declaration of this method. This method should be declared protected, since it will only be called internally by the class itself.

//udtdemoob.h

protected:
    HRESULT SequenceByElement(long start, long length, SAFEARRAY *SequenceArr);

The only difference from the UDTSequence method is that this one accepts only a pointer to a SAFEARRAY. Not the pointer to pointer used in SAFEARRAY (UDTSequence).

The algorithm to fill the array is really simple. For every UDTVariable in the array, we set successive values starting from start into the Value member of our structure, convert this numerical to BSTR and assign it to the Namemember of the structure. Finally set the value of the Special member to be either of type long or double and assign to it the same numeric value, except that when we use the double version add "0.5" to have different data there.

In the implementation file of our class add the method definition.

//udtdemoob.cpp

HRESULT CUDTDemoOb::SequenceByElement(long start,
                                      long length,
                                      SAFEARRAY *SequenceArr)
{
    return( S_OK );
}

we may skip checking the incoming variables in this method, since these are supposed to be called only inside the class, and the initial checks taken before calling these.

//udtdemoob.cpp

HRESULT CUDTDemoOb::SequenceByElement(long start,
                                      long length,
                                      SAFEARRAY *SequenceArr)
{
    HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound );
    if( FAILED( hr ) ) return( hr );

    return( S_OK );
} 

The first check to be performed is the lower bound of the array. Although we state that we handle zero-based arrays, one may pass a special bounded array. In VB it is easy to get one-based arrays. It is also a way to know we have a valid SAFEARRAY pointer.

The following code makes the conversion from numeric to string, and assigns the string value to the Name member of the a_udt structure.

//udtdemoob.cpp

HRESULT CUDTDemoOb::SequenceByElement(long start,
                                      long length,
                                      SAFEARRAY *SequenceArr)
{
    HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound );
    if( FAILED( hr ) ) 
        return( hr );

    hr = ::VariantChangeType( &a_variant, &a_udt.Special, 0, VT_BSTR );
    hr = ::VarBstrCat( strDefPart, a_variant.bstrVal, &a_udt.Name );

    return( S_OK );
} 

You may see the code in the accompanying project, so we are going to explain the big picture. Inside the loop this line is executed.

//udtdemoob.cpp

HRESULT CUDTDemoOb::SequenceByElement(long start,
                                      long length,
                                      SAFEARRAY *SequenceArr)
{
    HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound );
    if( FAILED( hr ) ) 
        return( hr );

    hr = ::VariantChangeType( &a_variant, &a_udt.Special, 0, VT_BSTR );
    hr = ::VarBstrCat( strDefPart, a_variant.bstrVal, &a_udt.Name );

    hr = ::SafeArrayPutElement( SequenceArr, &i, (void*)&a_udt );

    return( S_OK );
} 

In this line of code, the system adds the a_udt in the ith position of the array. What we have to know is that in this call, the system makes a full copy of the structure we pass in it. The reason the system may perform the full copy is the usage of the IRecordInfo interface we used in the creation of the array. As a result we have to release the memory held by any BSTR or VARIANT we use. In our situation we only release the a_variant variable since this holds the reference of the only resource allocated string.

Let's move to the ::SafeArrayAccessData method and check out the differences. The first change, is that now we use a pointer to UDTVariable p_udt. The second big difference is that inside the loop there is only code to set the members of the structure, through the pointer. The only actual code to access the array is outside the loop with the methods to access and release the actual memory the data resides to. There is also one more check inside the loop

//udtdemoob.cpp
    //....
    if( p_udt->Name ) 
        ::SysFreeString( p_udt->Name );
    //....

This is to demonstrate that since we access the data without any other interference we have to release any memory allocated for a BSTR string, a VARIANT or even an interface pointer before assigning data to it. As it was pointed before, checking for the NULL value might be adequate for this simple demonstration.

I hope it is obvious that it is better calling the second method - ::SafeArrayAccessData - when there is need to access all or most of the data in the array, but might also be appropriate to use the the ::SafeArrayGetElement and::SafeArrayPutElement pair of methods if you want to modify one or two elements at a time.

As a final step insert the following lines at the end of the body of the UDTSequence method, and test it with the VB client project. You may comment out which ever you like to see how it works, and that they both give the same results.

//   hr = SequenceByElement( start, length, *SequenceArr );
     hr = SequenceByData( start, length, *SequenceArr );

Static Arrays

Our method presents a fault in the design. It may only return a dynamically created array. This means the array is created on the heap. Try adding the following lines in VB and check this out.

    dim a_udt_arr(5) as UDTVariable
    dim a_udt_ob     as UDTDemoOb

    a_udt_arr = UDTDemoOb.UDTSequence(15, 5) ''Error here

Well, conformant arrays, I think this is what they call them, are only available as [in] arguments in this demo. So for the moment add one more check to our UDTSequence method. The other problem is that arrays are always passed as doubly referenced pointers.

So let's try out a modify the array approach.

Add one more property to the interface

Call it Item like in collections. The signature will be

//udtdemoob.idl

    [propput, .....]
    Item( [in] long index, 
          [in] SAFEARRAY(UDTVariable) *pUDTArr,
          [in] UDTVariable *pUDT );
    [propget, .....]
    Item( [in] long index,
          [in] SAFEARRAY(UDTVariable) *pUDTArr,
          [out, retval] UDTVariable *pUDT );

The reason we add this, is to demonstrate some checks for the incoming arrays. As you may have guessed by the method definition, arrays although defined as [in] are still modifiable in every way. Our first check is to see if it is an array of UDTVariable structures. Since this check is performed in at least two methods, we may put it in its own protected function inside the object implementation class.

As you have noticed, our object still does not keep any state about the incoming arrays.

HRESULT IsUDTVariableArray( SAFEARRAY *pUDTArr, bool &isDynamic )

The only difference in what you might expect is the bool reference at the end of the declaration. Well, this check function will be able to inform us if a) we may actually modify the array, (append or remove items by reallocating the memory, or even destroy and recreate the array), b) we may only modify individual UDTVariable structures inserted in the array. The former feature will not be implemented in the demonstrating project.

Our first check is the number of dimensions of the incoming array. We want this to be one dimensioned. After reading the tutorial you may expand this to multidimensional arrays although there is a slight issue.

long dims = SafeArrayGetDim( pUDTArr );
if( dims != 1 ) {
    hr = Error( _T("Not Implemented for multidimentional arrays") );
    return( hr );  
}

the next step is to check that the array is created so as to hold structures. This is easily done by checking that the features flag of the incoming array indicates records support.

unsigned short feats = pUDTArr->fFeatures; //== 0x0020;
if( (feats & FADF_RECORD) != FADF_RECORD ) {
    hr = Error( _T("Array is expected to hold structures") );
    return( hr );
}

Final check is to compare the name of the structure the array holds with ours. To do this we have to get access to the IRecordInfo interface pointer the array holds.

IRecordInfo *pUDTRecInfo = NULL;
hr = ::SafeArrayGetRecordInfo( pUDTArr, &pUDTRecInfo );
if( FAILED( hr ) &&  !pUDTRecInfo )
    return( hr );

Now do the comparing.

BSTR  udtName = ::SysAllocString( L"UDTVariable" );
BSTR  bstrUDTName = NULL; //if not null. we are going to have problem
hr = pUDTRecInfo->GetName( &bstrUDTName);
if( VarBstrCmp( udtName, bstrUDTName, 0, GetUserDefaultLCID()) != VARCMP_EQ ) {
    ::SysFreeString( bstrUDTName );
    ::SysFreeString( udtName );
    hr = Error(_T("Object Does Only support [UDTVariable] Structures") ); 
    
    return( hr );
}

In the accompanying project there are also some more checks as demonstration, which are available only through the debugger. Implementing the Item property is straightforward after this.

Using VARIANTS

I do not think this is enough so far, as we have not discussed using our structure with variants. So let's add one more property to our object. Add the following definition to our interface.

HRESULT VarItem([in] long items, [out, retval]
                 LPVARIANT pUdtData );

Now go to the definition of the new property in the implementation file of the CUDTDemoOb class and let's do something.

First some checks. The usual check for the null pointer, and then check if the VARIANT contains any data. If it is not empty we should clear it.

    if( !pUdtData )
        return( E_POINTER );

    if( pUdtData->vt != VT_EMPTY )
            ::VariantClear( pUdtData );

The next step is to implement the algorithm which is to return a) a single UDTVariable structure if the item variable is equal or less than one (1). b) an array of structures if item is larger than one (1).

In both situations we have to set the type of the outgoing VARIANT to VT_RECORD, and this is the only similarity in accessing the VARIANT pUdtData variable. For the single UDTVariable structure, we have to set the pRecInfo member of the VARIANT to a valid IRecordInfo interface pointer. This has been demonstrated earlier. Then assign the new structure to the pvRecord member of the variant. Returning an array on the other hand, we must update the type of the outgoing VARIANT to be of type VT_ARRAY as well. Then we just assign an already constructed array to the parray member of the variant. Both the assignments are easily done, since we have already implemented appropriate properties and methods in our object.

if( items <= 1 ) { 
    IRecordInfo *pUdtRecordInfo = NULL;
    hr = ::GetRecordInfoFromGuids( LIBID_UDTDemo, 
                                   1, 0, 
                                   0, 
                                   UDTVariable_IID, 
                                   &pUdtRecordInfo );
    if( FAILED( hr ) ) { 
        HRESULT hr2= Error( _T("Can not create RecordInfo" 
                            "interface for UDTVariable") );
        return( hr );
    } //assign record information on the variant

    pUdtData->pRecInfo = pUdtRecordInfo;
    pUdtRecordInfo = NULL;  //MIND. we do not release the interface.
                            //VariantClear should 
    
    pUdtData->vt = VT_RECORD; 
    pUdtData->pvRecord= NULL; 
    hr= get_UdtVar( (UDTVariable*) &(pUdtData->pvRecord) ); 

} else  {

    //here the valid pointer of the union is the array.
    //so the array holds the record info.

    pUdtData->vt = VT_RECORD | VT_ARRAY;
    hr = UDTSequence(1, items, &(pUdtData->parray) );
}

I think this is enough for a basic tutorial on UDT's with COM. There is no interface defined to access the second typeUDTArray defined in the type library, but this should be straightforward at this moment (I tricked you :) ). In the demo project, I've explicitly added the structure in the library body, so you can play with this in VB.

"Safe Arrays" in EVENTS

I've also said that there is a flaw in the code created by the wizard for the interfaces creates to pass any kind of arrays back. This is partially been taken care of with the implementation of the VarItem method. An event method is demonstrated in the project. Here is what has been changed in the generated method.

Supposing that not many of us have used events in the controls, I am going to be a bit more specific on this.

Let's begin the journey to ConnectionPoints. First we have to add a method to the IUDTDemoObEvents interface. Here is the signature of this method. So far you have the knowledge to understand the signature of this method. Additionally only the UDTDemo.idl has changed so far.

[id(1), helpstring
 ("Informs about changes in an array of named vars")] 
     HRESULT ChangedVars(SAFEARRAY(UDTVariable) *pVars);


 

Now compile once more the project, and check the Object Browser in the VB client. You may see the event declared in the object.


 

Now where the project is compiled, and the UDTDemo type library is updated, we may update the CUDTDemoObclass to use the IUDTDemoObEvents interface. In the project window, right click on the CUDTDemoOb class, and from the popup menu select Implement connection point.


 

In the following dialog box, select the (check on it) _IUDTDemoObEvents interface and press [ok].


 

The wizard has now added one more file into the project. "UDTDemoCP.h" in which theCProxy_IUDTDemoEvents< class T > template class is implemented, and handles the event interface of the UDTDemoOb coclass object. The CUDTDemoOb class is now deriving from the newly generated proxy class.

The proxy class holds the Fire_ChangedVars method, which is implemented and we can call it from any point of our class to fire the event.

So let's go to the implementation of the UDTSequence method just for the demonstration and fire the event.

//UDTDemoOb.cpp  - UDTSequence method

    //hr = SequenceByElement( start, length, *SequenceArr );
    hr =  SequenceByData( start, length, *SequenceArr);
    
    return Fire_ChangedVars( SequenceArr  );
    //<<----  changed here //return S_OK;      

Now compile the project, and watch the output.

    warning C4800: 
    'struct tagSAFEARRAY ** ' : forcing value to bool 'true' or 'false'
    (performance warning)

This is not really a warning. This is an implementation error and causes runtime problems. Let's see just for the demonstration of it. Open the VB Client again and add the following in the declarations of the demo form. I hope you know what the WithEvents keyword means.

    Dim WithEvents main_UDT_ob As UDTDemoOb

Update the following as well

Private Sub Form_Load()

    Set main_UDT_ob = New UDTDemoOb

End Sub


Private Sub Form_Unload(Cancel As Integer)

    Set main_UDT_ob = Nothing

End Sub

Private Sub main_UDT_ob_ChangedVars(pVars() As UDTDemo.UDTVariable)

    Debug.Print pVars(1).Name, pVars(1).Special, pVars(1).Value

End Sub

Set a breakpoint in the debug statement of the event handler and run the client. See what we get.


 

And in stand alone execution we get


 

Well, the actual error is the following and should be the expected error since we know the warning. This was discovered in the VC++ debugger as the return HRESULT of the Invoke method.

0x80020005 == Type Mismatch

It's time we checked the code the wizards generated for us.

HRESULT Fire_ChangedVars(SAFEARRAY * * pVars)
{
    CComVariant varResult;
    T* pT = static_cast<T*>(this);
    int nConnectionIndex;
    CComVariant* pvars = new CComVariant[1];
    int nConnections = m_vec.GetSize();
        
    for( nConnectionIndex = 0; nConnectionIndex < nconnections; nConnectionIndex++) { 
        pT->Lock();
        CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
        pT->Unlock();
        IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
        if (pDispatch != NULL)
        {
            VariantClear(&varResult);
            pvars[0] = pVars;
            DISPPARAMS disp = { pvars, NULL, 1, 0 };
            pDispatch->Invoke( 0x1, 
                               IID_NULL,
                               LOCALE_USER_DEFAULT, 
                               DISPATCH_METHOD,
                               &disp,
                               &varResult,
                               NULL, NULL);  
        }
    }
    delete[] pvars;
    return varResult.scode;
}

lets check the trouble lines.

    CComVariant* pvars = new CComVariant[1];
    int nConnections = m_vec.GetSize();

This logically assumes that there might be more than one clients connected with the instance of our object. But no error check means that at least one client is expected to be connected. This is wizard code so it should perform some checks. We are not expected to know every detail of the IConnectionPointImpl ATL class.

    int nConnections = m_vec.GetSize();
    if( !nConnections ) 
        return S_OK;

    CComVariant* pvars = new CComVariant[1];

Of course I'm exaggerating, but this is my way of doing such things.

This final line, incorrectly assumes that there is only one client connected to our object. Each time Invoke is called inside the loop, the varResult variable is set to the return value of the method being invoked. Neither varResult is being checked for returning any error code, neither the return value of the Invoke method itself, which in our project gave the right error. So as is, calling the event method, will succeed or fail depending on notifying the last object connected with our UDTDemoOb object. Consider using a Single Instance Exe Server with clients connected on it !

    pDispatch->Invoke( 0x1, .. .

    return varResult.scode;

this is not to blame anyone, since if we'd like per connection error handling we should make it ourselves. Just remember that you have to take care of it depending on the project.

The Actual Problem

pvars[0] = pVars;

CComVariant does not handle arrays of any kind. But since it derives directly from the VARIANT structure it is easy to modify the code to do the right thing for us. We used VARIANTs earlier so you may try it yourselves first.

    //pvars[0] = pVars;
    
    pvars[0].vt = VT_ARRAY | VT_BYREF | VT_RECORD;
    pvars[0].pparray = pVars;

To pass any kind of array with a VARIANT you just have to define the VT_Type of the array, or'd with the VT_ARRAYtype. The only difference from our previous example is that here we use the VT_BYREF argument as well. This is necessary since we have a pointer to pointer argument. Of course byref in VB means we use the "pparray"member of the variant union. For an array holding strings it would be

pvars[0].vt = VT_ARRAY | VT_BSTR; //array to strings
pvars[0].parray = ...

pvars[0].vt = VT_ARRAY | VT_BYREF | VT_BSTR; //pointer to array to strings
pvars[0].pparray = ...

Again, although we deal with an array holding UDT structures we do not have to set an IRecordInfo interface inside the variant.

Compile the project and try this out. Do not fear unless you change the idl file of the project the code does not change. This is the reason we first define all methods in the event (sink) interface and then implement the connection point interface in our object.

Final Note

As most of you may have noticed this has been written quite some time ago. The reason it is posted at this moment is that I had to use user defined structures (UDTs) for a demo project I work on, and this article was really helpful during its implementation. So I hope it is worth reading and helpful to the developer community as well.

References:

MSDN Library: 

Platform SDK \Component Services \ COM \ Automation \ User Defined Data Types. Extending Visual Basic with C++ DLLs, by Bruce McKinney. April 1996

MSJ magazine: 

Q&A ActiveX / COM, by Don Box. MSJ November 1996 Underastanding Interface Definition Language: A Developer's survival guide, by Bill Hludzinski MSJ August 1998.

Books: 

Beginning ATL COM Programming, by Richard Grimes, George Reilly, Alex Stockton, Julian Templeman, Wrox Press, ISBN 1861000111 Professional ATL COM Programming, by Richard Grimes. Wrox Press. ISBN 1861001401