Page #44 (IDL Syntax)

< BACK  NEXT >
[oR]

Automation

One of the goals COM had was to let the component developers select the programming language of their choice. This was a very ambitious goal, given that each language has its own set of data types and that there is no safe mapping between data types from one language to another.

Let s examine these problems in detail and see the solution that COM had to offer.

Basic Data Types

The semantics of many data types are not the same under different languages. For example, data type char under Visual C++ has no equivalent match under VB. Data type short under Visual C++ is equivalent to data type integer under VB. Moreover, there is no uniform definition of data types to deal with date and currency.

COM s solution was to restrict the base data types that will work across all languages. The SDK provides an enumerated type called VARTYPE to precisely define the semantics of each of the automation compatible data types. Table 2.8 lists some important VARTYPEs, their precise IDL definition, and the comparable data type in C++ and VB.

Table 2.8. Some Basic Automation Data Types

VARTYPE

IDL Interpretation

Microsoft VC++

Microsoft VB

VT_I2

16-bit, signed

short

Integer

VT_I4

32-bit, signed

long

Long

VT_DATE

Date

DATE (double)

Date

VT_CY

Currency

CY (int64)

Currency

VT_R4

Single precision decimal

float

Single

VT_R8

Double precision decimal

double

Double

VT_UNKNOWN

Interfacepointer

IUnknown*

Interface Ref.

Though unsigned data types appear to be automation-compatible (in the sense that MIDL will not complain), they are not supported across all languages.

graphics/01icon02.gif

Do not use unsigned data types if automation is your objective.


Strings

Previously, we saw that the C/C++ language provides adequate support to deal with strings of OLECHARs that are NULL terminated. However, there are other programming languages, such as VB and Java, that prefer length-prefixed OLECHAR strings. To satisfy all the programming communities, the SDK introduced an extended data type called BSTR.

BSTRs are length-prefixed as well as NULL -terminated strings of OLECHARs. The length prefix indicates the number of bytes the string consumes (not including the terminating NULL) and is stored as a four-byte integer that immediately precedes the first character of the string. Figure 2.4 shows the string Hi as a BSTR.

Figure 2.4. BSTR representation of Hi. Adapted from [Box-98].
graphics/02fig04.gif

From Figure 2.4, it is obvious that one can treat a BSTR as an LPWSTR. However, you cannot treat an LPWSTR as a BSTR. The marshaler packs data using the value specified in the memory preceding the location pointed by the BSTR. If an LPWSTR is used as a BSTR, this memory location could be either an invalid address or would contain a bogus value. This may result in either an access violation (if you are lucky) or huge amounts of bogus data being transmitted.

graphics/01icon02.gif

Never use an LPWSTR as a BSTR (it s okay to use a BSTR as an LPWSTR).


To manage BSTRs, SDK provides several API functions. The SDK documentation describes the API usage in detail. Two important APIs that I will cover here deal with memory allocation issues.

Allocation of BSTR does not go through the COM task memory allocator directly in the sense that APIs such as CoTaskMemAlloc and CoTaskMemFree cannot be used. Instead, COM provides two other APIs, SysAllocString and SysFreeString, to deal with BSTRs. The following are their prototypes:

 BSTR SysAllocString(const OLECHAR* sz);  void SysFreeString(BSTR bstr); 

SysAllocString can be used to allocate and initialize a BSTR. SysFreeString can be used to free the memory previously allocated via a call to SysAllocString.

You may be wondering how the marshaler knows that BSTRs require special allocation APIs and not the standard task memory allocator APIs. The magic lies in IDL s support for defining wire representation of a data type, BSTR in our case. Al Major covers this topic in detail in [Maj-99].

When a BSTR is passed as an [in] parameter, it is the caller s responsibility to construct the parameter before invoking the method and to free the memory after returning from the method. The following example shows the version of the interface method StringParam revised to use BSTR:

 // Interface method definition  HRESULT RevisedStringParam([in] BSTR bstrVal); 

Using this interface method, the client-side code can be modified as:

 // Client code  LPOLESTR pwszName = OLESTR("Alexander, The Great");  BSTR bstrName = ::SysAllocString(pwszName);  HRESULT hr = pMyExplore->RevisedStringParam(bstrName);  ::SysFreeString(bstrName); 

