Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / ATL
Article

Using User Defined Types in COM & ATL

Rate me:
Please Sign up or sign in to vote.
4.81/5 (43 votes)
17 Jan 2001CPOL 436K   2.5K   138   111
A Step by Step tutorial on SAFEARRAYs and UDTs in COM
  • Download source files - 35 Kb
  • Preface

    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 the Value 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 the typedef 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 and coclass 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, and comment 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".


    Image 1

    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.


    Image 2


    Image 3

    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.


    Image 4

    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?.


    Image 5

    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 inside the 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. The SysFreeString 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 a not 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 the SAFEARRAY(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.


    Image 6  

    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" use VT_RECORD, b) 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 ::SafeArrayPutElement method, 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 Name member 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 type UDTArray 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);


    Image 7

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


    Image 8

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


    Image 9

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


    Image 10

    The wizard has now added one more file into the project. "UDTDemoCP.h" in which the CProxy_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.


    Image 11

    And in stand alone execution we get


    Image 12

    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_ARRAY type. 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

    License

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


    Written By
    Web Developer
    Greece Greece
    This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

    Comments and Discussions

     
    Generalchange in content Pin
    anandssunku2-May-10 5:21
    anandssunku2-May-10 5:21 
    GeneralRe: change in content Pin
    ehaerim22-Dec-12 10:43
    ehaerim22-Dec-12 10:43 
    QuestionHow to use "map" or "list" in UDT? Pin
    ehaerim20-Dec-09 9:21
    ehaerim20-Dec-09 9:21 
    QuestionSending a single UDT as an event parameter. (VS2008) Pin
    Billy Howell11-Mar-09 4:22
    Billy Howell11-Mar-09 4:22 
    AnswerRe: Sending a single UDT as an event parameter. (VS2008) Pin
    Billy Howell5-Jun-09 7:26
    Billy Howell5-Jun-09 7:26 
    GeneralRe: Sending a single UDT as an event parameter. (VS2008) Pin
    karlgilkey20-Oct-09 5:32
    karlgilkey20-Oct-09 5:32 
    GeneralRe: Sending a single UDT as an event parameter. (VS2008) Pin
    Gary C Metalle17-Aug-11 16:19
    Gary C Metalle17-Aug-11 16:19 
    GeneralRe: Sending a single UDT as an event parameter. (VS2008) Pin
    ehaerim22-Dec-12 10:49
    ehaerim22-Dec-12 10:49 
    Question1 client but 4 connections? Pin
    ehaerim6-Oct-08 3:41
    ehaerim6-Oct-08 3:41 
    GeneralAbout your way of modifying UUIDs Pin
    ehaerim29-Aug-08 8:09
    ehaerim29-Aug-08 8:09 
    AnswerRe: About your way of modifying UUIDs Pin
    ioannhs_s31-Aug-08 22:03
    ioannhs_s31-Aug-08 22:03 
    Questionuuid not available from .tlh file. Why? and How to fix? Pin
    ehaerim25-May-08 8:00
    ehaerim25-May-08 8:00 
    AnswerRe: uuid not available from .tlh file. Why? and How to fix? Pin
    ioannhs_s30-Jun-08 2:53
    ioannhs_s30-Jun-08 2:53 
    GeneralGood Article Pin
    Logan from Singapore27-Dec-07 19:52
    Logan from Singapore27-Dec-07 19:52 
    Generalstruct type redefinition Pin
    Joseph Franklin S11-Sep-07 1:40
    professionalJoseph Franklin S11-Sep-07 1:40 
    QuestionDlls Pin
    Member 374096222-Jan-07 20:16
    Member 374096222-Jan-07 20:16 
    QuestionHow Could i pass the default parameters in the ATL Methods Pin
    Rachana Agrawal31-Aug-06 1:25
    Rachana Agrawal31-Aug-06 1:25 
    QuestionIConnectionPoint in Local Servers of ATL/COM Pin
    nareshkavali10-Aug-06 1:10
    nareshkavali10-Aug-06 1:10 
    GeneralPassing UDT having methods from .NET Pin
    Diana Fernandez24-Nov-05 19:49
    Diana Fernandez24-Nov-05 19:49 
    GeneralIt is long life Pin
    yinguangbo8-Nov-05 16:45
    yinguangbo8-Nov-05 16:45 
    QuestionElement Not Found Pin
    cweed1-Sep-05 1:00
    cweed1-Sep-05 1:00 
    Questionwhat a guy! Pin
    tpeter568626-Aug-05 11:02
    tpeter568626-Aug-05 11:02 
    GeneralDeployment of ATL using UDTs with a VB EXE Pin
    Niraj Vatvani5-Aug-05 5:28
    Niraj Vatvani5-Aug-05 5:28 
    Firstly, a hearty applause for ioannhs_s regarding this excellent piece of work. I am a complete newbie to ATL and I used this tutorial as a crash course, without any issues regarding its development. The one issue I do have is regarding its deployment on machines other than the one used to develop this.

    Here is what I have:
    1. A Win2K machine
    2. A VB6 EXE that uses this ATL

    The good news is that all works well in the machine used to develop the ATL and the VB6 EXE.

    The trouble is that the VB6 EXE "refuses to start" after installing the ATL DLL in a different machine. Actually, I got around this by hacking the way VB6 develops its EXE (specifically, the intermediate OCA file that gets generated after registering the ATL DLL in the VB6 env), and also making sure the OCA generated by VB6 is also copied to the non-development machine. I don't know why the OCA is very important, but the VB6 app just does not want to work if it isn't there. Furthermore, I had to manipulate 3 registry entries, all relating to a typelib 32 character uuid entry that VB6 generates randomly and references it in the OCA, for example:
    [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\TypeLib\{E33E3541-49E7-4819-972A-EAF3846BC65F}

    Only after doing these drastic manipulations does the VB6 EXE work on the non-development machine. The non-development machines I have tested this procedure on is using WinXP and Win2K.

    So, my question is:
    Is there a "legal" and "correct" method to deploy a VB6 EXE using this ATL without hacking VB6's OCA and also the registry?

    /*****************************/
    /*******START SIGNATURE*******/
    At work, never ever be irreplaceable. If you cannot be replaced, you can't be promoted!;P But then again, promotions are for people still striving to reach their level of incompetence! :->
    /********END SIGNATURE********/
    /*****************************/
    QuestionWhy can't pass a array of UDT into COM from VB ? Pin
    JERKII.SHANG24-May-05 14:16
    JERKII.SHANG24-May-05 14:16 
    QuestionCan't add more than 1 structure? Pin
    darkaeon7-Mar-05 15:09
    darkaeon7-Mar-05 15:09 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.