As you've seen, the flexibility afforded by pointers in C++ can inflict pain on the unwary when it comes to making remote calls. In C++, pointers can do the following:
Because IDL must actually transmit the data pointed to by a pointer, it must have a lot of information about what that pointer is pointing to and in what format the data is stored. The designers of IDL were faced with the choice between modeling the language to work as transparently as possible with C++-style pointers, even if the result was greater overhead, or exposing the complexity to the developer in the hope of gaining efficiency. In the end, they decided to offer several options.
To better accommodate a wide range of programs, IDL offers three pointer types: full, unique, and reference. These are specified in IDL with the ptr, unique, and ref attributes, respectively, and cannot be combined. If you don't specify a pointer type, the pointer type is determined by the pointer_default attribute in the interface header. If you don't specify the pointer_default attribute, all nonattributed pointers are assumed to be unique pointers. In this section, we'll look at how and when to use the IDL pointer attributes, as well as how to pass interface pointers as parameters.
Full pointers most closely model the attributes of C++-style pointers. Although they are rather inefficient, full pointers are useful when you distribute code that was formerly executed within a single process. Full pointers are expensive because they permit pointer aliasing, which means that they allow more than one pointer to point to the same memory location. For example, imagine a complex memory structure such as a doubly linked list. If you have a pointer to an arbitrary element in the list, several pointers reference the same memory location, as shown in Figure 16-5.
Figure 16-5. Aliasing with a full pointer.
This situation complicates matters for the marshaling code because the marshaling code must manage several pointers to the data. This is done by maintaining a dictionary of all marshaled pointers. The underlying stub code does this by resolving the various pointers to the addresses and then determining whose copy of the data is the most recent version. In general, you can avoid the need for full pointers by developing a careful interface design.
A unique pointer is identical to a full pointer in every respect but one. The difference is that unique pointers cannot cause aliasing of data, which means that storage referred to by a unique pointer cannot be accessed through any other pointer in the function. If you are certain that a pointer will not be aliased, you can mark it as a unique pointer in the IDL code. The MIDL compiler can generate efficient marshaling code for unique pointers because it can make certain assumptions about how that pointer will be used. Unique pointers have the following characteristics:
When you pass an uninitialized pointer to a method to retrieve an outbound parameter, you must set the pointer to null. Uninitialized pointers might work when you make in-process calls, but proxies and stubs assume that all pointers are either initialized with a valid address or are set to null when the call is made. This requirement is a common source of programming errors and can be difficult to track down.
From the perspective of IDL, reference pointers are the simplest type of pointer and thus require the least amount of processing by the marshaling code. Like unique pointers, reference pointers do not permit pointer aliasing. Unlike unique pointers, they cannot have the value null. A reference pointer must always refer to a valid memory address. Reference pointers are mainly used to implement C++-style reference semantics and allow for outgoing parameters. Function return types, however, cannot be reference pointers.
To summarize, reference pointers have the following characteristics:
Although IDL can support most types of pointers to data, pointers to C++ classes or functions are off-limits to remote method calls. The only way to access code running in another address space is through an interface pointer. For this reason, a method of an interface will occasionally need to return a pointer to another interface. You might ask, "Wait a minute, isn't that what QueryInterface is for?" Well, QueryInterface is useful for retrieving an interface pointer on an object, but sometimes a method needs more information in order to return an interface pointer. In such cases, it makes sense to design a method in a custom interface to do the job.
Many standard interfaces have methods that return pointers to other interfaces. To see some examples of such interfaces, you can look up the definitions of the IServiceProvider::QueryService, IMoniker::BindToObject, IClassFactory:: CreateInstance, IClassActivator::GetClassObject, IStorage::CreateStream, IOleItemContainer::GetObject, and ITypeInfo::CreateInstance methods, to name but a few.
The simplest way to design a method that accepts an interface pointer as a parameter is to prototype the interface pointer argument as void**, as shown in the following declaration:
HRESULT GetInterfacePointer1([out] void** ppvObject); // Error |
The problem with this approach is that the MIDL compiler cannot generate proxy/stub code for this method because it doesn't know how to marshal a void** argument. For this reason, the preceding declaration will result in a compile error reported by the MIDL compiler. For interfaces designed to be implemented by in-process components only, you can prefix the method declaration with the [local] attribute, indicating that no marshaling code needs to be generated by the MIDL compiler, as shown here:
[local] HRESULT GetInterfacePointer1([out] void** ppvObject); |
For interfaces that might be implemented in executable or remote components, you must provide more information about the parameter in the IDL file so that the correct proxy/stub code can be generated. Perhaps the simplest way to do this is to specify the IUnknown interface, as shown in the following code. This approach is flexible because all interfaces derive from IUnknown. However, the client of this object probably doesn't want an IUnknown pointer, which means that the client will most likely follow with an immediate QueryInterface call for the desired interface. This approach puts an extra burden on the client code and is less efficient because two round-trips must be made to the component in order to retrieve the desired interface pointer.
HRESULT GetInterfacePointer2([out] IUnknown** ppvObject); |
Recall from Chapter 8 that connection points suffer from this problem. The IConnectionPoint::Advise method is declared below in IDL notation. Although clients always pass the IUnknown pointer of their sink to the Advise method, it is a rare object that is interested in the sink for its IUnknown interface. The typical connectable object simply calls QueryInterface to request the desired interface.
HRESULT Advise ( [in] IUnknown * pUnkSink, [out] DWORD * pdwCookie ); |
Another approach to this problem is to name the interface being passed, as shown in the following code. Although this tight coupling technique works well initially, it can return to haunt the project at a later date. Problems begin when an improved version of the interface—for example, IMyCustomInterface2—is released; the client will need to obtain a pointer to the IMyCustomInterface interface before calling QueryInterface to request the IMyCustomInterface2 interface.
HRESULT GetInterfacePointer3([out] IMyCustomInterface** ppvObject); |
By now, you might be wondering how the IUnknown::QueryInterface method is defined. After all, QueryInterface seems to be able to correctly handle interface pointers for any interface, standard or custom. The definition of the QueryInterface method taken from the unknwn.idl file is shown here:
HRESULT QueryInterface( [in] REFIID riid, [out, iid_is(riid)] void **ppvObject); |
The IDL attribute iid_is is designed specifically for methods that have an interface pointer as a parameter. You use it to specify the interface identifier (IID) of the interface pointer being transmitted. The QueryInterface method obtains this IID from its first parameter. You can use this design when you create methods that return interface pointers, as shown in the following code. This approach effectively solves the issue of versioning custom interfaces because you always specify the interface pointer being marshaled. Specifying the interface pointer also obviates the need for the client to make an extra QueryInterface call to obtain the desired interface.
HRESULT GetInterfacePointer4([in] REFIID riid, [out, iid_is(riid)] void** ppvObject); |
The GetInterfacePointer4 method definition implies the following client-side code:
IMyCustomInterface* pCustomInterface; pObject->GetInterfacePointer4(IID_IMyCustomInterface, (void**)pCustomInterface); |
You can also use the IDL attribute call_as to help map a nonremotable method, such as one that uses void** as an argument, to a method that can be remoted. For example, without the call_as attribute, the method declared here cannot be remoted because it uses a void** argument, and thus the method must be defined using the local attribute:
[local] HRESULT GetInterfacePointer([in] REFIID riid, [out] void** ppvObject); |
Using the call_as attribute, you can map the GetInterfacePointer method to a remotable version of the function, appropriately named RemoteGetInterfacePointer, as shown here:
[call_as(GetInterfacePointer)] HRESULT RemoteGetInterfacePointer([in] REFIID riid, [out, iid_is(riid)] IUnknown** ppvObject); |
Using the call_as attribute to map local-only methods to those that can be remoted requires you to write binding routines that map the local types to the remotable ones. These binding routines must be compiled and then linked with the proxy/stub code generated by the MIDL compiler. Listing 16-1 shows the binding routines required for the GetInterfacePointer method declared above using the call_as attribute:
call_as.cpp
// Compile and then link this file with the proxy/stub code // generated by MIDL. #include "component.h" HRESULT __stdcall ITest_GetInterfacePointer_Proxy(ITest* Me, REFIID riid, void** ppv) { return ITest_RemoteGetInterfacePointer_Proxy(Me, riid, (IUnknown**)ppv); } HRESULT __stdcall ITest_GetInterfacePointer_Stub(ITest* Me, REFIID riid, IUnknown** ppv) { return Me->GetInterfacePointer(riid, (void**)ppv); } |
Listing 16-1. Binding routines used with the GetInterfacePointer method.