ATL simplifies using BSTRs by providing a class called CComBSTR. Using this class, the client-side code can be written as:

 #include <atlbase.h>  ...  CComBSTR bstrName = "Alexander, The Great";  HRESULT hr = pMyExplore->RevisedStringParam(bstrName); 

Note that the client project need not be ATL-based. To use the class CComBSTR, all that is needed is the header file <atlbase.h>.

Similar to ATL, the native COM support under Visual C++ defines another class, _bstr_t, to deal with BSTRs. Following is our client-side code using this class:

 #include <comdef.h>  ...  _bstr_t bstrName = "Alexander, The Great";  HRESULT hr = pMyExplore->RevisedStringParam(bstrName); 

When a BSTR is passed as an [out] parameter, it is the server s responsibility to allocate the string and the caller s responsibility to free it, as illustrated in the following code snippet:

 // interface method definition  HRESULT GetString([out] BSTR* pVal);  // server side code  STDMETHODIMP CMyExplore::GetString(BSTR *pVal)  {   *pVal = ::SysAllocString(OLESTR("Alexander, The  Great"));    if (NULL == *pVal) {     return E_OUTOFMEMORY;    }    return S_OK;  }  // Client side code  BSTR bstrName = NULL;  HRESULT hr = pMyExplore->GetString(&bstrName);  if (SUCCEEDED(hr)) {   // use bstrName    ::SysFreeString(bstrName);  } 

Failure to call SysFreeString will result in a memory leak. Once again, the ATL class CComBSTR can save us from making such a mistake. The destructor of CComBSTR automatically calls this API, if need be:

 // Server side  STDMETHODIMP CMyExplore::GetString(BSTR *pVal)  {   CComBSTR bstrName = "Alexander, The Great";    if (NULL == static_cast<BSTR>(bstrName)) {     return E_OUTOFMEMORY;    }    *pVal = bstrName.Detach();    return S_OK;  }  // Client side  {   CComBSTR bstrName;    HRESULT hr = pMyExplore->GetString(&bstrName);    if (SUCCEEDED(hr)) {     // use bstrName    }  } // bstrName gets freed automatically when leaving the scope 

Unfortunately, class _bstr_t doesn t provide the functionality for an instance to be used as an [out] parameter.

graphics/01icon01.gif

Should you use CComBSTR or _bstr_t ?

Both of them have some distinct advantages.

Class CComBSTR lets you use an instance for an [out] parameter; _bstr_t doesn t.

Class _bstr_t provides reference counting on the instance. Therefore, it is more efficient if its instance is frequently assigned to another instance.


Booleans

The SDK defines a data type called VARIANT_BOOL for dealing with boolean variables. The two possible values for this data type are VARIANT_TRUE and VARIANT_FALSE, which correspondingly map to True and False under VB and VBScript.

Note that IDL defines another data type, boolean, to represent a boolean value. However, this data type is not supported under programming languages other than C/C++ and Java.

graphics/01icon02.gif

To define an automation-compliant boolean parameter, never use boolean or BOOL data types. Always use VARIANT_BOOL.


Variants

Programming languages such as VB and Java support typed data; that is, a variable can be defined to hold a specific data type, as shown in the following VB example:

 Dim Count as Integer  Count = 10 

However, scripting languages such as VBScript and Jscript forego this notion of typed data in favor of increased programming simplicity. These typeless languages support only one data type called a variant. A variant can contain any type of data. When a specific data type is assigned to a variant, or one variant is assigned to another, the run-time system will automatically perform any needed conversion.

Even many typed languages such as VB support variants natively.

The SDK defines a discriminated union to deal with variants. It is called a VARIANT. Each supported data type has a corresponding discriminator value, represented as the VARTYPE. We have seen some of them earlier. Table 2.9 lists some of the frequently used VARTYPEs. For a complete list, check out IDL file wtypes.idl supplied with the SDK.

Table 2.9. Variable Types Supported by a VARIANT

Type

Description

VT_EMPTY

has no value associated with it

VT_NULL

contains an SQL style NULL value

VT_I2

2 byte signed integer (short)

VT_I4

4 byte signed integer (long)

VT_R4

4 byte real (float)

VT_R8

8 byte real (double)

VT_CY

64-bit currency

VT_DATE

DATE (double)

VT_BSTR

BSTR

VT_DISPATCH

IDispatch*

VT_ERROR

HRESULT

VT_BOOL

VARIANT_BOOL

VT_VARIANT

VARIANT*

