Five Variations on the Theme of Implementing a Simple Automation Object

A few short steps from where I'm writing this chapter sits a piano. I often take breaks and play for a while to let my brain incubate and organize the next stage of writing. It happens that my current practice includes Beethoven's Fifteen Variations with Fugue on a Theme from the Eroïca Symphony, op. 35, and Brahms's Variations and Fugue on a Theme by Handel for Pianoforte, op. 24. Fabulous pieces, but perhaps they have influenced me a bit too much as I've worked on Automation. The samples in this chapter seem to follow a similar pattern: a number of variations (only five of them!) on a theme of a simple automation object, followed by a longer and more complex fugue in the form of an automated version of our dear friend Cosmo. What's scary is that the musical analogy holds better than you might think.

To illustrate a simple automation object, let's fully define the Beeper object used as an example earlier in this chapter. This object, implemented in a DLL, has one property and one method in its incoming interface. Furthermore, we'll support both English and German interfaces in all but the last variation. Our theme is specified as follows:

Property:

Sound (German: Ton), a long that can be set to 0, 16, 32, 48, or 64, corresponding to the allowable values that can be sent to the MessageBeep function.

Method:

Beep (German: Piep), takes no arguments (void) and returns a long, the sound that was played.

We'll examine five different ways to implement the IDispatch interface through which Sound and Beep can be invoked, progressing as follows. (The name in the parentheses is the directory of the associated sample.)

Variation I

(BEEPER1) A completely raw and manual implementation of IDispatch that supports simultaneous access to the dispinterface in either English or German, with full help in either language. This variation raises an exception if the controller tries to change the Sound property to an incorrect value, but it does not have any type information.

Variation II

(BEEPER2) A more convenient implementation of IDispatch that takes advantage of type information to perform type coercion and name-to-dispID mapping. This variation supports English and German simultaneously and raises exceptions. The exception handling mechanism, however, works only for a single-threaded model.

Variation III

(BEEPER3) Solves the single-thread exception problem of Variation II through the error object mechanisms that OLE provides.

Variation IV

(BEEPER4) Implements our dispinterface through a dual interface, illustrating the special techniques involved.

Variation V

(BEEPER5) A reprise of Variation III that takes advantage of an OLE-provided Standard Dispatch object to free us from having our own IDispatch entry points at all. However, this allows us to support only one language.

Beeper2 and Beeper3 will usually be the most common of these implementation techniques because they are the most flexible and are easy to implement (depending on what sort of threading you use). Beeper4 will be the next most common, suitable for when you need more performance. Beeper5 is easier to implement than the others but is restricted to a single language. Lastly, although Beeper1 won't be too useful for most automation objects, it is useful when you need to implement only a simple IDispatch interface—to use with an event sink, for example—because you have no need for type information or even for implementing IDispatch::GetIDsOfNames. These five techniques will show us the full set of options when it comes to IDispatch. Choose the method that makes sense for you.

To let you play these variations, the BEEPTEST\DISPTEST directory in this chapter contains a DispTest/Visual Basic 3 program for thoroughly pummeling each Beeper. BEEPTEST\NEWVB has the same program saved in the format of the next version of Visual Basic. If you want to watch what happens with one of these objects in a controller's source code, you can use the AutoCli sample from Chapter 15, which is written specifically for using a Beeper object.

Before looking at each variation, note that the DLL server code surrounding the object is the same in all of them (with one minor exception for multithreading concerns). The files DBEEPER.CPP and DBEEPER.H contain the standard DLL entry point and exported functions as well as the class factory implementation. The Beeper's CLSID in all cases is {00021125-0000-0000-C000-000000000046}, defined as CLSID_Beeper in INC\BOOKGUID.H. In each sample, the most important files are BEEPER.CPP, BEEPER.H, and the ODL files that are present. BEEPER.CPP contains the implementation of the object in the class CBeeper; BEEPER.H contains the definitions; and the ODL files provide type information. CBeeper is usually defined as follows in BEEPER.H:


class CBeeper : public IUnknown  //Possibly a different base interface
{
//Possible friend declarations

protected:
ULONG m_cRef; //Object reference count
LPUNKNOWN m_pUnkOuter; //Controlling unknown
PFNDESTROYED m_pfnDestroy; //To call on closure

long m_lSound; //Type of sound

//Some kind of IDispatch implementation pointer and other
//variables are needed to manage implementation.

public:
CBeeper(LPUNKNOWN, PFNDESTROYED);
~CBeeper(void);

BOOL Init(void);

//Nondelegating object IUnknown
STDMETHODIMP QueryInterface(REFIID, PPVOID);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);

//Possibly other member functions
};

typedef CBeeper *PCBeeper;

The comments in the code above mark where we'll generally modify for the variations of the object. In all cases, however, the CBeeper class has a set of IUnknown functions, a reference count, an m_lSound value, and the other members shown here.