Automation Basics

[Previous] [Next]

Unlike a typical COM object, which exposes interfaces and methods, an Automation object exposes methods and properties. A method is a function that can be called by the object's clients. A property is an attribute of the object, such as a color or a file name.

Automation-aware languages such as Visual Basic shield the programmer from reference counting, interface pointers, and other idioms of COM. They also permit you to access Automation methods and properties as easily as you access local subroutines and variables. The following Visual Basic statements instantiate an Automation object and invoke a method named Add to add 2 and 2:

Dim Math as Object Set Math = CreateObject ("Math.Object") Sum = Math.Add (2, 2) Set Math = Nothing 

In this example, Math is a variable of type Object. "Math.Object" is the Automation object's ProgID. (Recall from Chapter 18 that a ProgID is the string analogue of a COM CLSID.) The final statement frees the object by performing the Visual Basic equivalent of calling Release through an interface pointer.

In Visual Basic, accessing an Automation property is syntactically similar to calling an Automation method. The next example creates a bank account object and sets the balance in the account by assigning a value to a property named Balance:

 Dim Account as Object Set Account = CreateObject ("BankAccount.Object") Account.Balance = 100 

Checking the balance in the account is as simple as reading the property value:

 Amount = Account.Balance 

Reading or writing an Automation property is analogous to accessing a public member variable in a C++ class. In truth, COM objects can't expose member variables any more than C++ objects can expose private data members. The illusion that an Automation object can expose values as well as methods is part of the magic of Automation.

Automation clients written in VBScript look very much like Automation clients written in Visual Basic. The following script uses VBScript's built-in FileSystemObject object, which is in reality an Automation object, to create a text file containing the string "Hello, world":

 Set fso = CreateObject ("Scripting.FileSystemObject") Set TextFile = fso.CreateTextFile ("C:\Hello.txt", True) TextFile.WriteLine ("Hello, world") TextFile.Close 

To try this script for yourself, use Notepad or a program editor to enter these statements in a text file and save the file with the extension .vbs. Then double-click the file in the operating system shell or type START filename.vbs in a command prompt window. On Windows 98 and Windows 2000 systems, this will invoke the built-in Windows Scripting Host, which will open the file and execute the statements found inside it.

You can also write Automation clients in C++. The next section shows you how, but be warned that it isn't pretty because a C++ client doesn't have the Visual Basic run-time or a scripting engine to serve as a mediator between it and the Automation server. The good news is that Visual C++ aids in the creation of Automation clients by generating easy-to-use wrapper classes based on MFC's COleDispatchDriver class. You'll see what I mean later in this chapter.

IDispatch: The Root of All Automation

Automation looks fairly simple from the outside, and for a Visual Basic programmer, Automation is simple. But the fact that COM is involved should be a clue that what goes on under the hood is a far different story.

The key to understanding how Automation works lies in understanding the COM interface known as IDispatch. An Automation object is a COM object that implements IDispatch. IDispatch contains four methods (which are listed in the table below), not counting the three IUnknown methods common to all COM interfaces. Of the four, Invoke and GetIDsOfNames are the most important. A client calls Invoke to call an Automation method or to read or write an Automation property. Invoke doesn't accept a method or a property name such as "Add" or "Balance." Instead, it accepts an integer dispatch ID, or dispid, that identifies the property or method. GetIDsOfNames converts a property name or a method name into a dispatch ID that can be passed to Invoke. Collectively, the methods and properties exposed through an IDispatch interface form a dispinterface.

The IDispatch Interface

Method Description
Invoke Calls an Automation method or accesses an Automation property
GetIDsOfNames Returns the dispatch ID of a property or a method
GetTypeInfo Retrieves an ITypeInfo pointer (if available) for accessing the Automation object's type information
GetTypeInfoCount Returns 0 if the Automation object doesn't offer type information; returns 1 if it does

When Visual Basic encounters a statement like

 Sum = Math.Add (2, 2) 