VT_IUNKNOWN

IUnknown*

VT_DECIMAL

16 byte fixed point

VT_UI2

Unsigned short

VT_UI4

Unsigned long

VT_I8

Signed 64-bit int (int64)

VT_UI8

Unsigned 64-bit int

VT_INT

Signed machine int

VT_UINT

Unsigned machine int

To indicate that a variant is a reference, flag VT_BYREF can be combined with the above tags.

A VARIANT can also be represented as a VARIANTARG type. Typically, VARIANTARG is used to represent the method parameters and VARIANT is used to refer to method results.

As with BSTRs, the SDK provides APIs and macros to deal with VARIANTs.

A variant has to be initialized before it can be used and has to be cleared after it has been used. The SDK provides the following two APIs for these purposes:

 void VariantInit(VARIANTARG* pvarg);  void VariantClear(VARIANTARG* pvarg); 

To understand how to use these APIs, consider a method that accepts a VARIANT as an [in] parameter.

 HRESULT SetCount([in] VARIANT var); 

The following code fragment shows how to pass a long value to this method:

 VARIANT var;  VariantInit(&var);  V_VT(&var) = VT_I4;  V_I4(&var) = 1000;  pMyInterface->SetCount(var);  VariantClear(&var) 

The SDK also provides many other APIs to manipulate variants, such as copying one variant into another or converting one variant type to another. These APIs are all documented in the SDK.

To simplify manipulating variants, ATL provides a class called CComVariant, and Visual C++ natively provides a class called _variant_t. Using CComVariant, for example, the previously defined code fragment can be simplified as:

 CComVariant var(1000);  pMyInterface->SetCount(var); 

As with CComBSTR, the cleanup for CComVariant occurs automatically when the variable goes out of scope.

graphics/01icon02.gif

If you are not using safe wrapper classes such as CComVariant or _variant_t to deal with variants, make it a practice to call VariantClear before any assignment operation. This will obviate any potential resource leak.


Safe Arrays

Programming languages such as C++ support arrays intrinsically. However, it does so with no index protection, no size limit, and no initialization. An array is just a pointer to a random memory location. Even C++ programmers are reluctant to use raw arrays. Many of them write protected wrapper classes to deal with arrays.

VB on the other hand, provides a more protected way of dealing with arrays; it stores the array bounds and does a run-time check to ensure that the boundaries are not violated.

To deal with arrays safely, the SDK defines a data structure similar to the one that VB uses internally. This data structure is called SAFEARRAY. A SAFEARRAY is an array of other automation-compatible data types. The following example shows how to declare a safe array of BSTRs as a method parameter:

 HRESULT GetNameList([out, retval] SAFEARRAY(BSTR)* pNameList); 

Though the above method declaration is automation-compatible, and the marshaler knows how to marshal all the elements of the array, not all languages can deal with safe arrays when declared this way. Our variant data type comes in handy here. A safe array can be passed as a variant, as shown in the following code fragment:

 // prototype  HRESULT SetNames([in] VARIANT nameList);  // client code  SAFEARRAY *pSA = CreateAndFillSafeArrayWithValidStrings();  VARIANT v; ::VariantInit(&v);  V_VT(&v) = VT_ARRAY | VT_BSTR;  V_ARRAY(&v) = pSA;   //pSA should contain each element of type BSTR  HRESULT hr = SetNames(v); 

In the above code, each element in the array is of a BSTR type. This is acceptable to typed languages only. The typeless languages cannot deal with an array of BSTRs. If an array is intended to be used from a typeless language, it should be declared as an array of variants, as follows:

 V_VT(&v) = VT_ARRAY | VT_VARIANT;  V_ARRAY(&v) = pSA;     // pSA should contain each element of type                         // VARIANT 

As with BSTRs, the SDK provides a number of API functions for creating, accessing, and releasing safe arrays. Their use is straightforward and is not covered here. To provide programmatic simplification under C++, one can create a wrapper class to deal with safe arrays. For an excellent article on safe arrays, see Ray Djajadinata s article, Creating a COM Support Class for SAFEARRAY in Visual C++ Developer [Ray-98].

Automation-Compatible Interfaces

If an interface is intended to be used by an automation client, all the methods in the interface should use only the automation-compatible parameters. To ensure this, an interface can be annotated with the oleautomation attribute, as shown below:

 [   ...    oleautomation,    ...  ]  interface IVideo : IUnknown  {   ...  }; 

