The IDispatch Interface

 < Free Open Study > 



The key to late binding is through the standard IDispatch interface. IDispatch is formally defined in IDL within <oaidl.idl> as the following:

// This standard COM interface is used for dynamic binding to a coclass. [ object, uuid(00020400-0000-0000-C000-000000000046), pointer_default(unique) ] interface IDispatch : IUnknown {      // Allows a client to see if the object can provide a type library.      HRESULT GetTypeInfoCount( [out] UINT * pctinfo );      // Get the type library.      HRESULT GetTypeInfo(           [in] UINT iTInfo,           [in] LCID lcid,           [out] ITypeInfo ** ppTInfo );      // Find the numerical ID of some method or property in the object.      HRESULT GetIDsOfNames(           [in] REFIID riid,           [in, size_is(cNames)] LPOLESTR * rgszNames,           [in] UINT cNames,           [in] LCID lcid,           [out, size_is(cNames)] DISPID * rgDispId );      // Call that method or property!      HRESULT Invoke(           [in] DISPID dispIdMember,           [in] REFIID riid,           [in] LCID lcid,           [in] WORD wFlags,           [in, out] DISPPARAMS * pDispParams,           [out] VARIANT * pVarResult,           [out] EXCEPINFO * pExcepInfo,           [out] UINT * puArgErr ); };

As you can see, IDispatch derives from IUnknown like any COM compliant interface must do. Beyond the three inherited methods of IUnknown, IDispatch defines exactly four functions, and therefore a coclass implementing IDispatch must flesh out seven methods. The beauty of IDispatch is in its flexibility. Once a late bound client obtains a pointer to the object's IDispatch interface, it is possible for that client to invoke any property or any method exposed via IDispatch with any number of parameters of any type. This is no small feat. This proposition becomes even more interesting as you discover that a late bound client can perform this magic using exactly two of the four methods of IDispatch: GetIDsOfNames() and Invoke().

Accessing a coclass using IDispatch does come with some restrictions. First and foremost, each and every method parameter and property defined in your implementation of IDispatch must be variant compliant. If you recall from Chapter 4, I mentioned that IDL conceptually provides two sets of data types: the set C and C++ developers love, and a subset of these types that every COM language mapping can agree on. Keep in mind that the variant compliant set of data types has its roots in the Visual Basic Variant data type, and therefore your IDispatch implementations cannot have C++ centric data types as parameters.

As well, realize that when an object supports late binding, you should make use of the universal marshaler (a.k.a. type library marshaling), which was detailed in Chapter 5. This may not be a bad thing in most developer's minds, as this ensures out-of-process access to our COM interfaces without any additional work on our part.

The Methods of IDispatch

The following table shows a breakdown of each method of IDispatch:

Method of IDispatch

Meaning in Life

GetTypeInfoCount()

This method is used by clients wishing to know if the object's functionality is described in an associated type library. This method fills the [out] parameter to zero (0) if the object does not support type information or one (1) if it does.

GetTypeInfo()

Allows a client to access a component's type library programmatically. Typically this is done if the client needs to discover at run time the number of, and types of, parameters to send into a function.

GetIDsOfNames()

This method is a major player in late binding. A client calls this method to retrieve a numerical cookie (termed a DISPID) that identifies the number of the method or property it is attempting to call.

Invoke()

The real workhorse of IDispatch. This is the method that invokes the property or method on behalf of the client, based on the numerical cookie obtained from GetIDsOfNames().

One question that might cross your mind at this point is, how can a single interface allow a client to access any number of methods and properties contained in the coclass using only two methods? You know that once an interface is published, you cannot directly append items to the existing layout, or you will break polymorphism and crash clients (as detailed in Chapter 2). Objects implementing IDispatch do not append their own custom methods directly to the IDispatch interface, nor do they derive a new custom interface from IDispatch to expose additional functionality. Instead, each coclass implementing IDispatch maintains a dispinterface.

The Dispinterface and DISPIDs

A dispinterface is the term for a specific implementation of IDispatch by a given coclass. As a coclass exposes functionality using a single interface, it must identify each property and method with a numerical cookie called a DISPID (dispatch identifier). A DISPID is not a GUID, but a simple numerical index that specifies a given member of the dispinterface. The DISPID data type is defined in <oaidl.idl> as a LONG:

// DISPIDs are not GUIDs. typedef LONG DISPID; 

To obtain the associated DISPID for some property or method, the late bound client will send in the text name of the method or property it is hoping the object supports, by calling GetIDsOfNames(). Using this method, a late bound client is able to obtain the numerical value of a given property or method in the dispinterface. GetIDsOfNames() takes a total of five parameters:

// Breaking down GetIDsOfNames(). HRESULT GetIDsOfNames(      [in] REFIID riid,              // Reserved, and will always be IID_NULL.      [in] LPOLESTR * rgszNames,     // Text name of method/property.      [in] UINT cNames,              // Number of names.      [in] LCID lcid,                // The language ID.      [out] DISPID * rgDispId );     // Place to hold the DISPIDs. 

The first parameter is reserved for (possible) future use, and will always be IID_NULL. The second and third parameters represent the string name and the number of names requested.

Note 

If a client calls GetIDsOfNames() with more than one name, the first name (rgszNames[0]) corresponds to the member name. Any additional names (e.g., rgszNames[1], (rgszNames[2])) correspond to the parameters for the method or property.

The fourth parameter is the locale requested (for example, U.S. English). The final parameter is a place to store the numerical value of the method or property (a.k.a. the DISPID). Here is a C++ client obtaining a pointer to an object's IDispatch interface, and calling GetIDsOfNames() to see if it supports a method named DrawASquiggle():

// Create an object using late binding, obtain an IDispatch pointer, // and see if the object supports a method named 'DrawASquiggle' int main(int argc, char* argv[]) {      ...      IDispatch* pDisp = NULL;      CLSID clsid;      DISPID dispid;      // Obtain the CLSID (as we have no *_i.c file).      CLSIDFromProgID(L"RawDisp.CoSquiggle", &clsid);      // Create the CoSquiggle.      CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IDispatch,                    (void**)&pDisp);      // See if the object supports a method named 'DrawASquiggle'      LPOLESTR str = OLESTR("DrawASquiggle");      pDisp->GetIDsOfNames(IID_NULL, &str, 1, GetUserDefaultLCID(), &dispid);      ... }

Note that the client-side code above obtains the language ID using the COM library function GetUserDefaultLCID(). The LOCALE_SYSTEM_DEFAULT constant can be used as well, to specify we are asking for the language ID used on the target machine (English, Latvian, Arabic, or whatnot). The scriptable objects we will be developing will all ignore the LCID parameter; however if you are interested in creating internationally aware COM objects, check out MSDN.

Once the client knows the DISPID that identifies the property or method, a call to Invoke() may be made to actually trigger the item in the dispinterface. As you may guess, one of the parameters to Invoke() is the DISPID. Here is a breakdown of each parameter of the Invoke() method:

// Breaking down the Invoke() method. HRESULT Invoke( [in] DISPID dispIdMember,               // DISPID of method or property. [in] REFIID riid,                       // Reserved (also IID_NULL) [in] LCID lcid,                         // Local ID (again). [in] WORD wFlags,                       // Flag used to specify a property or method. [in, out] DISPPARAMS * pDispParams,     // An array of parameters for the method. [out] VARIANT * pVarResult,             // A place to store the logical return value.     [out] EXCEPINFO * pExcepInfo,           // Error information (if any). [out] UINT * puArgErr );                // Error information index (if any). 

As you recall, a dispinterface can contain properties and/or methods. The value of the WORD parameter (wFlags) will specify if the client wishes to invoke a method (DISPATCH_METHOD), a "put" version of a property (DISPATCH_PROPERTYPUT), or a "get" version of the property (DISPATCH_PROPERTYGET). Recall that an interface property is identified by two methods in the object and marked in IDL with the [propput] or [propget] attributes. Using this flag, Invoke() can call the correct "get_" or "put_" method in the coclass.

The DISPPARAMS structure is an array of VARIANT compatible data types, which will contain the parameters for the invoked method (we will see the details of this structure in a moment). Finally, beyond the final two parameters (which are used for automation error handling) we have an [out] parameter of type VARIANT*. This will be used to hold the logical return value of the method (if any). To illustrate, here is our C++ client now calling the DrawASquiggle() method (using the obtained DISPID), which for the sake of simplicity takes no parameters and returns nothing:

// Call DrawASquiggle using IDispatch int main(int argc, char* argv[]) {      ...      pDisp->GetIDsOfNames(IID_NULL, &str,1,                         LOCALE_SYSTEM_DEFAULT, &dispid);      // Package parameters and Invoke function number 'dispid'      DISPPARAMS params = {0, 0, 0, 0};      pDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT,                   DISPATCH_METHOD, &params, NULL, NULL, NULL);      pDisp->Release();      ... }

Figure 10-1 illustrates a typical late bound client interaction. As you can see, activating some method in the object's dispinterface requires two round trips beyond the initial connection. As an optimization, the calling client may cache the DISPID of the method or property after the first round trip, and use it for subsequent calls to Invoke():

click to expand
Figure 10-1: Late binding using IDispatch.