it calls GetIDsOfNames to convert "Add" into a dispatch ID. It then calls Invoke, passing in the dispatch ID retrieved from GetIDsOfNames. Before calling Invoke, Visual Basic initializes an array of structures with the values of the method parameters—in this case, 2 and 2. It passes the array's address to Invoke along with the address of an empty structure that receives the method's return value (the sum of the two input parameters). The Automation object examines the dispatch ID, sees that it corresponds to the Add method, unpacks the input values, adds them together, and copies the sum to the structure provided by the caller.

A good way to picture this process is to see how a C++ programmer would call an Automation method. The following code is the C++ equivalent of the first example in the previous section:

 // Convert the ProgID into a CLSID. CLSID clsid; ::CLSIDFromProgID (OLESTR ("Math.Object"), &clsid); // Create the object, and get a pointer to its IDispatch interface. IDispatch* pDispatch; ::CoCreateInstance (clsid, NULL, CLSCTX_SERVER, IID_IDispatch,     (void**) &pDispatch); // Get the Add method's dispatch ID. DISPID dispid; OLECHAR* szName = OLESTR ("Add"); pDispatch->GetIDsOfNames (IID_NULL, &szName, 1,     ::GetUserDefaultLCID (), &dispid); // Prepare an argument list for the Add method. VARIANTARG args[2]; DISPPARAMS params = { args, NULL, 2, 0 }; for (int i=0; i<2; i++) {     ::VariantInit (&args[i]);    // Initialize the VARIANT.     args[i].vt = VT_I4;          // Data type = 32-bit long     V_I4 (&args[i]) = 2;         // Value = 2 } // Call Add to add 2 and 2. VARIANT result; ::VariantInit (&result); pDispatch->Invoke (dispid, IID_NULL, ::GetUserDefaultLCID (),     DISPATCH_METHOD, &params, &result, NULL, NULL); // Extract the result. long lResult = V_I4 (&result); // Clear the VARIANTs. ::VariantClear (&args[0]); ::VariantClear (&args[1]); ::VariantClear (&result); // Release the Automation object. pDispatch->Release (); 

You can plainly see the calls to IDispatch::GetIDsOfNames and IDispatch::Invoke, as well as the ::CoCreateInstance statement that creates the Automation object. You can also see that input and output parameters are packaged in structures called VARIANTARGs, a subject that's covered in the next section.

You also use IDispatch::Invoke to access Automation properties. You can set the fourth parameter, which was equal to DISPATCH_METHOD in the preceding example, to DISPATCH_PROPERTYPUT or DISPATCH_PROPERTYGET to indicate that the value of the property named by the dispatch ID in parameter 1 is being set or retrieved. In addition, IDispatch::Invoke can return error information in an EXCEPINFO structure provided by the caller. The structure's address is passed in Invoke's seventh parameter; a NULL pointer means the caller isn't interested in such information. Invoke also supports Automation methods and properties with optional and named arguments, which matters little to C++ clients but can simplify client code written in Visual Basic.

It should be evident from these examples that Automation leaves something to be desired for C++ programmers. It's faster and more efficient for C++ clients to call conventional COM methods than it is for them to call Automation methods. Automation looks easy in Visual Basic for the simple reason that Visual Basic goes to great lengths to make it look easy. Peel away the façade, however, and Automation looks very different indeed.

Automation Data Types

