Link:
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.IDLimport "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.IDLimport "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.cppextern "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.IDLlibrary 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 likeDo 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.rgsHKCR{ 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.hprotected: 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.hCUDTDemoOb(){ 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.cppSTDMETHODIMP 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.cppSTDMETHODIMP 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) nameif( 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 SubNow 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.hpublic: 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.hpublic: 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.cppSTDMETHODIMP 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.cppSTDMETHODIMP 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_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.cppconst 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.hprotected: 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.cppHRESULT 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.cppHRESULT 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.cppHRESULT 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.cppHRESULT 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 problemhr = 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 UDTDemoObEnd SubPrivate Sub Form_Unload(Cancel As Integer) Set main_UDT_ob = NothingEnd SubPrivate Sub main_UDT_ob_ChangedVars(pVars() As UDTDemo.UDTVariable) Debug.Print pVars(1).Name, pVars(1).Special, pVars(1).ValueEnd 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(this); int nConnectionIndex; CComVariant* pvars = new CComVariant[1]; int nConnections = m_vec.GetSize(); for( nConnectionIndex = 0; nConnectionIndex < nconnections; nConnectionIndex++) { pT->Lock(); CComPtr sp = m_vec.GetAt(nConnectionIndex); pT->Unlock(); IDispatch* pDispatch = reinterpret_cast (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 stringspvars[0].parray = ...pvars[0].vt = VT_ARRAY | VT_BYREF | VT_BSTR; //pointer to array to stringspvars[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 1996MSJ 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 1861001401License
This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.
A list of licenses authors might use can be found