Doing so directs MIDL to warn if it encounters any method parameter that is not automation-compatible.

Properties and Methods

We know that any COM object exposes one or more interfaces. We also know that each interface contains a set of predefined functions. Generally speaking, these functions can be classified into two subsets one that takes some action, and one that obtains or changes the current state of the object. In order to provide a uniform semantic meaning of an automation object across different languages, COM defined the first set of functions as interface methods and the second set as interface properties.

A method is a member function that specifies an action that an object can perform, such as drawing a line or clearing the screen. A method can take any number of arguments (some of which can be marked as optional), and they can be passed either by value or by reference. A method may or may not have an output parameter.

IDL provides a function level attribute called method to indicate that the function is a method.

A property is a member function that sets or returns information about the state of the object, such as color or visibility. Most properties have a pair of accessor functions a function to get the property value and a function to set the property value. Properties that are read-only or write-only, however, have only one accessor function.

Table 2.10 shows the attributes available on properties.

Table 2.10. Property Attributes

Attribute

Description

propget

obtains the property value

propput

sets the property value

propputref

sets the property value. Value has to be set by reference

Each property or method also requires an identification tag called dispatch identifier or DISPID. The SDK defines DISPID as a value of type long:

 typedef long DISPID; 

A DISPID is set on a method using attribute id, as shown in the following example:

 [id(1), propget] HRESULT GetValue(); 

The SDK predefines many DISPIDs. Their semantics are documented in the SDK. For example, a DISPID of 0 on a property implies that the property is the default value. The behavior is illustrated in the following code fragment:

 // Interface method definition  [propput, id(0)] HRESULT LastName([in] BSTR newVal);  // VB client usage example 1  person.LastName = "Gandhi"     // Standard way of setting a property  // VB client usage example 2  person = "Gandhi"              // Setting LastName property is simplified                                 // because of id(0) 

User-defined DISPIDs start from one.

DISPIDs are scoped by the interface. Therefore, two interfaces can use the same value for a DISPID without any interference.

A property can result in two functions one to get and one to set the property. In this case, both the functions should have the same DISPID.

Specifying DISPIDs is optional. If not specified, a DISPID is automatically assigned.

Run-time Binding

Recall from Chapter 1 that an interface is primarily a vtbl -based binary layout. In order for a C++ client to use an interface, its vtbl information, and the signature of all the methods, has to be available when the client code is being compiled. The client could invoke only those methods it was made aware of during compilation.

Such a vtbl -based binding mechanism works with typed languages such as C++ and VB (using automation-compatible interfaces). However, in general, it is not suitable for automation controllers. Automation controllers do not require the knowledge of any object until the object is created at run time.

What is needed is a mechanism so that an automation controller, or any client, can ask the object at run time if it supports a particular method. If the method is indeed supported, the client would like to know, at run time, the number of parameters and the data type of each parameter so that it can construct the parameter list and invoke the method.

COM provides a way to define an interface that supports such a run-time-binding functionality. However, defining such an interface requires a different IDL syntax; it is declared using the keyword dispinterface (short for dispatch interface), as shown in the following example:

 [   uuid(318B4AD2-06A7-11d3-9B58-0080C8E11F14),    helpstring("DISVideo Interface")  ]  dispinterface DISVideo  {   properties:    methods:      [helpstring("Obtain the S-Video signal value"), id(1)]      long GetSVideoSignalValue();  }; 

Note that the COM programming community has adopted the convention of using the prefix DI on the interface name to indicate that it is a dispinterface type interface.

But by now we know that COM components can communicate using vtbl-based interfaces only. How, then, does the client deal with a dispinterface ?

It turns out a dispinterface is just an abstraction limited to the IDL file. What the client sees is a vtbl -based interface. This interface is defined as IDispatch in the SDK. The interface definition is shown here:

 interface IDispatch : IUnknown  {   HRESULT GetTypeInfoCount( [out] UINT * pctinfo);    HRESULT GetTypeInfo( [in] UINT iTInfo, [in] LCID lcid,      [out] ITypeInfo ** ppTInfo);    HRESULT GetIDsOfNames(     [in] REFIID riid,      [in, size_is(cNames)] LPOLESTR * rgszNames,      [in] UINT cNames,      [in] LCID lcid,      [out, size_is(cNames)] DISPID * rgDispId);    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  ); 

