| < Free Open Study > |
|
No discussion of automation would be complete without an examination of the concept of a safe array. The safe array is an automation data type which allows you to send around a variable length array of variant compatible types. As you will see in Chapter 11, IDL allows you to send around traditional C style arrays as interface parameters, and your early bound clients are happy to work with these methods. Late bound clients are not happy to work with this type of array. Instead, late bound clients make use of a special self-describing data structure, the SAFEARRAY, defined in <oaidl.idl> as the following:
// Dual interfaces and raw dispinterfaces may send around variable length // arrays of variant compliant data using the SAFEARRAY. // typedef struct tagSAFEARRAY { USHORT cDims; // Number of dimensions in array. USHORT fFeatures; // Used to deallocate memory in the array. ULONG cbElements; // Size of each member in the array. ULONG cLocks; // Number of locks on the array. PVOID pvData; // The actual data in the array. // For each dimension in the array, the SAFEARRAYBOUND specifies the // number of elements in the array, and the value of the lower bound in the // array. SAFEARRAYBOUND rgsabound[]; } SAFEARRAY;
The SAFEARRAY structure contains another structure, SAFEARRAYBOUND, as one of its fields, which is defined as so:
// Holds information about each dimension in the array. typedef struct tagSAFEARRAYBOUND { ULONG cElements; LONG lLbound; } SAFEARRAYBOUND, * LPSAFEARRAYBOUND;
So, why in the world would a SAFEARRAY need to know the lower bound of the array? We all know arrays begin at index zero, right? Wrong, at least if you are a Visual Basic programmer. VB allows developers to do "cute" things such as create an array with a lower bound of 32 and an upper bound of 80. This might make the VB developer's life more satisfying, as he or she can loop over that array more intuitively (maybe not so for a tried and true C++ developer).
When you are working with the SAFEARRAY, you do have some help provided by the COM library. Rather than having to work with the raw structures yourself, you are provided with a set of API functions that allow you to create, access, and destroy the underlying structure. In this way, the SAFEARRAY is operated on in much the same way as the BSTR and VARIANT data type. Sadly, as of ATL 3.0, we do not have a comfy wrapper class to hide the grunge of working with this automation data type, so you will need to get familiar with the following core methods, as shown in the following table.
SAFEARRAY Library Function | Meaning in Life |
---|---|
SafeArrayCreate() | Creates a new SAFEARRAY structure. |
SafeArrayGetDim() | Returns the number of dimensions of the array. |
SafeArrayDestroy() | Destroys an existing SAFEARRAY structure, and deallocates all memory associated to the array. |
SafeArrayGetElement() | Retrieves an item in the SAFEARRAY. |
SafeArrayPutElement() | Inserts an item into the existing SAFEARRAY. |
SafeArrayGetUBound() | Returns the value of the upper bound in the array. |
SafeArrayGetLBound() | Returns the value of the lower bound in the array. |
SafeArrayAccessData() | Locks the array and retrieves a pointer to the underlying data. |
SafeArrayUnaccessData() | Unlocks the array and disposes of pointer obtained by SafeArrayAccessData(). |
To illustrate using a SAFEARRAY, consider this overused and trivial example. Assume an ATL-based coclass contains a method taking a SAFEARRAY of longs and returns the sum. To begin coding the IDL, you will see that a SAFEARRAY can be defined as an array of VARIANTs. This should set well with you, as you are aware that a SAFEARRAY is by definition an array of variant compatible types. Here is some IDL code defining a dual interface named IAddArrays (more on dual interfaces soon):
// Add this array of VARIANTS. [ object, uuid(938EA4F3-5802-11D3-B926-0020781238D4), dual, helpstring("ICoATLSquiggle Interface"), pointer_default(unique) ] interface IAddArrays : IDispatch { [id(1), helpstring("Add this array because I am too lazy")] HRESULT AddThisArrayOfNumbers([in] VARIANT theArray, [out, retval] long* sum); };
Assume this interface is implemented by the CHelloSafeArray object. Because we are expecting the COM client to pass us a safe array of longs (as opposed to an array of IDispatch pointers or BSTRs or whatnot) we will first want to check that we have the correct information in the array; otherwise we will return DISP_E_TYPEMISMATCH:
// Use the vt field of the VARIANT structure to determine what is // inside this array. STDMETHODIMP CHelloSafeArray::AddThisArrayOfNumbers(VARIANT theArray, long *sum) { // We need to first ensure that we have received an array of longs. if((theArray.vt & VT_I4) && (theArray.vt & VT_ARRAY)) { // Do stuff with the array... return S_OK; } return DISP_E_TYPEMISMATCH; }
Once we are able to determine that we do indeed have an array of longs, we can determine the upper and lower bounds of the array, using the SafeArrayGetUBound() and SafeArray- GetLBound() API functions. To fetch out an item in the array, we may make use of SafeArrayGetElement(). Here is the complete code for the AddThisArrayOfNumbers() method:
// This method takes a batch of data and adds things up. STDMETHODIMP CHelloSafeArray::AddThisArrayOfNumbers(VARIANT theArray, long *sum) { // We need to first ensure that we have received an array of longs. if((theArray.vt & VT_I4) && (theArray.vt & VT_ARRAY)) { // Fill a SAFEARRAY structure with the // values in the VARIANT array, using the 'parray' field. SAFEARRAY* pSA; pSA = theArray.parray; // Figure out how the array is packaged. long lBound; long uBound; SafeArrayGetUBound(pSA, 1, &uBound); SafeArrayGetLBound(pSA, 1, &lBound); // Loop over the members in the array and add them up. long ans = 0; long item = 0; for(long j = lBound; j <= uBound; j++) { SafeArrayGetElement(pSA, &j, &item); ans = ans + item; } // Set result. *sum = ans; SafeArrayDestroy(pSA); return S_OK; } return DISP_E_TYPEMISMATCH; }
A Visual Basic client may use this method as so:
' A VB client creating a safe array, and sending it into the coclass ' for processing. ' Private Sub Form_Load() ' Make an array with odd dimensions Dim VBArray(30 To 90) As Long ' Fill with some data Dim i As Integer For i = 30 To 90 Randomize VBArray(i) = Rnd(i) * 30 Next ' Send array in for addition. Dim o As New CHelloSafeArray MsgBox o.AddThisArrayOfNumbers(VBArray), , "The summation is..." End Sub
A C++ client needs to do much more work. As mentioned, ATL does not offer a wrapper around the SAFEARRAY structure, and therefore we must write code making use of the various SAFEARRAY API calls. Here is a C++ client calling the AddThisArrayOf- Numbers() method. Take note of the use of SafeArrayCreate(), SafeArrayDestroy(), and SafeArrayPutElement():
// C++ SAFEARRAY clients need to do more grunge. int main(int argc, char* argv[]) { CoInitialize(NULL); HRESULT hr; IAddArrays* pInterface = NULL; hr = CoCreateInstance(CLSID_CHelloSafeArray, NULL, CLSCTX_SERVER, IID_IAddArrays, (void**)&pInterface); // Make a safe array. SAFEARRAY* pSA = NULL; SAFEARRAYBOUND bound[1]; bound[0].lLbound = 0; bound[0].cElements = 30; pSA = SafeArrayCreate(VT_I4, 1, bound); // Fill the array. for(long i = 0; i < 30; i++) SafeArrayPutElement(pSA, &i, &i); // Now pack up the array... VARIANT theArray; VariantInit(&theArray); theArray.vt = VT_ARRAY | VT_I4; theArray.parray = pSA; long ans = 0; pInterface->AddThisArrayOfNumbers(theArray, &ans); cout << "The summation of this array is: " << ans << endl; // Clean up. pInterface->Release(); SafeArrayDestroy(pSA); CoUninitialize(); return 0; }
One problem with the safe array is that the use of such a construct entails that the client and coclass exchange a complete mass of data in one call. There is no manner to "pull" data from the object or work with that data in a more selective fashion. Often, a better approach is to create a COM collection, which we will do later in Chapter 11.
To help you understand how the IDispatch interface behaves, this lab will give you a chance to build a COM server in C++ that contains a single object supporting IDispatch. You will be developing a "dispinterface" for this object, which has been defined as a collection of properties and methods exposed via IDispatch. You will then create a series of late bound clients, including a web client using VBScript.
On the CD The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 10\RawDisp
Labs\Chapter 10\RawDisp\CPP Client
Labs\Chapter 10\RawDisp\VB Client
Labs\Chapter 10\RawDisp\VBScript Client
Here we are again, avoiding the help of ATL and writing another raw COM server, so feel free to leverage your existing code (that which does not kill you will make you stronger!). Create a new Win32 DLL project workspace (an empty project). Insert a new C++ class named CoSquiggle. CoSquiggle is an automation object (a.k.a. scriptable object), which by definition supports only IDispatch. The dispinterface of CoSquiggle supports three methods, named DrawASquiggle(), FlipASquiggle(), and EraseASquiggle(). The header file of CoSquiggle begins life as so:
// This coclass only supports IDispatch and thus late binding. #include <windows.h> class CoSquiggle : public IDispatch { public: CoSquiggle(); virtual ~CoSquiggle(); STDMETHODIMP_(DWORD)AddRef(); STDMETHODIMP_(DWORD)Release(); STDMETHODIMP QueryInterface(REFIID riid, void** ppv); STDMETHODIMP GetTypeInfoCount( UINT *pctinfo); STDMETHODIMP GetTypeInfo( UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo); STDMETHODIMP GetIDsOfNames( REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId); STDMETHODIMP Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr); private: void DrawASquiggle(); void FlipASquiggle(); void EraseASquiggle(); DWORD m_ref; // Set to zero in constructor... };
Now, provide a standard implementation for your IUnknown methods. AddRef() and Release() will be like any standard coclass implementation; however, be sure your QueryInterface() method only returns vPtrs for the IUnknown or IDispatch interfaces.
Now for the implementation of IDispatch. As our coclass has no type information to return to any interested clients, we may implement GetTypeInfoCount() and GetTypeInfo() quite easily:
// No type information here! STDMETHODIMP CoSquiggle::GetTypeInfoCount( UINT *pctinfo) { // We do not support type information in this object. *pctinfo = 0; return S_OK; } STDMETHODIMP CoSquiggle::GetTypeInfo( UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) { // Return NULL pointer as we do not support type info. *ppTInfo = NULL; return E_NOTIMPL; }
The GetIDsOfNames() method is responsible for returning a DISPID based on the incoming string. To represent the possible DISPIDs for your dispinterface, add some #define constants in your <cosquiggle.cpp> file and implement GetIDsOfNames() to return the correct DISPID to the client:
// The DISPID constants. #define DISPID_DRAWSQUIG 1 #define DISPID_FLIPSQUIG 2 #define DISPID_ERASESQUIG 3 // Client gives us a string name, so do a lookup // between the string and the cookie. STDMETHODIMP CoSquiggle::GetIDsOfNames( REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) { // First off, we only support one name at a time. if(cNames > 1) return E_INVALIDARG; // Which member of the dispinterface do they want? if(_wcsicmp(*rgszNames, L"DrawASquiggle") == 0) { *rgDispId = DISPID_DRAWSQUIG; return S_OK; } if(_wcsicmp(*rgszNames, L"FlipASquiggle") == 0) { *rgDispId = DISPID_FLIPSQUIG; return S_OK; } if(_wcsicmp(*rgszNames, L"EraseASquiggle") == 0) { *rgDispId = DISPID_ERASESQUIG; return S_OK; } else return DISP_E_UNKNOWNNAME; }
Next, implement Invoke() to call the correct private helper function of your dispinterface based on the DISPID:
// Now, based on the DISPID, call the correct helper function. STDMETHODIMP CoSquiggle::Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) { // We have no parameters for these functions, so we can just // ignore the DISPPARAMS. // Call dispinterface member. switch(dispIdMember) { case DISPID_DRAWSQUIG: DrawASquiggle(); return S_OK; case DISPID_FLIPSQUIG: FlipASquiggle(); return S_OK; case DISPID_ERASESQUIG: EraseASquiggle(); return S_OK; default: return DISP_E_UNKNOWNINTERFACE; } }
Finally, provide an implementation for each private helper function, for example:
// Give some implementation for the dispinterface members. void CoSquiggle::DrawASquiggle() { // Drawing a squiggle... MessageBox(NULL, "Drawing a squiggle", "CoSquiggle Invoke", MB_OK | MB_SETFOREGROUND); }
Go ahead and compile to ensure you have no typos.
You know the drill here! Insert another new class that will work as the class factory for CoSquiggle. Feel free to leverage an existing class factory, but be sure to modify CreateInstance() to create a CoSquiggle:
// Create the squiggle. STDMETHODIMP CoSquiggleClassFactory::CreateInstance(LPUNKNOWN pUnkOuter, REFIID riid, void** ppv) { // We do not support aggregation in this class object. if(pUnkOuter != NULL) return CLASS_E_NOAGGREGATION; CoSquiggle* pSquig = NULL; HRESULT hr; // Create the object. pSquig = new CoSquiggle; // Ask object for an interface. hr = pSquig -> QueryInterface(riid, ppv); // Problem? We must delete the memory we allocated. if (FAILED(hr)) delete pSquig; return hr; }
Next, insert a new CPP file to define the DLL component housing. As always, implement DllGetClassObject() and DllCanUnloadNow() in the usual manner. Don't forget to define global counters for locks and active objects, and increment and decrement accordingly (if you have worked through all the labs up until now, I can assume you know what this would entail ... but feel free to look back on previous labs).
Also provide a DEF file for these exports, and a REG file to enter the ProgID and CLSID entries for your CoSquiggle, as shown below:
LIBRARY "RAWDISP" EXPORTS DllGetClassObject @1 PRIVATE DllCanUnloadNow @2 PRIVATE REGEDIT ; This is the ProgID ; HKEY_CLASSES_ROOT\RawDisp.CoSquiggle\CLSID = {F43C0966-577B-11d3-B926-0020781238D4} ; This is the CLSID ; HKEY_CLASSES_ROOT\CLSID\{F43C0966-577B-11d3-B926-0020781238D4} = RawDisp.CoSquiggle HKEY_CLASSES_ROOT\CLSID\{F43C0966-577B-11d3-B926-0020781238D4} \InprocServer32 = E:\ATL\Labs\Chapter 10\RawDisp\Debug\RawDisp.dll
Compile your DLL and be sure to merge your REG file. Now that we have a new coclass supporting IDispatch, we turn our attention to some client code.
You have seen this code previously in this chapter. Create a Win32 Console Application. Create your CoSquiggle using CoCreateInstance() and ask for the IDispatch interface. From this interface, call GetIDsOfNames() and ask for access to some member of the CoSquiggle dispinterface; hold onto the DISPID. Now, using this DISPID, call Invoke() and send in an empty set of DISPPARAMS (as none of the methods in the dispinterface require parameters). As usual, release your acquired interfaces:
// A late bound C++ client. int main(int argc, char* argv[]) { CoInitialize(NULL); IDispatch* pDisp = NULL; CLSID clsid; DISPID dispid; CLSIDFromProgID(L"RawDisp.CoSquiggle",&clsid); LPOLESTR str = OLESTR("FlipASquiggle"); CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IDispatch, (void**)&pDisp); // Get DISPID. pDisp->GetIDsOfNames(IID_NULL, &str,1, LOCALE_SYSTEM_DEFAULT, &dispid); // Trigger method. DISPPARAMS params = {0, 0, 0, 0}; pDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, ¶ms, NULL, NULL, NULL); pDisp->Release(); CoUninitialize(); return 0; }
When a VB client wishes to bind late to a COM object, the protocol is to declare an Object variable and set it to the return value of CreateObject(). This VB method takes the ProgID of the server object, which allows SCM to look up the related CLSID at run time. Create a new VB Standard EXE and create a functional GUI. The code behind the GUI is responsible for fetching the IDispatch pointer and calling the members of the dispinterface. You will notice that the VB IntelliSense is not functional. This should make sense, as you have not set a reference to any type information. Here is some possible VB client code. Note that I have added an incorrect call to InvertACircle(). Our CoSquiggle does not support this method, and therefore GetIDsOfNames() will return DISP_E_UNKNOWNNAME. As this is discovered at run time, the code compiles without error:
' This will hold the IDispatch pointer ' ' [General][Declarations] Dim o As Object Private Sub GetIDisp_Click() ' Go get IDispatch from the coclass Set o = CreateObject("RawDisp.CoSquiggle") End Sub Private Sub UseIDisp_Click() ' Call GetIDsOfNames and Invoke for each. o.DrawASquiggle o.FlipASquiggle o.EraseASquiggle o.InvertACircle ' This will trigger a runtime error! End Sub
As C++, VB, and Java can all work with early binding, the need for IDispatch might seem illusive. But, as soon as you want to access your object from a web page, you will live by this COM interface. Using Notepad (yes, Notepad), type in the following HTML code, and save it as vbscriptclient.htm (be sure you substitute your ProgID if necessary):
' Some Web-enabled squiggling. ' <HTML> <HEAD> <TITLE> Web Tester </TITLE> </HEAD> <BODY> <SCRIPT language=VBScript> dim o set o = CreateObject("RawDisp.CoSquiggle") o.DrawASquiggle o.FlipASquiggle o.EraseASquiggle </SCRIPT> </BODY> </HTML>
Note | All data types in VBScript are Variants. |
Now, double-click on the HTML file. This will launch IE and load up your COM object!
You will see a warning box pop up as your page loads. This is because we have not specified that CoSquiggle is safe for scripting (we will fix this when we work with ATL later in this chapter). Here is CoSquiggle on the web:
Figure 10-2: Late binding using MS Internet Explorer and VBScript.
Now that you have seen how to create a very dynamic connection to an object using IDispatch as well as a very static connection to custom interfaces (and type information/MIDL-generated files), we can now turn our attention to a dual interface. But before we do, here is one final late bound client you may wish to try.
Just for kicks, what if we were to create a new toolbar within the MS Visual Studio IDE that would launch your CoSquiggle? I am sure you can see the need to draw, erase, and flip a squiggle as you write real production software, so what the heck. Begin by accessing the Tools | Macro menu selection. This will launch a dialog box that allows you to automate your installation of Visual Studio using (of course) VBScript. Type in a name for your new macro, such as ExerciseTheSquiggle. The name you provide will end up being the name of the VBScript method invoked from the toolbar (see Figure 10-3).
Figure 10-3: Creating a new Dev Studio Macro.
Once you have named your new macro, click on the Edit button to enter a description of this macro:
Figure 10-4: Describing the macro.
In the generated stub, write some VBScript code to do the job:
Sub ExerciseTheSquiggle() 'DESCRIPTION: This will use the raw C++ scriptable 'object I made in Chapter 10 because the author of 'this book is making me do this... dim o set o = CreateObject("rawdisp.cosquiggle") o.DrawASquiggle o.FlipASquiggle o.EraseASquiggle End Sub
Now, save the <sample.dsm> file, and go back into the Tools | Macro dialog box. Select the Options button, and then the Assign Macro to Toolbar option. Find your macro in the list box, and drag and drop the macro text anywhere onto the IDE. Pick a toolbar button from the final dialog box and close everything down. Now click the new button to execute your macro, and get some real work done!
| < Free Open Study > |
|