Page #43 (IDL File Structure)

< BACK  NEXT >
[oR]

IDL Syntax

It is time for us to start designing interfaces. The approach I have taken here is bottoms-up. We will first see how to define interface methods and related issues. Then we will take a look at building interfaces. Finally, we will focus on building the library section.

In order to explore various IDL concepts and issues, I have defined an interface, IMyExplore, for our reference. The server class that implements this interface will be referred to as CMyExplore. The sample project can be found on the accompanying CD.

Defining Interface Methods

Interface methods are defined within the scope of an interface. The following is an example of an interface defining a method, namely GetRandomValue.

 interface IMyExplore : IUnknown  {   [helpstring("Obtain a random value")]    HRESULT GetRandomValue([out] long* pVal);  }; 

An interface method consists of:

  1. A return value: Almost always, the return value is an HRESULT.

  2. A name: Any valid C-style name can be used.

  3. Parameters: A method can have zero or more parameters. Each parameter can have its own list of attributes.

In addition, each method can be optionally annotated with attributes. The most common attribute is helpstring, which is the equivalent of a C language comment.

The following code fragment shows the server-side implementation and the client-side usage of the method GetRandomValue:

 // Server side implementation  STDMETHODIMP CMyExplore::GetRandomValue(long *pVal)  {   *pVal = rand();    return S_OK;  }  // Client side usage  long lVal;  HRESULT hr = pMyExplore->GetRandomValue(&lVal); 
Directional Attributes

In order for the parameters to be marshaled, the marshaling logic needs to know the semantics of data transfer during the method invocation. The IDL specifies two attributes, [in] and [out], for this purpose. We have already seen them in passing.

  • An [in] attribute specifies that the data has to be transferred from the client to the server.

  • An [out] attribute specifies that the data has to be transferred from the server to the client.

  • An [in, out] combination specifies that the data has to be transferred from the client to the server and back to the client.

IDL method signatures have C-style call-by-value semantics. Therefore, all parameters with the [out] attribute have to be pointers to the type actually returned.

The following example illustrates the use of these attributes:

 // Interface method definition  HRESULT DirectionDemo(   [in] long val1,    [out] long* pVal2,    [in, out] long* pVal3,    [out] long* pVal4,    [in] long val5);  // Server implementation  STDMETHODIMP CMyExplore::DirectionDemo(long val1, long *pVal2,    long *pVal3, long *pVal4, long val5)  {   *pVal2 = val1 + 100;    *pVal3 = *pVal3 + 200;    *pVal4 = val5 + 300;    return S_OK;  }  // Client implementation  void DirectionDemo(IMyExplore* pMyExplore)  {   cout << "\n\nDirection demo test" << endl;    long val1 = 1, val2 = 2, val3 = 3, val4 = 4, val5 = 5;    cout << "Before: " << val1 << ", " << val2 << ", "      << val3 << ", "      << val4 << ", "      << val5 << endl;    HRESULT hr = pMyExplore->DirectionDemo(val1, &val2, &val3,      &val4, val5);    if (FAILED(hr)) {     DumpError(hr);      return;    }    cout << "After: " << val1 << ", " << val2 << ", "      << val3 << ", "      << val4 << ", "      << val5 << endl;  }  // Output  Direction demo test  Before: 1, 2, 3, 4, 5  After: 1, 101, 203, 305, 5 

Besides indicating the direction, the directional attributes also indicate the ownership of the memory allocated. The basic rules of memory management are:

  • For an [in] parameter, the memory is allocated and freed by the client.

  • For an [out] parameter, the memory is allocated by the server and freed by the client.

  • For an [in, out] parameter, the memory is allocated by the client, freed by the server, allocated once again by the server, and finally freed by the client. If possible, the two server-side operations can be combined as one reallocation action.

The memory allocations operations are primarily done using COM s memory management APIs that we discussed earlier. The marshaling mechanism uses these APIs when necessary.

Note that not all parameters require us to deal with memory management. Memory management rears its head when we start dealing with arrays and strings, as we will see later in the chapter.

Logical Return Value

Recall our example interface method that returned a random value as output. Let s look at the client code one more time.

 long lVal;  HRESULT hr = pMyExplore->GetRandomValue(&lVal); 

The method returns two values a physical return value of type HRESULT, and a logical return value of type long. To indicate that a parameter is the logical return value of a method, IDL supports [retval] as a parameter attribute, as illustrated in the following IDL statement:

 HRESULT GetRandomValue([out, retval] long* pVal); 

In environments that support the notion of logical return value, [retval] attribute indicates that the parameter should be mapped as the result of the operation. For example, VB maps the above definition as:

 Function GetRandomValue() as long 

The support for native COM within Visual C++ also makes it possible to map the parameter as the result of the operation. Using the native COM support, the client code can simply be written as:

 long lVal= pMyExplore->GetRandomValue(); 

Needless to say, there can be only one logical return value for a method, and that the attribute [retval] can be applied only to an [out] type parameter.

Base Data Types

Like any other programming language, IDL has a set of base data types. Table 2.5 provides a description of each of the base data types and their equivalent C++ representation.

Table 2.5. IDL Base Data Types

IDL Data Type

Visual C++ Equivalent

Description

void

void

A method with no parameters or a method that does not return a result value

boolean

unsigned char

Indicates TRUE or FALSE

byte

unsigned char

An 8-bit data item

small

char

An 8-bit integer number

short

short

A 16-bit integer number

int

int

A 32-bit integer number on a 32-bit platform

long

long

A 32-bit integer

hyper

_int64

A 64-bit integer

float

float

A 32-bit floating point number

double

double

A 64-bit floating point number

char

unsigned char

An 8-bit data item. Undergoes ASCII-EBCDIC conversion on transmission

wchar_t

unsigned short

16-bit unicode character

A parameter can be specified with any of these base types, or a pointer to a base type. MIDL recognizes these data types as intrinsic and generates marshaling code without requiring extra attributes other than [in] and [out].

Note that unlike C, a pointer to the base type is considered to be a pointer to a single value and not to an array. In order to specify an array, additional parameter attributes are needed. We will cover this later in the chapter.

IDL also allows signed and unsigned versions of all integer data types and pointers to all integer data types.

The base data types have a fixed size and sign. This information is essential for the NDR data format to achieve platform and architectural independence. All base data types, except int, have a well-defined C/C++ trans-lation. Data type int, however, is a special case. Its NDR data format is fixed, but its C/C++ translation is dependent on the architecture.

graphics/01icon02.gif

If possible, avoid using int as a data type when designing interfaces. Instead, use short, long, or hyper, as appropriate.


The base data types can be used to define C-style structures and unions. These new data types are referred to as extended data types.

String Parameters