One of the more interesting aspects of IDispatch::Invoke is how it handles input and output parameters. In Automation, all parameters are passed in data structures called VARIANTs. (Technically, input parameters are passed in VARIANTARGs and output parameters in VARIANTs, but because these structures are identical, developers often use the term VARIANT to describe both.) A VARIANT is, in essence, a self-describing data type. Inside a VARIANT is a union of data types for holding the VARIANT's data and a separate field for defining the data type. Here's how the structure is defined in Oaidl.idl:

 struct tagVARIANT {     union {         struct __tagVARIANT {             VARTYPE vt;             WORD    wReserved1;             WORD    wReserved2;             WORD    wReserved3;             union {                 LONG          lVal;         /* VT_I4                */                 BYTE          bVal;         /* VT_UI1               */                 SHORT         iVal;         /* VT_I2                */                 FLOAT         fltVal;       /* VT_R4                */                 DOUBLE        dblVal;       /* VT_R8                */                 VARIANT_BOOL  boolVal;      /* VT_BOOL              */                 _VARIANT_BOOL bool;         /* (obsolete)           */                 SCODE         scode;        /* VT_ERROR             */                 CY            cyVal;        /* VT_CY                */                 DATE          date;         /* VT_DATE              */                 BSTR          bstrVal;      /* VT_BSTR              */                 IUnknown *    punkVal;      /* VT_UNKNOWN           */                 IDispatch *   pdispVal;     /* VT_DISPATCH          */                 SAFEARRAY *   parray;       /* VT_ARRAY             */                 BYTE *        pbVal;        /* VT_BYREFVT_UI1       */                 SHORT *       piVal;        /* VT_BYREFVT_I2        */                 LONG *        plVal;        /* VT_BYREFVT_I4        */                 FLOAT *       pfltVal;      /* VT_BYREFVT_R4        */                 DOUBLE *      pdblVal;      /* VT_BYREFVT_R8        */                 VARIANT_BOOL *pboolVal;     /* VT_BYREFVT_BOOL      */                 _VARIANT_BOOL *pbool;       /* (obsolete)           */                 SCODE *       pscode;       /* VT_BYREFVT_ERROR     */                 CY *          pcyVal;       /* VT_BYREFVT_CY        */                 DATE *        pdate;        /* VT_BYREFVT_DATE      */                 BSTR *        pbstrVal;     /* VT_BYREFVT_BSTR      */                 IUnknown **   ppunkVal;     /* VT_BYREFVT_UNKNOWN   */                 IDispatch **  ppdispVal;    /* VT_BYREFVT_DISPATCH  */                 SAFEARRAY **  pparray;      /* VT_BYREFVT_ARRAY     */                 VARIANT *     pvarVal;      /* VT_BYREFVT_VARIANT   */                 PVOID         byref;        /* Generic ByRef        */                 CHAR          cVal;         /* VT_I1                */                 USHORT        uiVal;        /* VT_UI2               */                 ULONG         ulVal;        /* VT_UI4               */                 INT           intVal;       /* VT_INT               */                 UINT          uintVal;      /* VT_UINT              */                 DECIMAL *     pdecVal;      /* VT_BYREFVT_DECIMAL   */                 CHAR *        pcVal;        /* VT_BYREFVT_I1        */                 USHORT *      puiVal;       /* VT_BYREFVT_UI2       */                 ULONG *       pulVal;       /* VT_BYREFVT_UI4       */                 INT *         pintVal;      /* VT_BYREFVT_INT       */                 UINT *        puintVal;     /* VT_BYREFVT_UINT      */                 struct __tagBRECORD {                     PVOID         pvRecord;                     IRecordInfo * pRecInfo;                 } __VARIANT_NAME_4;         /* VT_RECORD            */             } __VARIANT_NAME_3;         } __VARIANT_NAME_2;         DECIMAL decVal;     } __VARIANT_NAME_1; }; 

The vt field holds one or more VT_ flags identifying the data type. Another field holds the actual value. For example, a VARIANT that represents a 32-bit long equal to 128 has a vt equal to VT_I4 and an lVal equal to 128. The header file Oleauto.h defines macros for reading and writing data encapsulated in VARIANTs. In addition, the system file Oleaut32.dll includes API functions, such as ::VariantInit and ::VariantClear, for managing and manipulating VARIANTs, and MFC's COleVariant class places a friendly wrapper around VARIANT data structures and the operations that can be performed on them.

When you write Automation objects, you must use Automation-compatible data types—that is, data types that can be represented with VARIANTs—for all the objects' properties and methods. The table on the below summarizes the available data types.

VARIANT-Compatible Data Types

