Developers Workshop to COM and ATL 3.0
| < Free Open Study > |
|
The only way a COM client and COM object can communicate is through an interface. As we have seen, COM interface methods may take combinations of [in], [out], [in, out], and [out, retval] parameters. When using the [out, retval] attribute, however, you cannot create a COM interface that looks like the following:
// A given method can only have a single [out, retval]. // Even more restrictive, it must be the last parameter in the method. [object, uuid(1626EA2F-5F3C-11D3-B928-0020781238D4), dual] interface IFeebleAttempt : IDispatch { [id(1)] HRESULT TakeThisThatAndTheOther ([out, retval] BSTR* msgOne, [out, retval] BSTR* msgTwo, [out, retval] BSTR* msgThree); [id(2)] HRESULT RetValInTheMiddle([in] int x, [out, retval] BSTR* msg, [in] int y); };
By law, a given COM interface method can have exactly one [out, retval], which must be attributed to the final parameter in the function. Therefore, while [out, retval] is great for sending back a single value to the COM client, most COM applications have a need to send back what could be referred to as "bulk data transfer." One approach is to configure a given method that contains a number of [out] only parameters. In this way, a client is able to fetch a number of logical values in a single call. Be aware, however, that not all COM language mappings support [out] only parameters. Thus, to begin this chapter we will investigate some common bulk data transfer techniques before diving into COM enumerations and COM collections.
Defining IDL Arrays as Method Parameters
On the CD Your companion CD includes the BulkServer ATL project (and a VB test client) that illustrate the techniques we are about to examine. Feel free to load it up and follow along.
The first trick a COM developer may employ to transfer bulk data between a client and coclass is to configure a method making use of an array of IDL base types. In this way, a COM client may send or retrieve a batch of items in a single round trip. Of course, the items in the array must be of the same type.
Assume you wish to define a method that takes a fixed array of BSTRs that may be received (and altered) by the coclass. A fixed size array may be specified in IDL using the [size_of()] attribute. Parameters that may be changed by the coclass are marked as [in, out]. If we have a standard vTable interface named IStringArray, the UseTheseStrings() method may be defined and implemented as so:
// This method takes some number of strings, which may be changed by the coclass. [ object, uuid(1626EA2F-5F3C-11D3-B928-0020781238D4)] interface IStringArray : IUnknown { HRESULT UseTheseStrings([in] short size, [in, out, size_is(size)] BSTR names[]); }; // First show the current strings in the array and then change them. STDMETHODIMP CCoStringArray::UseTheseStrings(short size, BSTR names[]) { USES_CONVERSION; // Show each BSTR in a message box. for(int i = 0; i < size; i++) { MessageBox(NULL, W2A(names[i]), "Message from the client!", MB_OK | MB_SETFOREGROUND); // Now change them. SysReAllocString(&(names[i]), L"This is different..."); } return S_OK; }
A Visual Basic client may exercise UseTheseStrings() as the following:
' Create some strings and send in for processing. ' Private Sub btnStrings_Click() Dim o As New CoStringArray ' Initial string array. Dim strs(3) As String strs(0) = "Hello" strs(1) = "there" strs(2) = "from" strs(3) = "VB..." o.UseTheseStrings 4, strs(0) ' Note calling syntax! ' See how they were changed. Dim i As Integer For i = 0 To 3 MsgBox strs(i), , "New String from coclass" Next i End Sub
This approach has a few drawbacks. First of all, this type of array is not variant compliant. If we were to add the [oleautomation] attribute to the IStringArray IDL declaration and recompile, we are saddened to see warning MIDL2039 : interface does not conform to [oleautomation] attribute. One solution to this problem is to rework IStringArray to use a variant compliant SAFEARRAY.
SAFEARRAYs as Method Parameters
To create an automation compatible array of types, you need to use the variant compliant SAFEARRAY structure. As you recall from the previous chapter, a SAFEARRAY resolves to an array of VARIANT types. Assume we have the following dual interface, ICoSafeArray:
// This interface can return a SAFEARRAY as well as receive a SAFEARRAY. [ object, uuid(E19D07B3-5FBF-11D3-B929-0020781238D4), dual ] interface ICoSafeArray : IDispatch { [id(1)] HRESULT UseThisSafeArrayOfStrings([in] VARIANT strings); [id(2)] HRESULT GiveMeASafeArrayOfStrings([out, retval] VARIANT* pStrings); };
When implementing this interface, we might first turn our attention to the UseThisSafeArrayOfStrings() method. Here, the client creates a SAFEARRAY of BSTRs and sends them into the server for processing. As ATL 3.0 has no SAFEARRAY wrapper class, we must drop down to low-level COM library calls.
We implement UseThisSafeArrayOfStrings() by first verifying that the VARIANT parameter coming our way is indeed an array of BSTRs by examining the vt field accordingly. Next, we will copy the contents of the array into a SAFEARRAY structure, and iterate over the items using SafeArrayAccessData(). When we are all finished, we release our control over the SAFEARRAY by calling SafeArrayUnaccessData():
// Client gives us a batch of strings packaged in a safe array, which we // use as in our program. STDMETHODIMP CCoSafeArray::UseThisSafeArrayOfStrings(VARIANT strings) { USES_CONVERSION; // Be sure we have an array of strings. if((strings.vt & VT_ARRAY) && (strings.vt & VT_BSTR)) { // Grab the array. SAFEARRAY *pSA = strings.parray; BSTR *bstrArray; // Lock it down. SafeArrayAccessData(pSA, (void**)&bstrArray); // Read each item. for(int i = 0; i < pSA->rgsabound->cElements; i++) { CComBSTR temp = bstrArray[i]; MessageBox(NULL, W2A(temp.m_str), "String says...", MB_OK | MB_SETFOREGROUND); } // Unlock it. SafeArrayUnaccessData(pSA); SafeArrayDestroy(pSA); } return S_OK; }
Now we can begin to deal with returning a SAFEARRAY of BSTRs to the client. We will implement GiveMeASafeArrayOfStrings() by making use of the COM library function SafeArrayCreate() to create a SAFEARRAY of some bounds. Next, we will fill the array with BSTRs and finally send this back to the client for processing (and disposal):
// Create a set of strings and give them back to the clients. STDMETHODIMP CCoSafeArray::GiveMeASafeArrayOfStrings(VARIANT *pStrings) { // Init and set the type of variant. VariantInit(pStrings); pStrings->vt = VT_ARRAY | VT_BSTR; SAFEARRAY *pSA; SAFEARRAYBOUND bounds = {4, 0}; // Create the array (client must free the array with SafeArrayDestroy()). pSA = SafeArrayCreate(VT_BSTR, 1, &bounds); // Fill the array with data. BSTR *theStrings; SafeArrayAccessData(pSA, (void**)&theStrings); theStrings[0] = SysAllocString(L"Hello"); theStrings[1] = SysAllocString(L"from"); theStrings[2] = SysAllocString(L"the"); theStrings[3] = SysAllocString(L"coclass!"); SafeArrayUnaccessData(pSA); // Set return value. pStrings->parray = pSA; return S_OK; }
Now assume we have a VB client that creates the coclass and requests a batch of strings. To declare a SAFEARRAY in VB, create a Variant data type, which can hold the return value of GiveMeASafeArrayOfStrings(). We can determine the size of this array using the intrinsic UBound() function, and place each item in a list box (see Figure 11-1):
' Get some SAFE strings from the coclass. ' Private Sub btnGetASafeArray_Click() Dim o As New CoSafeArray Dim strs As Variant ' Grab a number of BSTRs from the coclass. strs = o.GiveMeASafeArrayOfStrings Dim upper As Long Dim i As Long upper = UBound(strs) For i = 0 To upper List1.AddItem strs(i) Next i End Sub
When you are interested in sending variable length arrays of data between clients and servers, the SAFEARRAY can do wonders. You can create an array of any variant compliant type and provide a relatively simple way to work with bulk data.
The only down side, of course, is the items in the array must be the same type. If you wish to send around complex structures as parameters, you will need to roll your own UDTs.
Structures as Method Parameters
Another way to move bulk data between COM entities is to specify IDL structures as method parameters. This can be handy when you wish to group together related data under a specific name. Assume you have an interface method that allows a client to send in a structure as a parameter, which the coclass may reconfigure if necessary. We would begin by writing some IDL code to define the structure. To make things more interesting, let's define one of the fields of this structure to be from a custom enumeration. Here is the IPet interface, which has a single method taking a single complex parameter:
// A custom enumeration which specifies a type of pet. typedef enum PET_TYPE { petDog = 0, petCat = 1, petTick = 2 } PET_TYPE; // A structure describing the pet. typedef struct MyPet { short Age; BSTR Name; PET_TYPE Type; }MyPet; // An interface using the structure. [ object, uuid(1626EA31-5F3C-11D3-B928-0020781238D4)] interface IPet : IUnknown { HRESULT WorkWithAPet([in, out] MyPet* p); };
The implementation will take the incoming MyPet structure and dump out the current contents, then reassign the fields to some new values:
// Use this struct! STDMETHODIMP CCoPet::WorkWithAPet(MyPet *p) { USES_CONVERSION; // Show current Name of pet. MessageBox(NULL, W2A(p->Name), "Name of pet is", MB_OK); // Show current Age of pet. char buff[80] = {0}; sprintf(buff, "%d", p->Age); MessageBox(NULL, buff, "Age of pet is", MB_OK); // Show current Type of pet. char* strType[3] = {"Dog", "Cat", "Tick"}; MessageBox(NULL, strType[p->Type], "Type of pet is", MB_OK); // Now change everything. SysReAllocString(&(p->Name), L"Bubbles"); p->Age = 200; p->Type = petTick; return S_OK; }
Now before you rush out and try this at home, remember that the universal marshaler (oleaut32.dll) can only support the use of structure parameters using NT SP 4.0 (or Win98) and Visual Basic 6.0. If you do not have this configuration, you can still use structure parameters with C++ clients, provided that you build and register your own custom proxy/stub DLL. For this discussion, the world is a happy place and we all support the latest and greatest development workstations. Therefore, a VB client may work with this interface as so:
' Send in a structure for processing. ' Private Sub btnStruct_Click() ' Make a pet. Dim pet As MyPet pet.Name = "Fred" pet.Age = 20 pet.Type = petDog ' Send in for processing. Dim o As New CoPet o.WorkWithAPet pet ' See what happened after the call. MsgBox pet.Age, , "New Age of Pet" MsgBox pet.Name, , "New Name of Pet" ' Map enum to string. Dim t(2) As String t(0) = "Dog" t(1) = "Cat" t(2) = "Tick" MsgBox t(pet.Type), , "New Type of Pet" End Sub
Developing structure parameters can be a step in the right direction. However, in COM the greatest level of power and flexibility comes to us when we configure methods which define interfaces themselves as parameter types. As we have seen, this approach allows us to (in effect) send objects as parameters. Unlike classic OOP, we never really hold a true object reference, but an interface pointer.
Returning COM Interfaces as Method Parameters
The final method of bulk transfer we will examine is to have a method return an interface pointer to some other COM object. As we examined in Chapter 9, this design allows the COM developer to expose some hierarchy of objects within a server. For example, assume we have a topmost and directly creatable COM object named CoBoat. This object supports a single IBoat interface. Assume the GetTheEngine() method of IBoat returns an IEngine interface to the calling client. This is the classic Has-A relationship (a boat "has a" engine).
To begin, assume you have inserted a new CoBoat Simple Object supporting a [default] IBoat interface. Now assume you have also inserted a CoEngine Simple Object with IEngine marked as the [default]. The [default] interface of CoBoat can be defined as the following:
// We gain access to the engine only if we have a boat. [ object, uuid(E19D07B5-5FBF-11D3-B929-0020781238D4) ] interface IBoat : IUnknown { [helpstring("method GetTheEngine")] HRESULT GetTheEngine([out, retval] IEngine** pEngine); };
An ATL implementation of GetTheEngine() could make use of CComObject<>, calling CreateInstance().
// This method creates an engine and returns an IEngine reference to // the client. STDMETHODIMP CCoBoat::GetTheEngine(IEngine **pEngine) { // We need to create an inner object and give out IEngine. CComObject<CCoEngine> *pEng; CComObject<CCoEngine>::CreateInstance(&pEng); pEng->QueryInterface(IID_IEngine, (void**)pEngine); return S_OK; }
To complete this test situation, let's say that IEngine has a single method named Rev().
To use CoBoat from a VB test client, we can write the following: ' Create a boat and grab the engine. ' Private Sub btnRevMyBoat_Click() Dim i As CoEngine Dim myBoat As New CoBoat ' Get the inner object from the outer object. Set i = myBoat.GetTheEngine i.Rev End Sub
Note | Recall that if you do not wish to allow COM clients to directly create subobjects (such as CoEngine) you will need to alter your OBJECT_MAP by specifying the OBJECT_ENTRY_NON_CREATEABLE macro, as well as add the [noncreatable] coclass attribute. |
| < Free Open Study > |
|