In the COM community, there are many definitions of string types. Visual C++ natively supports char* and wchar_t* string types. Win32 API provides LPSTR, LPWSTR, and LPTSTR string types. The COM SDK provides OLECHAR* and OLESTR types. IDL supports an extended data type called BSTR to deal with strings. Then there are C++ classes called CComBSTR and _bstr_t available from ATL and Visual C++ environment, respectively.

Let s clear up the confusion surrounding all these definitions.

A string is typically represented as a null-terminated array of char type. Win32 SDK defines a macro LPSTR to indicate this. For the English language, each character in the string can be represented by exactly one byte. However, there are other regional languages that may require a variable number of bytes (some languages require up to three bytes) to represent a character. Hence, a char type string (LPST R) is often referred to as a multibyte string. In practice, the programming community prefers dealing with fixed byte strings. A good compromise was to define a character type that requires exactly two bytes for its representation. Such a character was termed a UNICODE character (also referred to as a wide character). A UNICODE character can deal with most regional languages. As a matter of fact, a UNICODE string can deal with multiple regional languages.

The Microsoft run-time library defines a standard data type called wchar_t to indicate a UNICODE character. The Win32 SDK defines a macro, WCHAR, to represent a UNICODE character and another macro, LPWSTR, to indicate a UNICODE string (which simply maps to data type wchar_t*).

To provide source code portability between UNICODE and non-UNICODE builds, the SDK provides two macros TCHAR and LPTSTR. These macros map to WCHAR and LPWSTR under UNICODE builds, and char and LPSTR under non-UNICODE builds. The Win32 SDK also provides macros to represent constant strings; these are LPCSTR, LPCWSTR, and LPCTSTR.

Now let s look at the string representation under COM.

Given that a string is an array of characters, it is tempting to assume that a string would get represented as a pointer to char, as in the following example:

 HRESULT StringParam([in] char* pszString); 

Not quite. Under IDL semantics, a pointer is assumed to point to a single instance, not to an array of characters. The above IDL statement would result in marshaling only the first character of the string.

To indicate that a parameter is a null-terminated array of characters, IDL introduces an attribute called [string]. Following is an example:

 HRESULT StringParam([in, string] char* pszString) 

While a pointer to a char data type can be used as a parameter, the programming community prefers using UNICODE strings. As wchar_t is not defined on many platforms, the COM SDK defined a data type called OLECHAR to represent UNICODE characters. Under Win32 platform, this simply maps to data type wchar_t, as you would expect.

Using OLECHAR as a data type, we can modify our example as:

 HRESULT StringParam([in, string] OLECHAR* pwszString) 

The above IDL statement implies that parameter pwszString is a nullterminated UNICODE string.

The following code snippet shows the implementation for the StringParam interface method:

 STDMETHODIMP CMyExplore::StringParam(OLECHAR *pwszName)  {   printf("String is %S\n", pwszName);    return S_OK;  } 

The following code illustrates its use on Win32 platform using Visual C++:

 wchar_t* pwszName = L"Alexander, the Great";  HRESULT hr = pMyExplore->StringParam(pwszName); 

Note that under Visual C++ a UNICODE string can be created using the L prefix, as shown above.

The SDK also provides a macro called OLESTR to help developers create a null-terminated UNICODE string. The pointer to such a string is represented by LPOLESTR. Using these two macros, the previous client code can be modified as:

 LPOLESTR pwszName = OLESTR("Alexander, the Great");  hr = p->StringParam(pwszName); 

Table 2.6 summarizes string data type definitions under various groups.

Table 2.6. String Data Type Definitions

Data Type

VC++ Native

Win32

Generic Win32

COM

Character

char

CHAR

TCHAR

wchar_t

WCHAR

OLECHAR

String Representation

Hello World

Hello World

_T( Hello World )

L Hello World

L Hello World

OLESTR

("Hello World")

String Pointer

char*

LPSTR

LPTSTR

wchar_t*

LPWSTR

LPOLESTR

Constant

const char*

LPCSTR

LPCTSTR

const

wchar_t*

LPCWSTR

LPCOLESTR

There is yet another string type called BSTR that is frequently used as a method parameter. We will cover this string type when we discuss automation.

There is often a need to convert multibyte strings to UNICODE strings and vice-versa. The Win32 SDK provides two APIs, MultiByteToWideChar and WideCharToMultiByte, to achieve this. In reality, however, developers most often end up converting UNICODE strings to portable multibyte type (LPTSTR) and vice-versa. Under non-UNICODE builds, this conversion should call one of the two above-mentioned APIs as appropriate. Under UNICODE builds, however, there is no real need for conversion.

To simplify coding, ATL provides two macros, W2T and T2W, that can be used for string conversions. The following example illustrates the use of one of these APIs:

 void OutputString(const WCHAR* pwszString)  {   USES_CONVERSION; // Should be declared before calling any                     // ATL conversion macros    LPTSTR pszString = W2T(pwszString);    printf("%s\n", pszString);  } 

As the conversion requires some memory to be allocated to hold the return value, the above-mentioned macros, and many of their variants, internally use a standard run-time routine called _alloca. This routine allocates memory on stack. The cleanup occurs when the calling function returns.

Note that there are two potential problems when using ATL-provided conversion macros.

First, if the macro is used in a C++ exception handling code, as shown below, the current implementation of Visual C++ compiler causes a memory corruption.

 try {   ...  }catch(CMyException& e) {   USES_CONVERSION;    LPTSTR pszError = W2T(e.GetErrorString());    DisplayError(pszError);  } 

graphics/01icon02.gif

Under Visual C++, within the catch block, do not use ATL-defined conversion macros or any other macro that uses _alloca.


The second problem is that ATL just assumes that the memory allocation via _alloca never fails; there is no validity check on the memory pointer returned from _alloca. When such a macro is used within some iteration logic, for example, and the iteration count happens to be a large value, there is a potential of crashing the program. The following code fragment illustrates this behavior:

 for(int i = 0; i<nRecords; i++)   { // The number of records in the                                      // database could be in millions    CMyRecord* p = GetRecord(i);    LPTSTR pszName = W2T(p->GetName());    ...  } 

The reason for the crash is that the memory allocated via _alloca gets freed when the function returns, not when the variable holding the memory location goes out of scope (a popular misconception). Within the for loop in the above code, the process runs out of memory at some point. In this case, _alloca returns NULL. However, ATL still goes ahead and uses the return value, resulting in a potential crash.

graphics/01icon02.gif

Do not use ATL conversion macros in a loop where the iteration count could potentially be very large.


Enumeration Data Type

IDL supports enumerated types with syntax identical to that of their C counterpart, as in the following example:

 enum MYCOLOR {MYRED, MYGREEN, MYBLUE}; 

The enumeration can be used as a parameter to a method, as in the following example:

 // Interface method definition  HRESULT GetEnum([out] enum MYCOLOR* pVal);  // Server code  STDMETHODIMP CMyExplore::GetEnum(enum MYCOLOR *pVal)  {   *pVal = MYRED;    return S_OK;  }  // Client code  enum MYCOLOR color;  HRESULT hr = pMyExplore->GetEnum(&color); 

To simplify enum data type declaration, enum definitions can also be used along with C-style typedefs. In this case, we can drop the keyword enum from the interface method declaration, as shown below:

 typedef enum { MYRED, MYBLUE, MYGREEN } MYCOLOR;  // Interface method definition  HRESULT GetEnum([out] MYCOLOR* pVal);  // Server code  STDMETHODIMP CMyExplore::GetEnum(MYCOLOR *pVal)  {   *pVal = MYRED;    return S_OK;  }  // Client code  MYCOLOR color;  HRESULT hr = pMyExplore->GetEnum(&color); 

Note that an enum type variable can be assigned only one value from the possible list of values. It cannot be used to pass a combination of values, as in the following case:

 // Server code  STDMETHODIMP CMyExplore::GetEnum(MYCOLOR *pVal)  {   *pVal = MYRED | MYGREEN;    return S_OK;  } 

If the value to be returned does not exactly match one of the possible values, the marshaler fails to marshal the value.

graphics/01icon02.gif

If a method intends to pass a combination of enumerated values as a parameter, declare the parameter as a long type, instead of the enum type.


An enum can be uniquely identified by a GUID, if desired, as shown here:

 typedef  [   uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),  ] enum {MYRED, MYGREEN, MYBLUE } MYCOLOR; 

By default, the NDR format for enum is a 16-bit unsigned short. To speed up transmission on 32-bit architectures, it is desirable to have enum values transmitted as 32 bits. IDL defines a v1_enum attribute just for this case:

 typedef  [   v1_enum,    uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),  ] enum {MYRED, MYGREEN, MYBLUE } MYCOLOR; 