Data Type Description
BSTR Automation string
BSTR* Pointer to Automation string
BYTE 8-bit byte
BYTE* Pointer to 8-bit byte
BYTE 8-bit byte
CHAR 8-bit character
CHAR* Pointer to 8-bit character
CY* 64-bit currency value
DATE 64-bit date and time value
DATE* Pointer to 64-bit date and time value
DECIMAL* Pointer to DECIMAL data structure
DOUBLE Double-precision floating point value
DOUBLE* Pointer to double-precision floating point value
FLOAT Single-precision floating point value
FLOAT* Pointer to single-precision floating point value
IDispatch* IDispatch interface pointer
IDispatch** Pointer to IDispatch interface pointer
INT Signed integer (32 bits on Win32 platforms)
INT* Pointer to signed integer
IUnknown* COM interface pointer*
IUnknown** Pointer to COM interface pointer
LONG 32-bit signed integer
LONG* Pointer to 32-bit signed integer
PVOID Untyped pointer
SAFEARRAY* SAFEARRAY pointer
SAFEARRAY** Pointer to SAFEARRAY pointer
SCODE COM HRESULT
SCODE* Pointer to COM HRESULT
SHORT 16-bit signed integer
SHORT* Pointer to 16-bit signed integer
UINT Pointer unsigned integer
UINT* Pointer unsigned integer
ULONG 32-bit unsigned integer
ULONG* Pointer to 32-bit unsigned integer
USHORT 16-bit unsigned integer
USHORT* Pointer to 16-bit unsigned integer
VARIANT* Pointer to VARIANT data structure
VARIANT_BOOL Automation Boolean
VARIANT_BOOL* Pointer to Automation Boolean

Generally speaking, Automation's dependence on VARIANT-compatible data types is a limitation that can frustrate developers who are accustomed to building "pure" COM objects—objects that use conventional COM interfaces rather than dispinterfaces and are therefore less restricted in the types of data that they can use. However, using only Automation-compatible data types offers one advantage: COM knows how to marshal VARIANTs, so Automation objects don't require custom proxy/stub DLLs. The trade-off is that you can't use structures (or pointers to structures) in methods' parameter lists, and arrays require special handling because they must be encapsulated in structures called SAFEARRAYs.

The BSTR Data Type

Most of the data types presented in the preceding section are self-explanatory. Two, however, merit further explanation. BSTR (pronounced "Bee'-ster") is Automation's string data type. Unlike a C++ string, which is an array of characters followed by a zero delimiter, a BSTR is a counted string. The first four bytes hold the number of bytes (not characters) in the string; subsequent bytes hold the characters themselves. All characters in a BSTR are 16-bit Unicode characters. A BSTR value is actually a pointer to the first character in the string. (See Figure 20-1.) The string is, in fact, zero-delimited, which means that you can convert a BSTR into a C++ string pointer by casting it to an LPCWSTR.

Figure 20-1. The BSTR data type.

In MFC, dealing with BSTRs frequently means converting CStrings to BSTRs and BSTRs to CStrings. CString::AllocSysString creates a BSTR from a CString:

CString string = _T ("Hello, world"); BSTR bstr = string.AllocSysString (); 

AllocSysString will automatically convert ANSI characters to Unicode characters if the preprocessor symbol _UNICODE is not defined, indicating that this is an ANSI program build. CString also includes a member function named SetSysString that can be used to modify an existing BSTR.

Converting a BSTR into a CString is equally easy. CString's LPCWSTR operator initializes a CString from a BSTR and conveniently converts the characters to 8-bit ANSI characters if the CString is of the ANSI variety:

CString string = (LPCWSTR) bstr; 

Be aware that if a BSTR contains embedded zeros (a very real possibility since BSTRs are counted strings), turning it into a CString in this way will effectively truncate the string.

The SAFEARRAY Data Type

SAFEARRAY is Automation's array data type. It's called a "safe" array because in addition to holding the data comprising the array elements, it houses information regarding the number of dimensions in the array, the bounds of each dimension, and more.

A SAFEARRAY is actually a structure. It is defined this way in Oaidl.h:

typedef struct  tagSAFEARRAY     {     USHORT cDims;     USHORT fFeatures;     ULONG cbElements;     ULONG cLocks;     PVOID pvData;     SAFEARRAYBOUND rgsabound[ 1 ];     } SAFEARRAY; 

SAFEARRAYBOUND is also a structure. It is defined like this:

typedef struct  tagSAFEARRAYBOUND     {     ULONG cElements;     LONG lLbound;     } SAFEARRAYBOUND;  

