Visual C++ 6 Unleashed

Visual C++ 6 Unleashed

By MICKEY WILLIAMS and David Bennett

Using IDL to Describe Custom COM Objects

As discussed in Chapter 29, "ATL Architecture," you define COM interfaces by using the Interface Definition Language (IDL). In that chapter, the IDL example extended IDispatch—the Automation interface used by scripting clients. You can use IDL to create custom interfaces that do not derive from IDispatch, however. These interfaces can use a wide variety of types, including structures and enumerations.

Understanding MIDL Attributes

An important part of an IDL source file is the attributes that tag elements in the source. As discussed in Chapter 29, elements in an IDL source file can be tagged with attributes enclosed in braces. Some of these attributes are optional; you may see the attributes on one interface but not another. Other attributes are required for all COM interfaces, such as these:

object Specifies to the Microsoft Interface Definition Language (MIDL) compiler that this is a COM interface instead of a Remote Procedure Call (RPC) interface.
uuid Contains the unique identifier for the interface, which will become its COM Interface ID (also known as its IID).

Every COM interface will have the preceding two attributes. Other commonly used attributes for COM interfaces follow:

dual Specifies that the interface may be called through an Automation interface or its function table.
helpstring Contains a description of the interface. The description is embedded into the type library; some development tools display this string as an aid to developers using the interface.
pointer_default Defines the default behavior for pointers in the interface. More information about MIDL pointer notation is provided in the section "Pointers in IDL," later in this chapter.

Other attributes follow:

hidden Prevents the interface, coclass, or library from being displayed in a browser.
size_is Specifies the size of a dynamic array using another parameter as an argument, as in size_is(nSize).
max_is Specifies the maximum index for a dynamic array using another parameter as an argument, as in max_is(nSize).
iid_is Specifies the COM interface ID for an interface pointer passed as a parameter using another parameter as an argument, as in iid_is(riid).

Methods in an Automation-compatible interface are prefixed with a dispatch ID. The dispatch ID is basically a required index when using the IDispatch interface or any interface derived from IDispatch. The dispatch ID is used by Automation to identify the particular interface that is to be invoked. Here is the syntax for an interface method with a dispatch ID:

[id(1)] HRESULT DrinkLatte();

Two other attributes are commonly used with interface methods:

propput Specifies a method used to set the value of a property exposed by the interface. The last parameter passed to the method must be an [in] parameter that will be used to set the property's value. The property must have the same name as the method.
propget Specifies a method used to retrieve the value of a property exposed by the interface. The last parameter passed to the method must be an [out, retval] parameter that will be used to retrieve the property's value. The property must have the same name as the method.

Each property can have one propput and one propget method.

Compiling an IDL Source File with MIDL

IDL source files are compiled by invoking the MIDL compiler via this command line:

MIDL latte.idl

If you use Visual C++ and the ATL COM Wizard to create your project, the MIDL compiler is invoked automatically as part of the project build activity.

Given a source file named Latte.idl, the MIDL compiler generates five output files:

dlldata.c Contains the functions required for the proxy/stub DLL.
Latte.h Contains C and C++ versions of the interface definitions.
Latte_i.c Contains definitions of the CLSIDs and IIDs used by the interfaces, type libraries, and coclasses found in Latte.idl.
Latte_p.c Contains proxy/stub marshaling code.
Latte.tlb Contains the type library for Latte.idl. The type library is a binary version of the IDL file and is used by programming languages such as Visual Basic that cannot read IDL files. Type libraries also are used by Automation controllers written in other languages to discover properties and methods exposed by a COM object.

The latte_i.c file typically would be included in one of the files used by C or C++ clients of interfaces defined in Latte.idl. Alternatively, the file could be linked into the program separately. This file contains definitions of the GUIDs used by all components in the IDL file.

The files latte_i.c, latte_p.c, latte.h, and dlldata.c can be compiled to create a proxy/stub DLL. This DLL is required if you're marshaling non–Automation-compatible interfaces across apartment boundaries. More information about creating a proxy/stub DLL is provided in the section "Compiling and Registering the Standard Proxy/Stub DLL," later in this chapter.

If you're targeting Windows NT 4.0 or Windows 2000, consider using the /Oicf compiler switch when invoking the MIDL compiler. This compiler switch notifies the compiler to generate code that uses interpretive marshaling features available only in Windows NT 4.0 and Windows 2000. Interpretive marshaling can greatly reduce the size of the marshaling code used in an interface. If you need to support Windows 95 or Windows 98, the /Oic compiler switch provides a somewhat less-optimized proxy.

Using Type Libraries

IDL files originally were created with the C and C++ RPC programmer in mind. Many languages cannot use IDL source files or the C and C++ source files generated by the MIDL compiler. For this reason, the MIDL compiler emits a type library, which is a binary representation of the IDL source.

Languages such as Java and Visual Basic extract information from type libraries instead of using the IDL source. This method allows these tools to provide a user interface that displays information about the COM interfaces in the type library.

When programming in C or C++, the LoadTypeLib or LoadRegTypeLib function is used to load the type library; these functions return a pointer to the ITypeLib interface. A related interface, ITypeLib2, is derived from ITypeLib and also returns type library attributes. The ITypeInfo and ITypeInfo2 interfaces are used to describe objects in the type library and are returned by ITypeLib methods.

Using Structures in IDL