The enumeration and each enumeration constant can have a helpstring as shown here:

 typedef  [   v1_enum,    uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),    helpstring("This is my color enumeration")  ] enum {   [helpstring("This is my red")] MYRED,    [helpstring("This is my green")] MYGREEN,    [helpstring("This is my blue")] MYBLUE  }MYCOLOR; 

The default value for an enumeration starts from zero. If desired, each enumeration item can be assigned an individual value, as shown here:

 typedef  [   v1_enum,    uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),    helpstring("This is my color enumeration")  ] enum {   [helpstring("This is my red")] MYRED  = 0x0001,    [helpstring("This is my green")] MYGREEN = 0x0002,    [helpstring("This is my blue")] MYBLUE = 0x0004  }MYCOLOR; 
Structures

An IDL struct is almost identical to a C struct except for the following differences:

  • Each field in an IDL struct can have one or more attributes.

  • For structs that require marshaling, bit fields and function declarations are not permitted.

Following is an example of an IDL struct:

 // Interface Definiton  typedef struct tagMYPOINT  {   long lX;    long lY;  }MYPOINT;  HRESULT StructDemo([out, retval] MYPOINT* pVal);  // Server side code  STDMETHODIMP CMyExplore::StructDemo(MYPOINT *pVal)  {   pVal->lX = 10;    pVal->lY = 20;    return S_OK;  }  // Client side code  MYPOINT pt;  HRESULT hr = pMyExplore->StructDemo(&pt); 

Like C, an IDL struct can contain fields with any valid data type.

Unions

IDL also supports C-style unions of data types. However, in order to be able to properly marshal the union, the marshaler has to know which union member is currently valid (in a union, only one member variable can be valid at a time). IDL solved this problem by associating an integer value with each union member, using a keyword, case, as shown below:

 typedef union tagMYNUMBER {   [case(1)] long l;    [case(2)] float f;  }MYNUMBER; 

When such a union is passed as a method parameter, it is necessary to pass another numeric parameter to indicate which union member is valid. A keyword, switch_is, is used for this purpose, as shown here:

 HRESULT SimpleUnionIn([in] short type,    [in, switch_is(type)] MYNUMBER num); 

This numeric parameter is referred to as a discriminator.

The following code fragment illustrates the usage:

 // Server code  STDMETHODIMP CMyExplore::SimpleUnionIn(short type, MYNUMBER num)  {   long l;    float f;    switch(type) {   case 1:      l = num.l;      break;    case 2:      f = num.f;      break;    }    return S_OK;  }  // client side usage  MYNUMBER num;  num.f = 15.0;  pMyExplore->SimpleUnionIn(2, num);  num.l = 10;  pMyExplore->SimpleUnionIn(1, num); 

It is too easy to make an error when hard-coded numeric values are used for a discriminator. Let s use our knowledge on enumerations and redefine our union:

 typedef enum tagMYVALUETYPE {   MYLONG = 1,    MYFLOAT = 2  }MYVALUETYPE;  typedef union tagMYNUMBER {   [case(MYLONG)] long l;    [case(MYFLOAT)] float f;  }MYNUMBER;  // method definition  HRESULT SimpleUnionIn([in] MYVALUETYPE type,    [in, switch_is(type)] MYNUMBER num); 

Now, the server-side code as well as the client-side code can avail the enumerator.

graphics/01icon02.gif

Using enumeration for discriminators in union data type simplifies programming.


Encapsulated Unions

From the previous illustration, it is obvious that a union data type and the discriminator always go hand-in-hand as parameters. It seems sensible to bundle these two parameters as one structure, as follows:

 typedef struct tagMYENUMBER {   MYVALUETYPE type;    [switch_is(type)] union {     [case(MYLONG)] long l;      [case(MYFLOAT)] float f;    };  }MYENUMBER;  // Server side code  STDMETHODIMP CMyExplore::EncapsulatedUnionIn(MYENUMBER num)  {   long l;    float f;    switch(num.type) {   case MYLONG:      l = num.l;      break;    case MYFLOAT:      f = num.f;      break;    }    return S_OK;  }  // Client side code  MYENUMBER num;  num.type = MYFLOAT;  num.f = 15.0;  pMyExplore->EncapsulatedUnionIn(num);  num.type = MYLONG;  num.l = 10;  pMyExplore->EncapsulatedUnionIn(num); 

Such a bundled union is referred to as an encapsulated or discriminated union.

Arrays

Like C, IDL allows you to pass an array of elements as a parameter. Also like C, an array can be specified using array syntax or pointer syntax. However, unlike C, where a pointer can point to a single value or to the beginning of an array, IDL requires a pointer to be annotated with one or more attributes to indicate that it is an array. Such a pointer is referred to as a sized pointer.

graphics/01icon01.gif

Without these special attributes, a pointer will always point to a single value under IDL.


Let s examine how these special attributes are used.