The cDims field holds the number of dimensions in the SAFEARRAY. rgsabound is an embedded array that contains one element for each dimension. Each element defines the bounds (number of storage elements) of one dimension as well as the index of that dimension's lower bound. Unlike C++ arrays, which number their elements from 0 to n, a SAFEARRAY's elements can be numbered using any contiguous range of integers—for example, -5 to n-5. fFeatures holds flags specifying what kind of data the SAFEARRAY stores and how the SAFEARRAY is allocated. cbElements holds the size, in bytes, of each element. Finally, pvData points to the elements themselves.

The Windows API includes numerous functions for creating and using SAFEARRAYs; all begin with the name SafeArray. MFC has its own way of dealing with SAFEARRAYs in the form of a class named COleSafeArray. The following code creates a COleSafeArray object that represents a one-dimensional SAFEARRAY containing the integers 1 through 10:

COleSafeArray sa; LONG lValues[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; sa.CreateOneDim (VT_I4, 10, lValues); 

The address of the VARIANT data structure in which the SAFEARRAY is stored can be retrieved with COleSafeArray's LPVARIANT or LPCVARIANT operator:

VARIANT* pVariant = (LPVARIANT) sa; 

One-dimensional arrays are relatively easy to create with COleSafeArray, but multidimensional arrays require more effort. Suffice it to say that even COleSafeArray can't make SAFEARRAYs as palatable to C++ programmers as ordinary arrays.

Late Binding vs. Early Binding

A C++ programmer seeing Automation for the first time might wonder why dispinterfaces should even exist: the reason for them is far from obvious. Given that Automation objects are inherently more complex and less efficient than their conventional COM counterparts, why not use custom COM interfaces instead of IDispatch interfaces and save developers a lot of time and trouble?

Dispinterfaces were created to allow Visual Basic programmers to use COM objects at a time when Visual Basic flatly didn't support conventional COM interfaces. In its early incarnations, Visual Basic couldn't call COM methods through ordinary interface pointers. Current versions of Visual Basic have partially eliminated this limitation, but to this day, many scripting languages, including VBScript, can talk to COM objects only through IDispatch interfaces.

What's so special about IDispatch? In a nutshell, it prevents a compiler (or an interpreter, as the case may be) from having to understand vtables. A COM interface pointer is really a pointer to a location inside the object that holds the address of a table of function pointers—in C++ parlance, a virtual function table, or vtable. If pMath holds an IMath interface pointer, when a C++ compiler encounters a statement like

pMath->Add (2, 2, &sum); 

it resolves the call by emitting code that extracts the address of the Add method from the interface's vtable. It knows the vtable's layout because the interface definition was #included in a header file. And therein lies the problem. Scripting languages don't understand C++ interface definitions. These languages can't resolve a statement like the following one unless they can somehow pass the method name to the object and ask the object itself to resolve the call:

Sum = Math.Add (2, 2) 

Scripting languages may not understand vtables, but they know IDispatch very well. Given a pointer to an IDispatch interface, they know where they can go in the vtable to find the addresses of GetIDsOfNames and Invoke. It's therefore a simple matter for them to call IDispatch::Invoke and "bind" to a method at run time.

That's the crux of IDispatch: shifting the responsibility for resolving method calls from the client to the object. Programmers call this late binding because the actual binding is done at run time. By contrast, we say that C++ clients use early binding because the bulk of the work required to resolve a method call is performed at compile time.

Dual Interfaces

The drawback to late binding is that it requires a run-time lookup that's not necessary in early binding. That impacts performance. And because of IDispatch's reliance on dispatch IDs, each property or method access nominally requires two round-trips to the server: a call to GetIDsOfNames followed by a call to Invoke. A smart Automation client can minimize round-trips by caching dispatch IDs, but the fact remains that late binding is inherently less efficient than early binding.

The choice between an IDispatch interface and a conventional COM interface is a choice between flexibility and speed. An object that exposes its features through an IDispatch interface serves a wider variety of clients, but an object that uses ordinary COM interfaces serves late binding clients (particularly C++ clients) more efficiently.

Dual interfaces are the COM equivalent of having your cake and eating it, too. A dual interface is an interface that derives from IDispatch. Its vtable includes entries for IDispatch methods (GetIDsOfNames, Invoke, and so on) as well as custom methods. Figure 20-2 shows the layout of the vtable for a dual interface that permits methods named Add and Subtract to be accessed indirectly through IDispatch::Invoke or directly through the vtable. Clients that rely exclusively on IDispatch can call Add and Subtract through IDispatch::Invoke; they won't even realize that the custom portion of the vtable exists. C++ clients, on the other hand, will effectively ignore the IDispatch section of the vtable and use early binding to call Add and Subtract. Thus, the same object can support early and late binding. Notice that methods defined in the custom half of the vtable must use Automation-compatible data types, just as methods exposed through IDispatch::Invoke must.

Figure 20-2. Virtual function table for a dual interface.

For MFC programmers, the greatest impediment to dual interfaces is the amount of effort required to implement them. MFC Technical Note 65 describes how to add dual interfaces to an MFC Automation server, but the procedure isn't for the faint of heart. The best way to do dual interfaces today is to forego MFC and instead use the Active Template Library (ATL), which makes creating dual interfaces truly effortless.

Type Libraries

Most Automation servers are accompanied by type libraries. In his book Inside COM (1997, Microsoft Press), Dale Rogerson describes a type library as "a bag of information about interfaces and components." Given a type library, a client can find out all sorts of interesting things about a COM object, including which interfaces it implements, what methods are present in those interfaces, and what each method's parameter list looks like. A type library can be provided in a separate file (usually with the extension .tlb, although .olb is sometimes used instead) or as a resource embedded in the object's executable file. Regardless of how they're packaged, type libraries are registered in the registry so that clients can easily locate them.

Type libraries can be used in a variety of ways. ActiveX controls, for example, use type information (the kind of data found in type libraries) to tell control containers what kinds of events they're capable of firing. Type libraries can also be used to implement IDispatch interfaces and to provide information to object browsers. But the big reason type libraries are important to Automation programmers is that they permit Visual Basic clients to access a server's Automation methods and properties using the custom portion of a dual interface. Given type information, today's Visual Basic programs can even use conventional COM objects—ones that expose their functionality through custom COM interfaces instead of IDispatch interfaces. Type libraries aren't only for Visual Basic users, however; C++ programmers can use them, too. Shortly, you'll see how you can use ClassWizard to generate wrapper classes that simplify the writing of MFC Automation clients. Significantly, ClassWizard can work its magic only if a type library is available.

How do type libraries get created? You can create them programmatically using COM API functions and methods, but most are created from IDL files. MIDL will read an IDL file and produce a type library from the statements inside it. You can also create type libraries by defining objects and their interfaces in ODL files and compiling them with a special tool called MkTypeLib. IDL files are the preferred method, but Visual C++ still uses ODL files for MFC Automation servers. The following ODL statements define a type library that contains descriptions of an Automation component named Math and a dispinterface named IAutoMath:

[uuid (B617CC83-3C57-11D2-8E53-006008A82731), version (1.0)] library AutoMath {     importlib ("stdole32.tlb");     [uuid (B617CC84-3C57-11D2-8E53-006008A82731)]     dispinterface IAutoMath     {         properties:             [id(1)] double Pi;         methods:             [id(2)] long Add (long a, long b);             [id(3)] long Subtract (long a, long b);     };     [uuid (B617CC82-3C57-11D2-8E53-006008A82731)]     coclass Math     {         [default] dispinterface IAutoMath;     }; }; 

The importlib statement in ODL is analogous to #include in C++. uuid assigns a GUID to an object or interface, and dispinterface defines a dispinterface. Statements inside a dispinterface block declare Automation methods and properties as well as their dispatch IDs. The object in this example features a property named Pi and methods named Add and Subtract. Their dispatch IDs are 1, 2, and 3, respectively.

When you write an MFC Automation server, AppWizard creates an ODL file and adds it to the project. Each time a method or property is added, ClassWizard modifies the ODL file so that the next build will produce an up-to-date type library. As long as you use the MFC wizards to craft MFC Automation servers, type libraries are a natural consequence of the build process and require no extra effort to generate.



Programming Windows with MFC
Programming Windows with MFC, Second Edition
ISBN: 1572316950
EAN: 2147483647
Year: 1999
Pages: 101
Authors: Jeff Prosise

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