Custom structures are defined in IDL much as they are in C and C++. Typically, the structure is defined as a typedef. The following code fragment defines a structure type named MyRect:

typedef struct tagMyRect
{
    int left;
    int right;
    int top;
    int bottom;
}MyRect;

After the MyRect structure is defined in the IDL file, it can be used just like any other type as a parameter:

HRESULT GetOurRect([out]MyRect* pRect);
HRESULT DrawOurRect([in]MyRect* pRect);

Structures such as MyRect are not Automation compatible. If you define and use a custom structure in your IDL, you must use custom interfaces, and you must build and register a proxy/stub DLL. This isn't difficult, but it does require an extra step when building your project. The steps required to build and register a proxy/stub DLL are discussed in the section "Compiling and Registering the Standard Proxy/Stub DLL," later in this chapter.

Using Enumerations in IDL

Like structures, enumerations are defined in IDL just as they are in C and C++. The following fragment defines an enumeration type named BaseballClubs that contains a few Major League Baseball teams:

typedef enum tagBaseballClubs
{
    Padres = 0,
    Braves,
    Yankees,
    Astros,
    Indians,
    Dodgers
}BaseballClubs;

After the enumeration is defined, it can be used like any other type:

HRESULT PlayBall([in]BasballClubs* pClub);
HRESULT GetWorldChamps([out]BasballClubs* pClub);

Later in this chapter, an example will use an enumerated type to return the operating system name to the client.

Pointers in IDL

If you're one of those people who thought pointers were a difficult topic when learning C or C++, you may need to study a few examples of how pointers work in a distributed system.

Consider what happens when a pointer value is transmitted between two distributed components. If you're writing the IDL for interfaces supported by distributed COM objects, your clients may be in the same process, they may be executing in separate processes, or they may even be executing on separate machines across a network, as Figure 30.1 shows.

30fig01.gif

Figure 30.1 An interface defines the contract, not the location of the client or server.

COM objects are placed into apartments that define their threading model. A component's apartment type controls the quantity and types of threads that are allowed to execute the component's methods. Three types of apartments exist:

A component running in the STA can assume thread affinity. COM will create a message loop that is used to pump messages to an STA component. For this reason, all components that provide a user interface must run in the STA to avoid deadlock.

When a client is in the same process as the server, pointers may be passed directly between the client and server, as Figure 30.2 shows.

30fig02.gif

Figure 30.2 No marshaling is needed for pointers inside a process.

When a client and server are located in different processes, a pointer to the address in the client's process is meaningless in the address space of the server. In order for the pointer to be useful in both processes, it must be marshaled. As Figure 30.3 shows, an address in the address space of the current process is provided to the client or server.

30fig03.gif

Figure 30.3 When pointers are marshaled between processes, the pointers are managed by the COM runtime library.

The previous examples demonstrate that marshaling of pointers is required between processes. Because interface pointers are bound to a single apartment, they are marshaled when passed between apartments, even when the apartments are part of the same process.

A similar pointer marshaling mechanism is required when the client and server are located on separate machines. The important point in this discussion is that the COM library and the operating system work together to marshal data properly using the proxy/stub DLL built from information in the IDL file.

When necessary, data is reconstituted across the network to provide the appearance of pointer transparency in the network. A great deal of work may be required when a pointer is shared between machines. You specify the work required for your pointers by using IDL attributes.

Three types of pointers are supported by COM:

You can define a different pointer type attribute for each pointer by specifying one of the three pointer attributes immediately before the parameter:

HRESULT Snore([in, ref] short* pDecibels);

Typically, you'll define a single pointer type for most of your pointers. You can define a default pointer type that will be used for all pointers in an interface with the pointer_default attribute:

[
    object,
    uuid(F8C53F2E-8744-4681-8D64-D5843DA46162),
    pointer_default(unique)
]
interface ICat : ISleepyAnimal
{
    HRESULT Sleep();
    HRESULT Eat();
    HRESULT Hide();
};

Top-level pointers will be set to ref if you don't supply a default pointer type. Pointers that are not top level—that is, pointers to pointers—and pointers embedded in structures will be set to unique if you don't supply a default type.

The full pointer type allows pointer aliasing: multiple pointers in a function call may refer to the same address. In return for this capability, you incur the cost required when the interface is marshaled; each pointer must be stored in a dictionary that is consulted to preserve the proper behavior after marshaling.

The full and unique pointer types also permit NULL values, so the marshaling code must test to ensure that invalid pointers aren't dereferenced. This gives you some flexibility when using your interface, but it increases the work done at runtime.

Using Direction Attributes in IDL

Every parameter in an IDL source file has an attribute that includes the parameter's direction. This attribute enables the MIDL compiler to generate the proper marshaling code for the interface. This attribute also defines the ownership of data passed via pointers. Three options are available:

[in] Specifies a parameter sent from the client to the server. If the server has a use for the data after the function call completes, it must make a local copy of the data, because the data belongs to the caller.
[out] Specifies a parameter, which must be a pointer, that carries data from the server to the client. The client allocates the top-level pointer, but any dynamic allocations other than the top-level pointer are made by the server and become the client's responsibility.
[in, out] Specifies that data is allocated by the client and passed to the server, which may optionally free the data and allocate new data for the client. This parameter must be a pointer. The newly allocated data, if any, becomes the responsibility of the client.

Share ThisShare This

Informit Network