Fixed-Size Arrays. When the size of the array is already known during the interface method design, the parameter can be specified using fixed-array syntax. Following is an example for sending array data from client to server:

 // Interface method definition  HRESULT SimpleArrayDemoIn([in] long alVal[100]);  // Server side code  STDMETHODIMP CMyExplore::SimpleArrayDemoIn(long alVal[])  {   long lLastVal = alVal[99];    // Get data from the last element                                  // of the array    return S_OK;  }  // Client side code  long alVal[100];  alVal[99] = 25;    // Set data for the last element of the array  pMyExplore->SimpleArrayDemoIn(alVal); 

Fixed arrays are easy to use and are optimized for marshaling. The wire representation of the data is identical to the in-memory buffer presented to the server. As the entire content of the array is already in the received buffer, the stub logic is smart enough to use the received buffer memory location as the actual argument to the method. Otherwise, it would have to allocate new memory, copy the received buffer into the memory, pass a pointer to the new memory as the method argument, and free the memory after the method returns.

Variable-Size Arrays. Fixed arrays are useful when the size of the array is constant and known at the interface design time. However, knowing the size of the array at design time is not always possible.

To allow the size of array to be specified at run time, IDL provides the size_is attribute. An array defined this way is known as a conformant array and its size (the number of elements) is called its conformance.

