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.
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.
import "oaidl.idl";
import "ocidl.idl";
[
uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F),
version(1.0),
helpstring("UDTDemo 1.0 Type Library")
]
library UDTDemo
{
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.
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID )
{
if (dwReason == DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap, hInstance, &LIBID_UDTDemo);
DisableThreadLibraryCalls(hInstance);
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE;
}
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.
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.
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.
library UDTDemo
{
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".
Then type "UDTDemoOb" as a short name in the "ATL object wizard properties".
We may use what ever name we like, but we have to avoid using "UDTDemo" as it collides
with the library name.
Then as I may always suggest, in the attributes tab, check the "support IsupportErrorInfo"
choice, leave it apartment threaded, but as it dawned on
me right now, check the "Support Connection points" on the dialog as well.
Pressing "ok" now the wizard will create two interfaces and a coclass
object for as in the IDL file, and a class to implement the UDTDemoOb interface.
We checked the support for connection points, because when we use the proxy code
generator for connection point interfaces, the code is far from right in the first
place, when any of the parameters is of array type. It gives a slight warning
about booleans, and compiles the wrong code. So we have to see it as well.
At this point, as It is mentioned at the beginning of this document, I am going to
replace the wizard generated UUIDs. the rest of you may compile the
project or check this with me.
You may skip this if you like
Do not compile the project yet.
First copy the library UUID and paste it above every UUID defined for a) the
IUDTDemoOb interface, b) the _IUDTDemoObEvents events interface and c)
the UDTDemo coclass. While you copy the UUID, you may comment out the wizard generated ones.
Then starting with the above stated order increase by one the first part of the
library interface, for each new occurrence. Parts of the code will look like this.
[
object,
uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F),
dual,
helpstring("IUDTDemoOb Interface"),
pointer_default(unique)
]
interface IUDTDemoOb : IDispatch
{
};
[
uuid(C21871A3-33EB-11D4-A13A-BE2573A1120F),
helpstring("_IUDTDemoObEvents Interface")
]
dispinterface _IUDTDemoObEvents
{
properties:
methods:
};
[
uuid(C21871A4-33EB-11D4-A13A-BE2573A1120F),
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.
HKCR
{
UDTDemo.UDTDemoOb.1 = s 'UDTDemoOb Class'
{
CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}'
}
UDTDemo.UDTDemoOb = s 'UDTDemoOb Class'
{
CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}'
CurVer = s 'UDTDemo.UDTDemoOb.1'
}
NoRemove CLSID
{
ForceRemove { CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F } = s 'UDTDemoOb Class'
{
ProgID = s 'UDTDemo.UDTDemoOb.1'
VersionIndependentProgID = s 'UDTDemo.UDTDemoOb'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{C21871A1-33EB-11D4-A13A-BE2573A1120F}'
}
}
}
End of skip area
Compile the project. Make sure everything is ok. If we check the project with the VB client
at this point, we will only see the UDTDemo object appear in the object
browser. This is correct.
So lets go on and add a property to our object. Using the property wizard add a
property named UdtVar, accepting a pointer to a UDTVariable. We'll get
later to the pointer thing. The UDTVariable is not in the type list of the
dialog, so we have to manually add it. Check the picture bellow.
This is how our interface looks like after pressing the [Ok] button.
[
object,
uuid(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(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
STDMETHOD(get_UdtVar)( UDTVariable *pUDT);
STDMETHOD(put_UdtVar)( UDTVariable *pUDT);
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.
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()
{
CComBSTR str = _T("Unnamed");
m_pUDT.Value = 0;
m_pUDT.Name = ::SysAllocString( str );
::VariantInit( &m_pUDT.Special );
}
virtual ~CUDTDemoOb()
{
m_pUDT.Value = 0;
::SysFreeString( m_pUDT.Name );
::VariantClear( &m_pUDT.Special );
}
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.
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.
STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT)
{
if( !pUDT )
return( E_POINTER );
if( !pUDT->Name )
return( E_POINTER );
m_pUDT.Value = pUDT->Value;
::SysFreeString( m_pUDT.Name );
m_pUDT.Name = ::SysAllocString( pUDT->Name );
::VariantClear( &m_pUDT.Special );
::VariantCopy( &m_pUDT.Special, &pUDT->Special );
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.
STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT)
{
if( !pUDT )
return( E_POINTER );
pUDT->Value = m_pUDT.Value;
::SysFreeString( pUDT->Name );
pUDT->Name = ::SysAllocString( m_pUDT.Name );
::VariantClear( &pUDT->Special );
::VariantCopy( &pUDT->Special, &m_pUDT.Special );
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 );
if( FAILED( hr ) )
return( hr );
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.
[
object,
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.
public:
STDMETHOD(UDTSequence)( long start,
long length,
SAFEARRAY(UDTVariable) *SequenceArr);
STDMETHOD(get_UdtVar)( UDTVariable *pUDT);
STDMETHOD(put_UdtVar)( 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.
public:
STDMETHOD(UDTSequence)( long start,
long length,
SAFEARRAY **SequenceArr);
STDMETHOD(get_UdtVar)( UDTVariable *pUDT);
STDMETHOD(put_UdtVar)( 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.
STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length )
{
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.
STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length, SAFAARRAY **SequenceArr )
{
return S_OK;
}
Now we may compile the project. Check again the VB client project, in the object
browser to see the new method, and that it returns an array of UDTVarables.
So return to the implementation of UDTSequence to start adding checking code.
First we have to test that the outgoing array variable is not null. The second
check is the length variable. It may not be less than or equal to zero.
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.
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, );
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
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
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 );
}
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].lLbound = 0;
rgsabound[0].cElements =length;
*SequenceArr = ::SafeArrayCreateEx( VT_RECORD, 1, rgsabound, pUdtRecordInfo );
pUdtRecordInfo->Release();
if( *SequenceArr == NULL ) {
HRESULT hr = Error( _T("Can not create array of UDTVariable "
"structures") );
return( hr );
}
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.
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.
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.
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.
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.
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
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 = 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
[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;
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;
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 );
}
pUdtData->pRecInfo = pUdtRecordInfo;
pUdtRecordInfo = NULL;
pUdtData->vt = VT_RECORD;
pUdtData->pvRecord= NULL;
hr= get_UdtVar( (UDTVariable*) &(pUdtData->pvRecord) );
} else {
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);
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 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.
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 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.
hr = SequenceByData( start, length, *SequenceArr);
return Fire_ChangedVars( SequenceArr );
Now compile the project, and watch the output.
warning C4800:
'struct tagSAFEARRAY ** ' : forcing value to bool 'true' or 'false'
(performance warning)
This is not really a warning. This is an implementation error and causes
runtime problems. Let's see just for the demonstration of it. Open the VB Client
again and add the following in the declarations of the demo form.
I hope you know what the WithEvents keyword means.
Dim WithEvents main_UDT_ob As UDTDemoOb
Update the following as well
Private Sub Form_Load()
Set main_UDT_ob = New UDTDemoOb
End Sub
Private Sub Form_Unload(Cancel As Integer)
Set main_UDT_ob = Nothing
End Sub
Private Sub main_UDT_ob_ChangedVars(pVars() As UDTDemo.UDTVariable)
Debug.Print pVars(1).Name, pVars(1).Special, pVars(1).Value
End Sub
Set a breakpoint in the debug statement of the event handler and run the client.
See what we get.
And in stand alone execution we get
Well, the actual error is the following and should be the expected error since we
know the warning. This was discovered in the VC++ debugger as the return HRESULT
of the Invoke method.
0x80020005 == Type Mismatch
It's time we checked the code the wizards generated for us.
HRESULT Fire_ChangedVars(SAFEARRAY * * pVars)
{
CComVariant varResult;
T* pT = static_cast<T*>(this);
int nConnectionIndex;
CComVariant* pvars = new CComVariant[1];
int nConnections = m_vec.GetSize();
for( nConnectionIndex = 0; nConnectionIndex < nconnections; nConnectionIndex++) {
pT->Lock();
CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
pT->Unlock();
IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
if (pDispatch != NULL)
{
VariantClear(&varResult);
pvars[0] = pVars;
DISPPARAMS disp = { pvars, NULL, 1, 0 };
pDispatch->Invoke( 0x1,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
&disp,
&varResult,
NULL, NULL);
}
}
delete[] pvars;
return varResult.scode;
}
lets check the trouble lines.
CComVariant* pvars = new CComVariant[1];
int nConnections = m_vec.GetSize();
This logically assumes that there might be more than one clients connected with
the instance of our object. But no error check means that at least one client is
expected to be connected. This is wizard code so it should perform some checks.
We are not expected to know every detail of the IConnectionPointImpl ATL class.
int nConnections = m_vec.GetSize();
if( !nConnections )
return S_OK;
CComVariant* pvars = new CComVariant[1];
Of course I'm exaggerating, but this is my way of doing such things.
This final line, incorrectly assumes that there is only one client connected to our
object. Each time Invoke is called inside the loop, the varResult variable is set
to the return value of the method being invoked. Neither varResult is being checked
for returning any error code, neither the return value of the Invoke method itself,
which in our project gave the right error. So as is, calling the event method,
will succeed or fail depending on notifying the last object connected with our
UDTDemoOb object. Consider using a Single Instance Exe Server with clients
connected on it !
pDispatch->Invoke( 0x1, .. .
return varResult.scode;
this is not to blame anyone, since if we'd like per connection error handling we should
make it ourselves. Just remember that you have to take care of it depending on the
project.
The Actual Problem
pvars[0] = pVars;
CComVariant does not handle arrays of any kind. But since it derives directly
from the VARIANT structure it is easy to modify the code to do the right thing for us.
We used VARIANTs earlier so you may try it yourselves first.
pvars[0].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;
pvars[0].parray = ...
pvars[0].vt = VT_ARRAY | VT_BYREF | VT_BSTR;
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