Wicked Code: Implementing Handler Marshaling Un...

We were unable to locate this content in de-de.

Here is the same content in en-us.

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MIND
Wicked Code
Implementing Handler Marshaling Under Windows 2000: DeviceClient Sample App
Jeff Prosise
Code for this article: Wicked0800.exe (127KB)
E
ver since COM made its debut in 1993, programmers have searched for ways to optimize the performance of COM method calls. Their never-ending quest for speed has produced a number of clever solutions, some as extreme as using custom marshaling to replace COM's built-in remoting infrastructure with remoting mechanisms of their own. But no technique is as effective at speeding the execution of cross-process and cross-machine method calls as preventing those method calls from leaving the caller's process in the first place. Knowledge of this fact led to the invention of the smart proxy, which replaces the standard object proxy in a client process with a custom proxy that handles selected calls in-process while allowing other calls to emanate as usual to remote server processes.
      Until recently, writing a smart proxy was a nontrivial undertaking best left to experienced COM developers. Then came Windows® 2000, which introduced a new form of marshaling called handler marshaling. Handler marshaling lets you inject customized marshaling code into a client process without having to resort to custom marshaling. It works by wrapping standard object proxies with objects called handlers. When a client places a call to an object that uses handler marshaling, the call goes to the handler, which can either handle the call locally (in the caller's process) or forward the call to the object that the handler is connected to. Forwarding is easy because handlers enjoy ready access to the proxies that COM uses to enact standard marshaling. Moreover, because you write the handler, you determine which methods are remoted and which ones aren't. You can even decide at runtime whether to remote a particular call or handle it in-proc.
      Not all objects stand to benefit from handler marshaling. Objects that are most likely to benefit include methods that return information that can be cached on the client side of a remote connection, and those with methods that are trivial enough to be duplicated inside the handler without imposing undue overhead on the client. Under these circumstances, handler marshaling is a great way to eliminate unnecessary round-trips between clients and servers independent of the behavior of the clients and servers themselves.
      ATL is a big help when it comes to writing handlers because it contains a great deal of aggregation support, and handler marshaling relies heavily on aggregation. As you'll see, a handler aggregates an object proxy and at the same time is aggregated itself by a COM-provided identity object. That means it pays to familiarize yourself with ATL macros such as COM_INTERFACE_ENTRY_AGGRE-GATE_BLIND before setting out to write a handler. Fortunately, a little knowledge goes a long way, and with a description of how handlers work and some sample code to serve as a guide, you can be writing handlers in no time.

The IStdMarshalInfo Interface

      The first step in adding handler marshaling support to an object is to implement a special interface named IStdMarshalInfo on the object. IStdMarshalInfo is a simple interface that contains just one method. That method, GetClassForHandler, is called by COM to retrieve your handler's CLSID in preparation for creating a handler in the client process. If the handler's CLSID is CLSID_Handler, then the following GetClassForHandler implementation injects that handler into each client process that marshals an interface pointer from instances of CComClass:

STDMETHODIMP CComClass::GetClassForHandler (ULONG dwDestContext,
    VOID* pvDestContext, GUID* pClsid)
{
    *pClsid = CLSID_Handler;
    return S_OK;
}
      When GetClassForHandler is called, the dwDestContext parameter contains an MSHCTX flag revealing whether the handler will be created on another machine (MSHCTX_DIFFERENTMACHINE), in another process (MSHCTX_LOCAL), or in another apartment in the same process (MSHCTX_INPROC). Most GetClassForHandler implementations won't care one way or the other, but if you want, GetClassForHandler can inspect dwDestContext and return a failure code to prevent a handler from being created. COM responds to failed GetClassForHandler calls by using standard marshaling to propagate method calls to the object.
      One of the projects that accompanies this article is an ATL project named DeviceServer. Inside DeviceServer is an ATL COM class named CDevice that uses IStdMarshalInfo to inject a handler whose CLSID is CLSID_DeviceHandler into client processes. CDevice also implements a pair of custom interfaces named IDeviceStatus and IDeviceControl. IDeviceStatus contains two methods, GetLastPosition and GetCurrentPosition, that a client can use to inquire about the state of an imaginary device. IDeviceControl contains a method named SetPosition that can be used to input a new positional value.
      CDevice's source code is shown in Figure 1. Its IDeviceStatus and IDeviceControl methods do little more than return the process ID of the process that hosts the object instance, but in a few moments this will enable you to prove that handler marshaling is working. First, though, you have to write a handler.

Implementing a Handler

      The second step in implementing handler marshaling is to write the handler itself. Before you code a handler, it helps to understand how COM will use it. Figure 2 illustrates the architecture of handler marshaling in Windows 2000. In this example, the object that the handler represents has two interfaces: IDeviceStatus and IDeviceControl. COM's proxy manager implements these interfaces in the client process by loading and aggregating interface proxies from your proxy/stub DLL. The handler, in turn, aggregates the proxy manager and hides the proxy manager's IDeviceStatus interface behind an IDeviceStatus interface of its own. All other interfaces implemented by the proxy manager are exposed on the handler through blind delegation of QueryInterface calls.
Figure 2 Handler Marshaling in Windows 2000
Figure 2 Handler Marshaling in Windows 2000

      The handler itself is aggregated by a COM-provided identity object. The identity object exposes all the interfaces exposed by the handler and the proxy manager and throws in a controlling unknown as well as its own implementation of IMultiQI. The presence of the identity object enables COM to control the handler's lifetime and also solve a nasty race condition that occurs if two threads unmarshal interfaces implemented by the same handler at the same time. The identity object and the proxy manager are tightly coupled via private interfaces that are exposed by the proxy manager and known to the identity object. The handler neither knows nor cares about these interfaces.
      The fact that the handler in Figure 2 implements some, but not all, of the interfaces of the object it represents has an important implication. First, because the handler implements IDeviceStatus, calls to IDeviceStatus methods placed by clients will activate code inside the handler. This affords the handler the opportunity to process IDeviceStatus calls locally (that is, in-proc). But because the handler doesn't provide its own implementation of IDeviceControl, calls to IDeviceControl methods will go straight to the proxy manager and therefore incur standard marshaling. In other words, IDeviceControl method calls will be forwarded to the object at the other end of the wire as normal, but IDeviceStatus method calls will be intercepted by the handler and can be remoted or handled in-proc at the handler's discretion.
      Using ATL to write a handler is basically a matter of writing an ATL COM class that meets the following criteria:

  • Supports aggregation
  • Aggregates COM's proxy manager
  • Implements one or more of the object's interfaces
  • Delegates QueryInterface calls for interfaces it doesn't implement to the aggregated proxy manager
      The first requirement, aggregation support, is easy. ATL COM classes are aggregatable by default. A nonaggregatable ATL COM class can be made aggregatable by removing the DECLARE_NOT_ AGGREGATABLE statement from the class declaration.
      Aggregating COM's proxy manager is equally easy thanks to a new API function in Windows 2000. If passed a controlling unknown and an SMEXF_HANDLER flag, CoGetStdMarshalEx aggregates COM's proxy manager and returns an IUnknown interface pointer. By calling this function as shown in the following code from the FinalConstruct function that an ATL class inherits from CComObjectRootEx, a handler can aggregate the proxy manager the moment the handler is created:

// In the class declaration
DECLARE_GET_CONTROLLING_UNKNOWN ()
CComPtr<IUnknown> m_spUnkInner;

HRESULT FinalConstruct ()
{
    return CoGetStdMarshalEx (GetControllingUnknown (),
        SMEXF_HANDLER, &m_spUnkInner);
}
      Implementing in the handler some or all of the interfaces that the object implements is straightforward because implementing an interface in an ATL handler is no different than implementing an interface in any ATL COM class. One twist, however, is that when one of its methods is called, a handler can elect to process the method call itself or forward the call to the object that the handler is connected to. The latter is accomplished by using the IUnknown pointer returned by CoGetStdMarshalEx to query the proxy manager for a pointer to the corresponding interface on the object proxy and calling the method through that pointer. Here's how a handler's IDeviceStatus::GetCurrentPosition method would call the object's IDeviceStatus::GetCurrentPosition method via the controlling unknown stored in m_spUnkInner:

// The handler's IDeviceStatus::GetCurrentPosition method
STDMETHODIMP CDevice::GetCurrentPosition (DWORD* pdwPos)
{
    HRESULT hr;
    IDeviceStatus* pDeviceStatus;
    hr = m_spUnkInner->QueryInterface (IID_IDeviceStatus,
        (void**) &pDeviceStatus);

    if (FAILED (hr))
        return hr;

    hr = pDeviceStatus->GetCurrentPosition 
        (pdwPos);
    pDeviceStatus->Release ();
    return hr;
}
      Delegating other QueryInterface calls can be accomplished with ATL's COM_INTERFACE_ENTRY_AGGREGATE_BLIND macro. The following ATL interface map handles QueryInterface calls for IDeviceStatus interface pointers locally, but delegates QueryInterface calls for all other interface types to the inner unknown stored in m_spUnkInner:

BEGIN_COM_MAP(CDeviceHandler)
    COM_INTERFACE_ENTRY(IDeviceStatus)
    COM_INTERFACE_ENTRY_AGGREGATE_BLIND (m_spUnkInner.p)
END_COM_MAP()
      If m_spUnkInner holds the proxy manager's IUnknown interface pointer, then queries for interfaces other than IDeviceStatus will go to the proxy manager. This serves the dual purpose of providing the identity object with access to the proxy manager's non-public interfaces and providing the client with access to interfaces that aren't explicitly implemented by the handler.
      The DeviceHandlerServer project accompanying this column contains a handler for the device object in Figure 1. CDeviceHandler is the ATL COM class that implements the handler. CDeviceHandler (whose source code appears in Figure 3) provides its own implementation of IDeviceStatus, but exposes the proxy manager's IDeviceControl interface. Its IDeviceStatus::GetCurrentPosition implementation delegates to the device object, but its IDeviceStatus::GetLastPosition implementation handles calls locally. (In real life, GetLastPosition would probably access position data cached in the handler.) If you tested this handler, you'd expect to see calls to GetLastPosition execute much more quickly than calls to GetCurrentPosition.

Registering a Handler

      The final step in preparing for handler marshaling is to register the handler. Handlers are registered just like normal in-proc COM objects, with one important exception: handlers require InprocHandler32 keys rather than InprocServer32 keys. This crucial distinction appears to be undocumented. I once lost half a day's work trying to figure out why COM wouldn't instantiate my handler until House of COM columnist Don Box pointed out that I needed to change the key name.
      If you write your handler in ATL, edit the .rgs file that contains the handler's registration script and change InprocServer32 to InprocHandler32. See the .rgs file in Figure 3 for an example. Also, be sure to set ThreadingModel equal to Both so the handler can be created in the caller's apartment, regardless of what type of apartment the caller is located in.

The DeviceClient Application

      To test CDeviceHandler, you need a client. That client is in a separate Visual C++-created project named DeviceClient that you can download along with the other code for this column from MSDN® Magazine's Web site.
Figure 4 DeviceClient
Figure 4 DeviceClient

      DeviceClient, shown in Figure 4, is a dialog-based MFC application that uses CoCreateInstance to instantiate the device object in Figure 1. Clicking the Get Last Position, Get Current Position, and Set Position buttons calls the object's IDeviceStatus::GetLastPosition, IDeviceStatus::GetCurrentPosition, and IDeviceControl::SetPosition methods, respectively. As you can see in DeviceClientDlg.cpp (shown in Figure 5), the client does absolutely nothing different to account for the fact that a handler is being used. It simply creates the object and places calls to it in the normal way. Behind the scenes, the handler intercepts calls to the object's IDeviceStatus methods and handles GetLastPosition calls locally. You can prove it by clicking the Get Last Position and Get Current Position buttons and comparing the ensuing process IDs to the client's process ID, which is shown in the box at the top of the dialog. The process ID returned by GetLastPosition should match the client's process ID, proving that the call was handled in-process. The process ID returned by GetCurrentPosition, however, should be that of the server process, demonstrating that COM's standard marshaling mechanism was invoked when the handler passed the call to the proxy manager.
      That's what handler marshaling is all about: processing selected method calls in-proc while allowing others to be remoted. Handlers are great for improving the performance of method calls that can be handled without going out over the wire, and for providing local implementations of methods that for one reason or another aren't remotable. You may find even more creative uses for them; if you do, let me know.

Drop Me a Line

      Are there tough Win32®, MFC, Microsoft Transaction Services, or COM(+) programming questions you'd like answered? If so, drop me a note at jeffpro@msn.com. Please include "Wicked Code" in the message title. I regret that time doesn't permit me to respond to individual questions, but rest assured that each and every one will be considered for inclusion in a future column.

Jeff Prosise is the author of Programming Windows with MFC (Microsoft Press, 1999). He is also a cofounder of Wintellect, a software consulting and education firm whose Web address is http://www.wintellect.com.

From the August 2000 issue of MSDN Magazine.

Page view tracker