Method GetIDsOfNames returns the DISPID for the given method name. To invoke a method, the client:

  1. obtains the DISPID of the method it is interested in, and

  2. calls IDispatch::Invoke, passing the DISPID as a parameter. The arguments needed by the method are packed as DISPPARAMS and passed to Invoke.

For performance optimization, a client can call GetIDsOfNames during compile time. This is referred to as early binding. Alternatively, GetIDsOfNames could be called during run time, just before the call to Invoke. This is referred to as late binding.

Run-time binding, early or late, will always be slower than vtbl -based binding. If a client is capable of supporting vtbl -based binding, such as the one written in C++, why enforce run-time binding?

What would be nice is to satisfy both vtbl -based clients as well as clients requiring run-time binding.

So far in our examples we derived our custom interfaces from IUnknown. What if we derive the interfaces from IDispatch instead?

Bingo! You are on the right track. You just laid out the mechanism for defining an interface that has the speed of direct vtbl binding and the flexibility of IDispatch binding. An interface defined this way is referred to as a dual interface.

However, there is one small issue that is left; deriving an interface from IDispatch doesn t automatically tell the IDL compiler that the interface is a dual interface. You have to annotate your interface with a dual keyword. The following example shows our interface IVideo defined as a dual interface:

 [   object,    uuid(318B4AD0-06A7-11d3-9B58-0080C8E11F14),    helpstring("IVideo Interface"),    dual,    pointer_default(unique)  ]  interface IVideo : IDispatch  {   [helpstring("Obtain the signal value")]    HRESULT GetSignalValue([out, retval] long* val);  }; 

The Server Side of the Story. How does the server code support dispinterface or a dual interface?

In order to support one or more dispinterfaces for an object, the server has to implement IDispatch interface.

The implementation of IDispatch::GetIDsOfNames would return a unique DISPID per method name.

The implementation of IDispatch::Invoke would examine the passed DISPID and would call the appropriate method on the object.

Implementing IDispatch is a very tedious process. Fortunately, ATL simplifies it by providing some C++ template classes. Richard Grimes book Professional ATL COM Programming [Gri-98] discusses this in detail. In the chapters to follow, we will be using ATL wizard to generate IDispatch implementation.

Collections and Enumerations

A topic on automation would not be complete without talking about collections and enumerations.

Automation clients frequently need to deal with sets of objects or variants that should be grouped together. An example of such a collection is a list of files in a subdirectory.

While there is no specific interface to define a collection, there are certain methods that a collection object is expected to implement.

Count

To allow the clients to determine the number of items in the collection, a collection object should implement a Count method. The following is its prototype:

 HRESULT Count([out, retval] long* count); 
Item

To fetch a particular item, a method called item should be implemented. The first parameter to the method is the index of the item. The data type for the index is dictated by the implementer and could be numeric, BSTR, or VARIANT.

Enumeration

A collection object should also allow the client to iterate over the items in the set. The following VBScript example shows the usage of such a collection object:

 'the filelist object has been obtained somehow. Iterate through  'and display the file names  for each file in fileList    msgbox file.name  next 

In order to support such an iteration style, the collection object should support a method with a DISPID of DISPID_NEWENUM. This method conventionally is named _NewEnum and returns a pointer to IUnknown, as shown in the following code snippet:

 [id(DISPID_NEWENUM)] HRESULT _NewEnum(   [out, retval] IUnknown **ppunk); 

graphics/01icon01.gif

An underscore preceding any method indicates that the method be hidden. It will not be displayed to users from tools such as VB s auto-statement completion.

DISPID_NEWENUM is defined as 4 in the SDK. If you come across a method that has a DISPID of 4 (or its equivalent 0xFFFFFFFC), it indicates that the method returns an enumeration object.


When this method is called on the object, the object should return a pointer to an interface IEnumVARIANT (defined in the SDK). Interface IEnumVARIANT falls into a category of interfaces called enumeration. These interfaces are typically prefixed as IEnum and are expected to support some methods such as Next and Reset. The semantics of these methods is documented in the SDK.

There are some other optional methods that a collection can support. Charlie Kindel s article on MSDN, Implementing OLE Automation Collection, [Kin-94] delineates the rules of automation collection fairly clearly.

ATL makes it easy to implement collections and enumerators. See Richard Grimes article in Visual C++ Developer s Journal [Gri-99] for some sample code.


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

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