The following example shows three stylistic variations of specifying a conformant array:

 HRESULT ConformantArrayIn([in] long lCount,    [in, size_is(lCount)] long aVal[];  HRESULT ConformantArrayIn([in] long lCount,    [in, size_is(lCount)] long aVal[*];  HRESULT ConformantArrayIn([in] long lCount,    [in, size_is(lCount)] long* aVal; 

All three styles are equivalent in terms of the underlying NDR format. Any of these methods allow the client to indicate the appropriate conformance and allow the server to use the conformance, as shown in the following code fragment:

 // Server side code  STDMETHODIMP CMyExplore::ConformantArrayIn(long lCount,    long alVal[])  {     long lFirstVal = alVal[0];             // Get data from the first                                             // element in the array      long lLastVal = alVal[lCount - 1];     // Get data from the last                                             // element in the array      return S_OK;  }  // Client side code  long alVal[100];  alVal[0] = 50;        // Set data for the first element in the array  alVal[99] = 25;       // Set data for the last element in the array  pMyExplore-> ConformantArrayIn(100, alVal); 

IDL also supports an attribute called max_is, a stylistic variation of the size_is attribute. While size_is indicates the number of elements an array can contain, max_is indicates the maximum legal index in an array (which is one less than the number of elements an array can contain). Using the max_is attribute, the previous example code could be rewritten as:

 // interface definition  HRESULT ConformantArrayIn2([in] long lCount,    [in, max_is(lCount)] long aVal[];  // Server side code  STDMETHODIMP CMyExplore::ConformantArrayIn(long lCount,  long alVal[])  {   long lFirstVal = alVal[0];    long lLastVal = alVal[lCount];      // Get data from the last                                        // element in the array  return S_OK;  }  // Client side code  long alVal[100];  alVal[0] = 50; // Set data for the first element in the array  alVal[99] = 25; // Set data for the last element in the array  pMyExplore-> ConformantArrayIn2(99, alVal); 

As with fixed arrays, conformant arrays are marshaling efficient. The received buffer for a conformant array can be passed directly to the method implementation as an argument.

The previous examples showed how to transfer data from a client to a server. A conformant array can also be used for obtaining values from the server. The client passes a potentially empty array to the server and has it filled with useful values, as illustrated in the following example:

 // interface method  HRESULT ConformantArrayOut([in] long lCount, [out, size_is(lCount)]    long alVal[]);  // Server side  STDMETHODIMP CMyExplore::ConformantArrayOut(long lCount, long alVal[])  {   alVal[0] = 25;                 // Set data for the first element                                   // in the array    alVal[lCount-1] = 50;          // Set data for the last element                                   // in the array    return S_OK;  }  // Client side  long alVal[100];  pMyExplore->ConformantArrayOut(100, alVal);  // dump the first and the last element of the array  cout << alVal[0] << ", " << alVal[99] << endl; 

Wire Efficient Arrays. In the previous example, the client expected the server method to fill in the entire array. But what if the server method cannot fill the entire array?

Consider this scenario: the principal of a school is trying to obtain the student grades as an array of long values:

 HRESULT GetGrades1([in] long lSize,    [out, size_is(lSize)] long alGrades[]); 

Assuming there are no more than 25 students in the class, the principal calls the method as follows:

 long alGrades[25];                    // A max of 25 students in the class  for(long i=0; i<25; i++){ alGrades[i] = 0;}   // initialize the array to 0  pMyExplore->GetGrades1(25, alGrades);                   // make the call  for(i=0; i<25; i++) {cout << alGrades[i] << endl;}      // dump the grades 

What if the class had less than 25 students? What if there were just five students in the class?

An easy way to solve this problem is to indicate the actual number of students in the class as an [out] value.

 HRESULT GetGrades2([in] long lSize, [out] long* plActual,    [out, size_is(lSize)] long alGrades[]); 

The client code needs slight modifications to deal with the actual number of students:

 long lActual = 0;  pMyExplore->GetGrades2(25, &lActual, alGrades);  for(i=0; i<lActual; i++) { cout << alGrades[i] << endl;  } 

This solves the problem. However, we are still not wire efficient. The marshaler doesn t know that only the first five entries in the array have meaningful values. It will still transmit the contents of the entire array on return.

We need to indicate to the marshaler that only some elements of the array should be transmitted. This is accomplished by using the keyword length_is, as shown below:

 HRESULT GetGrades3([in] long lSize,    [out] long* plActual,    [out, size_is(lSize), length_is(*plActual)] long alGrades[]); 

The client code stays the same as before. However, the marshaler now has enough information to transfer only the required five elements and not the entire array. After receiving these five elements, the client-side proxy code initializes the rest of the elements in the array to zero.

IDL also supports a stylistic variant of length_is known as last_is. Just as size_is and max_is are related, length_is and last_is can be related as follows:

 length_is(nCount) == last_is(nCount+1) 

An array specified with the length_is (or last_is) attribute is known as a varying array. The value specified using length_is is known as the variance of the array. The variance indicates the contents of the array, as opposed to the conformance, which indicates the capacity of the array.

When both the conformance and the variance are used together, as in the above example, the array is known as an open array.

Changing Array Index. The way we defined our GetGrade3 method, the marshaler assumes that the variance of the array is indexed at the first element. In our scenario, the valid grades were from alVal[0] to alVal[4]. What if the grades available at the server were not for the first five students but for the middle five students alVal[10] to alVal[14] ?

IDL solves this problem by defining an attribute, first_is, to indicate the index of the first valid element in a varying (or open) array. Using this attribute, the method can be redefined as:

 HRESULT GetGrades4([in] long lSize,    [out] long* plActual, [out] long* plFirst,    [out, size_is(lSize), length_is(*plActual),      first_is(*plFirst)] long alGrades[]); 

Our client code then becomes:

 long lActual = 0; long lFirst = 0;  pMyExplore->GetGrades4(25, &lActual, &lFirst, alGrades);  cout << "First is: " << lFirst << endl;  for(i=0; i<lActual; i++) { cout << alGrades[lFirst+i] << endl; }                                     // Dump grades starting from lFirst 

Finally, consider an extreme scenario. The principal realized that too many students were failing the class. Out of generosity, the principal decided to give each student some extra points. It was decided that this value be passed as the last element of the array, alVal[24].

On the forward trip:

  • The size (conformance) of the array is 25.

  • The valid number of elements (variance) is 1.

  • The index for the first valid element is 24.

On the return trip:

  • The conformance of the array is 25. [4]

    [4] The conformance of the array is dictated by the client. The server cannot change it.

  • The variance of the element is 5.

  • The index of the first valid element is 10.

For maximum wire efficiency, the interface method can be defined as follows:

 HRESULT GetGrades5([in] long lSize,    [in, out] long* plActual, [in, out] long* plFirst,    [in, out, size_is(lSize),    length_is(*plActual), first_is(*plFirst)] long alGrades[]); 

Developing the client and the server code for the above definition is left as an exercise for the reader.

Resizing an Array. In all the earlier examples dealing with arrays, the actual size of the array (conformance) was specified by the client. In doing so, the client has to make an assumption about the maximum size of the array. For instance, in the grades example, the principal assumed that the maximum number of students in a class is 25. What if there are more than 25 students in a class?

One way to solve this problem is to define an additional method on the interface to obtain the number of students in the class. The client (the principal) calls this method first, allocates the appropriate amount of memory, and then calls the actual method to obtain the grades. However, this mechanism implies two round trips to the server, which could be expensive. Moreover, there is no guarantee that the number of students cannot change between the first and second method call.

If you were a C programmer, your first thought would be to let the server method modify the size of the array. You would then redefine the interface method as follows:

 HRESULT GetGrades6([out] long* plSize,    [out, size_is(*plSize)] long alGrades[]); 

The fact is that the size of the array gets fixed when it is first marshaled. Thus, the server cannot change the size of the array (though it can change the contents of the array). The above line will generate an error during MIDL compilation that the conformance of the array cannot be specified by an outonly value.

How can we then change the size of the array? It turns out that we cannot; well, at least not directly.

What we can do is create a completely new array at the server side and return it to the client. To do so, the method definition has to be changed such that the result is returned as an array.

 HRESULT GetGrades6([out] long* plCount,    [out, size_is(,*plCount)] long** palVal); 

Pay special attention to the comma in the argument list to the size_is attribute. Attribute size_is accepts a variable number of comma-delimited arguments, one per level of indirection. If an argument is missing, the corresponding indirection is assumed to be a pointer to an instance and not an array. In our case, the definition suggests that argument palVal is a pointer to an array. The size of this array will be dictated by the server method.

With the above interface method definition, our server method implementation would be as follows:

 STDMETHODIMP CMyExplore::GetGrades6(long *plCount, long **palVal)  {   long lCount = 10;    // allocate enough memory    long* alVal = (long*) CoTaskMemAlloc(lCount * sizeof(long));    for (int i=0;i<lCount; i++) {     alVal[i] = i + 15;           // Set each value    }    *plCount = lCount;             // Set the count    *palVal = alVal;               // Set the return array    return S_OK;  } 

The client code would then become:

 long* palGrades;  long lCount = 0;  pMyExplore->GetGrades6(&lCount, &palGrades);  for(long i=0; i<lCount; i++)    { cout << palGrades[i] << endl; }  // dump grades  CoTaskMemFree(palGrades);            // free server-allocated memory 

Thus, by using an extra pointer indirection, the client no longer has to assume the size of the array.

Pointers

An IDL pointer points to a memory location, just like C. Following are a few examples of pointers:

 [in] long* plVal;       // passing a long value as a pointer  [out] long** pplVal;    // obtaining one element of long* type  [out] BSTR* pbsVal;     // obtaining one element of BSTR type 

Unlike C, where a pointer can point to a single value or to the beginning of an array, a pointer under IDL points to a single value unless it is specially attributed to indicate that it is an array. In the above examples, plVal is a pointer to a single long, pplVal is a pointer to a single long*, and pbsVal is a pointer to BSTR.

Recall that in a COM application, the caller and the callee could be running under two separate processes. The same memory location in two processes may contain entirely different pieces of data. Thus, if a pointer is passed as a method parameter from one process, it may point to a memory location with invalid data. Even worse, a valid memory location under one process may point to an inaccessible memory in another process, resulting in an access violation.

COM s solution to this problem is simple. From one process, instead of transmitting the pointer value, transmit the entire contents the pointer is pointing to. In the other process, take this data and recreate the memory just the way the first process was seeing it.

When a pointer is used as a method parameter, the marshaler will ensure that the data the pointer points to gets transmitted, no matter how complex is the data structure the pointer is pointing to. Furthermore, if the data structure itself contains a pointer as a member variable, the data pointed by this pointer is transmitted as well. The marshaler goes through the whole data structure recursively and dereferences every nonNULL pointer encountered until there are no more nonNULL pointers to dereference.

This mechanism introduces a new set of problems. Let s examine these problems and see how we can solve them.

For our analysis, we will consider the simple case where the client and the server are running as two separate processes. A more general case would be where the client and the server are in two different contexts (contexts are covered in Chapter 5).

Consider the following simple IDL method definition:

 HRESULT SetValue([in] long* pValue); 

If the client were to invoke the method as follows:

 long lVal = 100;  p->SetValue(&lVal); 

The proxy code has to ensure that the value 100 gets transmitted to the server, which requires dereferencing the pointer.

What if the client were to invoke the method as follows?

 p->SetValue(0); 

If the proxy were to dereference the passed pointer, it would result in an access violation.

The proxy (and the stub) could be smart enough to check for NULL before dereferencing a pointer. But then, should we always force the proxy and the stub to check each and every pointer for NULL ? Moreover, what if NULL is indeed a legal value for some cases?

Obviously, there are some cases where a pointer should never be NULL, and other cases where a pointer is allowed to have a NULL value. The interface designer would know of such cases when designing the interface.

To indicate that a pointer must never be NULL, IDL supports the [ref] attribute. Our SetValue method can be redefined as:

 HRESULT SetValue([in, ref] long* pValue); 

If the caller now makes the mistake of passing NULL as a parameter, the proxy code will return the error code RPC_X_NULL_REF_POINTER (0x800706f4). [5]

[5] MIDL-generated proxy and stub code assume the pointer to be non-NULL if a [ref] attribute is specified. They blindly dereference the pointer, causing an access violation. As the generated proxy/stub code always executes under an exception handler, it traps the access violation and translates it into the error code.

To indicate that a pointer can have a NULL value, IDL supports the [unique] attribute. When this attribute is specified to a pointer, the proxy code does additional checking to see if the passed parameter is NULL. It also inserts a tag in the ORPC packet indicating whether or not a NULL pointer was passed. The stub code examines this tag and, if the tag indicates that a NULL pointer was passed, reconstructs a NULL pointer.

Pointer Defaults. IDL assumes certain defaults for pointer parameters. To do this, it classifies pointers into two types. The named parameter to a method that is a pointer is referred to as a top-level pointer. Any subordinate pointer that is implied by dereferencing a top-level pointer is referred to as an embedded pointer.

Consider the following IDL method definition:

 typedef struct tagMYSTRUCT {   long lVal1;    long* plVal2;  }MYSTRUCT;  HRESULT MyMethod([in] long* plVal3,    [in, out] long** pplVal4, [out] MYSTRUCT* pVal5); 

In this definition, plVal3 and pVal5 are top-level pointers whereas *pplVal4 and pVal5->plVal2 are embedded pointers.

IDL uses the following logic for any pointer that is not explicitly attributed:

  • A top-level pointer is always assumed to be a ref pointer.

  • A top-level out-only pointer has to be a ref pointer. This is because IDL follows the call-by-value semantics of C: in order for a parameter to be an output parameter, it must have a memory address to hold the output value.

  • All embedded pointers are treated as unique by default. However, it is good practice to explicitly specify the attributes for embedded pointers. In fact, IDL supports an interface attribute, pointer_default, to specify the default pointer attribute for all embedded pointers.

A Link-List Example. Let s consider the case of passing a link-list of long values as a method parameter.

The data structure for our link-list is shown below:

 typedef struct tagMYLONGLIST {   long lVal;    struct tagMYLONGLIST* pNext;  }MYLONGLIST; 

Our link-list consists of a set of elements of type MYLONGLIST. Each element is linked to the next element by the member variable pNext. The last element in the list points to NULL.

In order to enforce that NULL is a legal value for pNext, we need to apply the unique attribute to this member variable. [6]

[6] Actually, for our example, the member variable is an embedded pointer and is therefore unique by default. Moreover, for a recursive data-type definition such as this, IDL will not accept the ref attribute, even if you try to force it to be one.

 typedef struct tagMYLONGLIST {   long lVal;    [unique] struct tagMYLONGLIST* pNext;  }MYLONGLIST; 

The interface method can be defined as:

 HRESULT MyLinkList([in] MYLONGLIST* pList); 

By IDL rules, this definition defaults to:

 HRESULT MyLinkList([in, ref] MYLONGLIST* pList); 

To indicate that NULL is a legal value for the parameter itself, we can modify our definition to:

 HRESULT MyLinkList([in, unique] MYLONGLIST* pList); 

Following is our server-side and client-side code snippets:

 // Server-side  STDMETHODIMP CMyExplore::MyLinkList(MYLONGLIST *pList)  {   long l;    while(NULL != pList) {     l = pList->lVal;      pList = pList->pNext;    }    return S_OK;  }  // Client-side  MYLONGLIST lastItem;  lastItem.lVal = 100;  lastItem.pNext = NULL;  MYLONGLIST firstItem;  firstItem.lVal = 200;  firstItem.pNext = &lastItem;  HRESULT hr = pMyExplore->MyLinkList(&firstItem); 

This will result in transferring the entire link-list as a method parameter.

What if I convert this link-list into a circular list? The only difference between a link-list and a circular list is that the last element of the circular list has to point back to the first element.

Try setting the last item to point to the first item and observe what happens.

 ...  firstItem.lVal = 200;  firstItem.pNext = &lastItem;  lastItem.pNext = &firstItem;  HRESULT hr = pMyExplore->MyLinkList(&firstItem); 

What you would observe is that the server code was never invoked. If you try to dump the return status code hr, it will most likely contain an invalid HRESULT. What happened?

In an attempt to build the wire data, the proxy code recursively started dereferencing the next pointer. As there was no NULL pointer to indicate the termination, the proxy code blindly went into an infinite loop and ran out of stack space. It just so happens that the proxy code runs under a structured exception handler. This prevented the program from crashing. However, the method call failed to go through.

If the proxy code could somehow detect that the pointer it is currently trying to dereference has already been dereferenced, then it can stop further dereferencing. How can we do that?

Detecting Duplicate Pointers. Consider the following interface method and its client-side code fragment:

 // Interface method  HRESULT MyMethod([in] long* pl1, [in] long* pl2);  // Client code  long lVal = 10;  p->MyMethod(&lVal, &lVal); 

In this example, we are passing the same pointer twice. What should the proxy code do in the presence of such duplicate pointers?

The first option would be to do nothing special. In this case, the proxy will treat pl1 and pl2 as two distinct pointers. Not only will the value 10 be transmitted twice, but also the server side will get two pointers pointing to two different locations. What if the semantics of the server method required an equivalence of pointers?

 STDMETHODIMP CMyExplore::MyMethod(long* pl1, long* pl2)  {   if (pl1 == pl2) {     ...    }  } 

In this case, the marshaler would break the semantic contract of the interface method.

To address this problem, IDL allows designers to indicate if the marshaling logic should check for duplicate pointers. Attribute ptr is used for this purpose:

 HRESULT MyMethod([in, ptr] long* pl1, [in, ptr] long* pl2); 

When this attribute is specified, the marshaler code will contain extra logic to detect if a pointer has already been used earlier or aliased. For all pointers that are aliased, the value is transmitted just once. The server-side stub code will recreate all the pointers. However, they all would point to the same location.

Pointers that use the ptr attribute are called full pointers.

Like unique pointers, a full pointer can have a NULL value.

Situations requiring full pointers are rare. However, the full pointer mechanism is useful for solving specific problems such as transmitting a circular list. The stack overflow problem in the previous example can be solved by using a full pointer as follows:

 // Interface definition  typedef struct tagMYCIRCULARLIST {   long lVal;    [ptr] struct tagMYCIRCULARLIST* pNext;  }MYCIRCULARLIST;  HRESULT MyCircularList([in, ptr] MYCIRCULARLIST* pList);  // Server side code  STDMETHODIMP CMyExplore::MyCircularList(MYCIRCULARLIST *pList)  {   if (NULL == pList) {     return S_OK;    }    long l;    MYCIRCULARLIST* pFirst = pList;    do {     l = pList->lVal;      pList = pList->pNext;    }while(pList != pFirst);    return S_OK;  }  // Client side code  MYCIRCULARLIST lastItem;  lastItem.lVal = 100;  lastItem.pNext = NULL;  MYCIRCULARLIST firstItem;  firstItem.lVal = 200;  firstItem.pNext = &lastItem;  lastItem.pNext = &firstItem;  HRESULT hr = pMyExplore->MyCircularList(&firstItem); 

In the above sample code, attribute ptr is used at two locations in the interface definition. Its use for the member variable definition pNext is obvious. Why is it used for the method parameter as well? This is left as an exercise for you (hint: remove the attribute from the method parameter and see what happens. The code is on the CD).

Attributes ref, unique, and ptr are all mutually exclusive, that is, not more than one such attribute can be specified for a pointer. Table 2.7 summarizes the differences between these attributes.

Table 2.7. Properties of IDL Pointer Types

Property

ref

unique

ptr

Can be Aliased?

No

No

Yes

Can be NULL?

No

Yes

Yes

Interface Pointers

In the previous section, we looked at marshaling data pointers. COM IDL has native support for marshaling interface pointers (if this could not be done, the whole essence of COM would make no sense).

To pass an interface IFoo as an input parameter, one simply declares it as an [in] parameter of type IFoo*.

 Interface IBar : IUnknown  {   HRESULT MyMethod([in] IFoo* pIFoo);  } 

If the client and the server are in two different contexts, the above method loads the stub for IFoo in the client context, and the proxy for IFoo in the server context.

It may seem odd at first that the stub is getting loaded in the client context and the proxy is getting loaded in the server context, instead of the other way around. The point to remember is that the role of the proxy is to pack the parameter data on an interface method call (from the calling context) and the role of the stub is to unpack the data (in the called context). In our case, when the interface pointer is being handed over to the server, the assumption is that the server will be calling the methods on the interface pointer. In some sense, the server becomes the client and the client becomes the server.

More precisely, in our example case, the client holds the proxy for the IBar interface and the stub for the IFoo interface, and the server holds the stub for the IBar interface but the proxy for the IFoo interface.

An interface pointer passed as an input parameter is always of the unique type, that is, the pointer can have a NULL value, but it cannot be aliased.

To obtain an interface pointer to IFoo, one has to declare an out parameter of type IFoo**, as follows:

 HRESULT MyMethod([out] IFoo** ppIFoo); 

In this case, the proxy for IFoo gets created in the client code and the stub for IFoo gets created in the server code.

The second case is the most often use-case under COM (recall from the previous chapter that the client obtains an appropriate interface from the server).

Ambiguous Interfaces. Quite often an interface pointer is passed as a pointer to its base interface. Consider, for example, interface IBar derived from interface IFoo. The server returns a pointer to IBar as a pointer to IFoo. The following code illustrates the scenario:

 //  // IDL definition  //  interface IBar : IFoo  {   ...  }  HRESULT MyMethod([out] IFoo** ppIFoo)  //  // Server implementation  //  class CBar : public IBar  {   ...  }  STDMETHODIMP CMyServer::MyMethod(IFoo** ppIFoo)  {   CBar* pBar = CreateBar();    *ppIFoo = pBar;    ...  } 

When a client invokes MyMethod, the marshaler is not aware that it had to load the proxy/stub for IBar and not IFoo. If the client reinterprets the returned pointer to the IBar type (which it will) and tries to use any method from IBar, the program will behave unpredictably.

For such cases, to make the marshaler aware of the actual interface being passed, we need to provide the marshalar with the identifier of the actual interface. IDL provides the iid_is attribute to accomplish this:

 HRESULT MyMethod([in] REFIID riid,    [out, iid_is(riid)] IFoo** ppIFoo) 

Here, the client is explicitly informing the marshaler that the return type of the interface is of type riid (IID_IBar in our case), allowing the marshaler to load the correct proxy/stub during run time.

Defining IDL Interfaces

A COM interface exposes a group of related functions, analogous to a C++ class definition. The following is an example of an interface definition, IFoo:

 [   object,    uuid(5E7A5F3E-F4F4-11D2-9B37-0080C8E11F14),    helpstring("This Interface can get and set color of      the object"),    pointer_default(unique)  ]  interface IFoo : IUnknown  {   typedef enum { MYRED, MYGREEN, MYBLUE} MYCOLOR;    HRESULT SetColor([in] MYCOLOR val);    HRESULT GetColor([out] MYCOLOR* pVal);  }; 

The definition of the interface is marked by the IDL keyword interface. This keyword is followed by the logical name of the interface, IFoo in our example.

Every COM interface, except interface IUnknown, is derived from another interface. Interface IUnknown is a special interface and is discussed separately.

Note that IDL doesn t support multiple inheritance; that is, an interface cannot be derived from more than one interface. From our previous chapter, we know why this restriction was placed on the interface definition. However, this has not been a big concern in the COM community. As we will see later, this restriction is only on the interface definition and not on the implementation of the interface, thus providing developers an opportunity to use multiple inheritance. Of course, the programming language being used should support the feature.

The interface body is simply a collection of method definitions and supporting type definition statements. Unlike the C++ class definition that supports the notion of public and private methods, any method defined in the interface is always a public method. (If you really wanted to make a method private, why would you put in an interface that, by its definition, gets exposed to the public?)

Interface Attributes

A COM interface can be annotated using the typical IDL attribute syntax; attributes are defined within square brackets and each attribute is separated by a comma.

Every COM interface requires at least two IDL attributes: object and uuid.

COM-Style Interface. The first requisite attribute is an object. It specifies that the interface is a COM interface. If this attribute is not specified, the interface is assumed to be a DCE RPC interface.

graphics/01icon02.gif

Never forget to specify the object attribute when defining a COM interface.


Identification. The second requisite attribute is uuid, a GUID that uniquely identifies the interface. In the case of interfaces, this GUID is referred to as an interface ID or IID. The attribute specification is shown here:

 uuid(5E7A5F3E-F4F4-11D2-9B37-0080C8E11F14), 

When an MIDL compiler processes the interface, it gives a C-style symbolic name to the GUID, such as IID_<interfacename>. For the IFoo interface, the symbolic name is IID_IFoo.

Embedded Pointers. If you recall from the previous section on pointers, each embedded pointer can be explicitly annotated as either unique, ref, or ptr. Instead of annotating each embedded pointer individually, IDL provides an interface-level attribute, pointer_default, to indicate the default behavior of any embedded pointer in the interface, as shown here:

 pointer_default(unique) 

Providing Helpful Hints. Quite often it is necessary to provide helpful hints to the developers on what the interface is about and when to use a specific method. The keyword helpstring can be used as an interface attribute, as follows:

 helpstring("This Interface can get and set color of the object"), 
The Root Interface IUnknown

The COM interface IUnknown serves the same purpose as the IGeneral interface defined in the previous chapter. If you recall, all interface IGeneral did was to maintain the reference count on the interface and to let the client obtain other, more meaningful interfaces from the object. The follow-ing is the final version of IGeneral that appeared at the end of the previous chapter:

 class IGeneral  { public:    virtual VRESULT _stdcall Probe(char* pszType,      IGeneral** ppRetVal) = 0;    virtual void _stdcall AddReference() = 0;    virtual void _stdcall Delete() = 0;  }; 

The following is the definition for interface IUnknown, taken from SDK IDL file unknwn.idl:

 [   local,    object,    uuid(00000000-0000-0000-C000-000000000046),    pointer_default(unique)  ]  interface IUnknown  {   HRESULT QueryInterface(     [in] REFIID riid,      [out, iid_is(riid)] void **ppvObject);    ULONG AddRef();    ULONG Release();  } 

At this point, all the attributes on the interface IUnknown, except the local attribute, should be reasonably clear. Attribute local informs MIDL not to generate any marshaling code for this interface and relaxes COM s requirement that every interface method should return an HRESULT.

Interface IUnknown is functionally equivalent to the class IGeneral defined in the previous chapter. The similarities follow:

  • Method QueryInterface is analogous to the IGeneral::Probe method; it returns the requested interface. [7] If the requested interface is not found, the implementation of QueryInterface should return a E_NOINTERFACE error code.

    [7] Why the interface pointer is returned as void* and not the more intuitive IUnknown* is for legacy reasons.

  • Method AddRef is analogous to the IGeneral::AddReference method; it increments the reference count on the underlying object. It should be called for every new copy of a pointer to an interface.

  • Method Release is analogous to the IGeneral::Delete method; it decrements the reference count on the underlying object. A reference count of zero indicates that the object is no longer used and that the server is free to release any resources associated with the object.

graphics/01icon01.gif

The return value from AddRef is the value of the reference count after the increment. It is meant to be used only for diagnostic purposes, as the value is unstable under certain situations.


IUnknown is the root of all COM interfaces. Every other legal COM interface should derive either directly from this interface or from one other COM interface that itself is derived from IUnknown, either directly or indirectly. This means, at the binary level, any COM interface is a pointer to vtbl that has the first three entries as QueryInterface, AddRef, and Release.

The symbolic name of the interface ID for IUnkown is IID_Iunknown, defined as follows:

 extern "C" const IID IID_IUnknown; 

This IID, along with many other system-defined IIDs, is provided in the SDK library, uuid.lib. Any COM program, client or server, has to be linked with this library to resolve the COM-defined symbols.

Defining COM Classes

A COM class, or coclass, is a named declaration that represents concrete instantiable type and the potential list of interfaces it exposes. A coclass is typically declared within the scope of a library section. The following is an example of a coclass, MyFoo:

 [   uuid(5E7A5F40-F4F4-11D2-9B37-0080C8E11F14),    helpstring("My Foo Class")  ]  coclass MyFoo  {   interface IFoo;  }; 

The definition of a coclass is marked by the IDL keyword coclass. This keyword is followed by the logical name of the COM class, MyFoo in the above example.

The body of a COM class is simply a list of interfaces the class can potentially expose. Each interface in the list is tagged by the keyword interface or dispinterface. [8]

[8] Dispinterfaces are covered later in the chapter under automation.

graphics/01icon01.gif

The coclass section exposes the potential list of interfaces, not the actual list. The number of interfaces actually implemented by the COM object could be less, more, or totally different than what is listed in the coclass section. This section identifies the interfaces a COM object could potentially support. The client code cannot rely on just this information; it has to go through the usual QueryInterface to get the interface(s) it needs.


Like interfaces, a COM class requires a unique identification. This is done by supplying a GUID argument to our all too familiar IDL keyword, uuid. When the MIDL compiler processes the coclass, it gives a C-style symbolic name to the GUID such as CLSID_<coclassname>. For coclass MyFoo in the previous example, the symbolic name would be CLSID_MyFoo.

Defining IDL Libraries

The library section is defined primarily to indicate to the MIDL compiler that a type library needs to be generated. Following is an example of the library definition:

 [   uuid(5E7A5F3F-F4F4-11D2-9B37-0080C8E11F14),    version(1.0),    helpstring("My Foo 1.0 Type Library")  ]  library MyFooLib  {   importlib("stdole32.tlb");    importlib("stdole2.tlb");    [     uuid(5E7A5F40-F4F4-11D2-9B37-0080C8E11F14),      helpstring("My Foo Class")    ]    coclass MyFoo    {     [default] interface IFoo;    };  }; 

The definition of the library is marked by the IDL keyword library. This keyword is followed by the logical name of the library, MyFooLib in the above example.

The library body contains definitions and/or references to the interfaces and user-defined data types that need to be saved into the MIDL-generated type library. In addition, the library section can define zero or more coclass definitions.

Library Attributes

A COM library can specify attributes using the typical IDL attribute syntax; attributes are defined within square brackets, and each attribute is separated by a comma.

Identification. As with interfaces, the library is uniquely identified by a GUID using the IDL keyword uuid, though in this case the GUID is referred to as a library ID or LIBID. When the MIDL compiler processes the library section, it gives a C-style symbolic name to the GUID such as LIBID_<libraryname>. For the library MyFooLib in the previous example, the symbolic name would be LIBID_MyFooLib.

Version Management. The generated type library has a major and minor version number. The designer can explicitly specify this version number using the attribute version. The major version and minor version are separated by a dot.

Removing Redundancy

As mentioned earlier, any interface definition and data type definition referenced within the scope of the library section will be saved in the type library, even if some of this data has already been saved in some other type library.

For instance, take another look at the library definition for MyFooLib. It references IFoo, which in turn references IUnknown. IUnknown references IID in its QueryInterface method and the IID is an alias for the structure GUID. Therefore, just referencing IFoo in the library section causes the definitions for IUnknown, as well as GUID, to be saved into the type library.

Definitions such as IUnkown and GUID are so commonly used in the IDL files, it doesn t make sense to have the same information being saved in every type library that ever gets created. A better solution would be to create a separate type library for such commonly used definitions.

As a matter of fact, SDK already includes a type library containing all COM system interfaces and data type definitions. The latest version of this type library is 2.0 and is defined in file stdole2.tlb. Another standard type library, stdole32.tlb, contains automation interfaces (automation is covered in the next section).

In order to exclude definitions that are already present in a different type library, IDL keyword importlib can be used. This keyword takes the type library name as an argument.

 importlib("stdole2.tlb"); 

During IDL compilation, MIDL ensures that any definition found in the specified type library is not saved in the generated type library, thus reducing the type library size.

graphics/01icon01.gif

MIDL shipped with Visual C++ searches for the type library files using the PATH environmental variable (not the INCLUDE path, as you would expect if you were a C++ programmer). This problem has been fixed in the newer versions of Platform SDK (January 2000 or later).



< 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