Of the four methods of IDispatch, GetIDsOfNames() and Invoke() are the members that make automation possible. GetTypeInfoCount() and GetTypeInfo() allow a client to ask the object if it supports any type information (i.e., a type library) and if so, retrieve it. The end result is the client is able to obtain an ITypeInfo interface. From this interface pointer, a client may dynamically thumb through the server's *.tlb file, and discover any documentation ([helpstrings]) held in the type information, create objects, extract IIDs, and so forth. More often than not, these methods will be of little use to you as a COM client, unless you are interested in building a type library browser utility such as the OLE/COM Object Viewer.

Approaches to Building a Raw Dispinterface in C++

Before we see how ATL is kind enough to create a complete implementation of IDispatch for you, we will begin by examining how we might create a dispinterface by hand using raw C++. Truth be told, you could implement a dispinterface using a number of possible techniques. One option is to go 100% C++ and avoid any type information. You might consider this "really late binding" as the COM client could not even open up an object browser to examine what the functionality of the coclass might be. Another option is to generate a type library for clients to examine in an object browsing utility, while still requiring all requests to funnel through your C++ implementation of IDispatch. This would give your COM client the luxury of examining the contents of your server at design time (for example, by using the VB Object Browser) while still forcing them to access your IDispatch implementation at run time.

Yet another approach is to make your type information work for you as you build the coclass. The COM library contains a set of functions that allow you to load up your type library at run time, and "peek" inside to see what the server contains. If you recall from Chapter 5, we made use of the COM library function LoadTypeLibEx() to automatically register our type information. When we develop "dual interface" objects later in this chapter, we will use a similar approach to provide an implementation of IDispatch for a coclass.

Implementing IDispatch in Raw C++

The first possible approach to take when developing a dispinterface is to do everything yourself. As with most programming endeavors, this gives you the biggest bang for the buck as far as flexibility is concerned, but requires much more coding. Assume you have a coclass named CoSquiggle that exposes its services exclusively through IDispatch. In other words, CoSquiggle will not derive from any custom interfaces such as IDraw, IShapeEdit, or whatnot. Clients must obtain an IDispatch pointer and call GetIDsOf- Names() and Invoke() to trigger the given method or property. The first step, then, is to create a class definition for CoSquiggle which derives from IDispatch, and prototypes the seven inherited methods (don't forget IUnknown!). Here is the header file for the CoSquiggle coclass:

// This coclass only supports IDispatch and thus late binding. class CoSquiggle : public IDispatch { public:      CoSquiggle();      virtual ~CoSquiggle();      // IUnknown methods.      STDMETHODIMP_(DWORD)AddRef();      STDMETHODIMP_(DWORD)Release();      STDMETHODIMP QueryInterface(REFIID riid, void** ppv);      // IDispatch methods.      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:      DWORD m_ref; };

To fill out the details of CoSquiggle, we will begin with IUnknown. As an automation object is a true blue COM object, CoSquiggle will maintain a reference count, and thus AddRef() and Release() would be implemented in the standard and usual manner. QueryInterface() is also a "normal" implementation; however, this time we must be sure to return vPtrs for IID_IUnknown and IID_IDispatch exclusively:

// Bump the reference counter. // (initialize m_ref to zero in the constructor of CoSquiggle) STDMETHODIMP_(DWORD) CoSquiggle::AddRef() {      return ++m_ref; } // Standard Release() logic. STDMETHODIMP_(DWORD)CoSquiggle::Release() {      --m_ref;      if(m_ref == 0)      {           delete this;           return 0;      }      return m_ref; } // A 100% dispinterface will always return two vPtrs to the COM client. STDMETHODIMP CoSquiggle::QueryInterface(REFIID riid, void** ppv) {      // We do not support any custom interfaces here!      if(riid == IID_IUnknown)           *ppv = (IUnknown*)this;      else if(riid == IID_IDispatch)           *ppv = (IDispatch*)this;      else      {           *ppv = NULL;           return E_NOINTERFACE;      }      ((IUnknown*)(*ppv))->AddRef();      return S_OK; } 

When it comes to the implementation of IDispatch::GetTypeInfoCount(), we have a no-brainer. Recall that a COM client can call this method to discover if the automation object supports an associated type library. If so, that client may make use of it at run time to examine the functionality of the server. This iteration of CoSquiggle does not support any type information, thus we can set the incoming UINT pointer to zero, informing the client we have no type information to return:

// GetTypeInfoCount() will set the UINT to zero if it does not support type // information. STDMETHODIMP CoSquiggle::GetTypeInfoCount( UINT *pctinfo) {      // We do not support type information in this object.      *pctinfo = 0;      return S_OK; }

Implementing GetTypeInfo() is also easy in this case (and not much harder in other cases) as we don't have any type info to return! Assume for a moment that CoSquiggle did indeed support type information. If so, we would set the ITypeInfo* parameter to refer to our server's specific type library. Again, ITypeInfo provides a number of methods that a calling client can call to discover the interfaces, coclasses, helpstrings, and so forth defined in our IDL code. As mentioned, object browsers make heavy use of ITypeInfo (and related interfaces) to extract our type information. Here, we can simply set the ITypeInfo pointer to NULL, and return a strange little HRESULT, E_NOTIMPL, informing the client that we have no implementation of this method to speak of. While use of E_NOTIMPL is considered bad style, this should make sense to us in this context, as a COM client would have ideally called GetTypeInfoCount() to begin with and already discovered we have nothing by way of type information to offer:

// GetTypeInfo() returns a pointer to our type library's contents (if we have any). STDMETHODIMP CoSquiggle::GetTypeInfo(UINT iTInfo, LCID lcid,                                  ITypeInfo **ppTInfo) {      // Return NULL pointer as we do not support type info.      *ppTInfo = NULL;      return E_NOTIMPL; }

So far so good, but now the work begins. Recall that clients using late binding must first make a call to GetIDsOfNames() to obtain the DISPID of the method they are looking to invoke. Assume CoSquiggle has three methods in its dispinterface, named DrawA- Squiggle(), FlipASquiggle(), and EraseASquiggle().

We may associate DISPIDs to each method with help of the preprocessor #define directive (or the C++ const keyword—take your pick). The client will send us a LPOLESTR, and if this is a string we recognize, we will set the rgDispId parameter to the correct DISPID:

// An implementation of GetIDsOfNames() allows a client to send in a string // parameter for us to test against. #define DISPID_DRAWSQUIG     1 #define DISPID_FLIPSQUIG     2 #define DISPID_ERASESQUIG     3 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;      // Are they asking for the DISPID for DrawASquiggle?      if(_wcsicmp(*rgszNames, L("DrawASquiggle") == 0)      {           *rgDispId = DISPID_DRAWSQUIG;           return S_OK;      }      // Are they asking for the DISPID for FlipASquiggle?      if(_wcsicmp(*rgszNames, L("FlipASquiggle") == 0)      {           *rgDispId = DISPID_FLIPSQUIG;           return S_OK;      }      // Are they asking for the DISPID for EraseASquiggle?      if(_wcsicmp(*rgszNames, L("EraseASquiggle") == 0)      {           *rgDispId = DISPID_ERASESQUIG;           return S_OK;      }      else           return DISP_E_UNKNOWNNAME; }

Notice that this implementation of GetIDsOfNames() uses basic if/else logic to test the string against our known dispinterface members. By convention, return DISP_E_UN- KNOWNNAME when the late bound client asks for a DISPID you cannot account for.

The logic behind GetIDsOfNames() can grow quickly, as your automation objects support more and more functionality. For example, if CoSquiggle had a dispinterface defining 20 unique methods, this would entail testing between 20 possible strings and returning 20 unique DISPIDs. In raw C++ this is a bother; we will soon see how to make use of a server's type information and the COM library to resolve the implementation of GetIDsOfNames() to a few lines of code.

Finally, we have to provide an implementation of Invoke(). This is where the real action happens. Once the client has obtained the correct DISPID, it uses the acquired IDispatch pointer to call Invoke(), sending in the numerical cookie and any necessary parameters. The parameters sent into Invoke() are packaged up as an array of VARIANT data types, and held in the DISPPARAMS structure. It might be strange to think that one of the parameters to Invoke() is in turn a set of parameters. However, this is how a late bound client is able to send any number of parameters to an automation object. As luck would have it, the methods in our dispinterface (DrawASquiggle(), EraseASquiggle(), and FlipASquiggle()) do not take any parameters, and therefore we can ignore the processing of any client-supplied DISPPARAMS. Here, then, is an implementation of the Invoke() method for our late bound CoSquiggle:

// Now, based on the DISPID, call the correct helper function STDMETHODIMP CoSquiggle::Invoke(   DISPID dispIdMember,          // DISPID of dispinterface member.   REFIID riid,                  // Reserved.   LCID lcid,                    // Locality.   WORD wFlags,                  // Are we calling a method or property?   DISPPARAMS *pDispParams,      // Any arguments for the method.   VARIANT *pVarResult,          // A place to hold the logical return value.   EXCEPINFO *pExcepInfo,        // Any error information?   UINT *puArgErr)               // Again, any error information? {      // We have no parameters for these functions, so we can just      // ignore the DISPPARAMS.      // Based on DISPID, call the correct member of the dispinterface.      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;      } }

As you can see, Invoke() will examine the DISPID parameter and call internal helper functions accordingly. To keep things simple, assume that each helper function simply launches an appropriate MessageBox(). Now, as long as CoSquiggle has a class object to activate it and has been packaged up into a COM server (DLL or EXE), we can allow any COM-enabled language to make use of it, including the dumb clients of the world.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net