IMessageFilter Member Functions

After calling CoRegisterMessageFilter, you can expect an occasional call to the IMessageFilter member functions. This section both explains how to implement each function and describes the behavior of COM's default filter.

IMessageFilter::HandleInComingCall

COM calls the HandleInComingCall function when it detects any remote call into the process that registered the message filter. In other words, this function is a single entry point for all external calls made into this process, regardless of whether that call is from a client to an object or from an object to a client's sink.


DWORD HandleInComingCall(DWORD dwCallType, HTASK threadIDCaller
, DWORD dwTickCount, LPINTERFACEINFO pInterfaceInfo);

typedef enum tagCALLTYPE //dwCallType values
{
CALLTYPE_TOPLEVEL = 1,
CALLTYPE_NESTED = 2,
CALLTYPE_ASYNC = 3,
CALLTYPE_TOPLEVEL_CALLPENDING = 4,
CALLTYPE_ASYNC_CALLPENDING = 5
} CALLTYPE;

typedef struct tagINTERFACEINFO
{
IUnknown *pUnk;
IID iid;
WORD wMethod;
} INTERFACEINFO;

For synchronous calls (CALLTYPE_TOPLEVEL, CALLTYPE_TOPLEVEL_CALLPENDING, and CALLTYPE_NESTED), the filter can process HandleInComingCall in the following ways:

When the call is asynchronous (CALLTYPE_ASYNC, a new call; or CALLTYPE_ASYNC_CALLPENDING, which occurs while this process is inside another synchronous call) or input-synchronized (as determined by the filter based on INTERFACEINFO), it doesn't really matter what a filter returns—HandleInComingCall merely gives the filter a chance to prepare before COM forwards the call to the appropriate object.

The dwTickCount argument to this function indicates the time elapsed since the original call was made unless dwCallType is CALLTYPE_TOPLEVEL, in which case dwTickCount is meaningless.

Default filter

Returns SERVERCALL_ISHANDLED in all circumstances.

IMessageFilter::RetryRejectedCall

When a client caller makes a call to a remote object (or an object makes a call back to a remote client), the recipient of that call can reject or delay the call by returning SERVERCALL_REJECTED or SERVERCALL_RETRYLATER from its implementation of HandleInComingCall.

When the recipient rejects a call, COM, on the caller side, first checks whether a connection to that remote process still exists, failing with RPC_E_CONNECTIONTERMINATED if one doesn't. If the connection is still valid, COM processes any pending incoming calls to the caller's process (calling HandleInComingCall in its own filter, of course) and tries to make the remote call again. If COM runs out of things to do on the caller side, the remote process might be in a temporary blocking state. COM then notifies the calling process by calling RetryRejectedCall in its filter, giving the caller the chance to wait for a while before trying again:


DWORD RetryRejectedCall(HTASK threadIDCallee, DWORD dwTickCount
, DWORD dwRejectType);

This also gives the caller a chance to display a dialog box informing the user of the delay, allowing the end user to wait longer or cancel the call completely, avoiding deadlocks. The dwTickCount argument contains the time elapsed since the original call so that RetryRejectedCall can check the limits of its patience.

The dwRejectType argument contains the SERVERCALL_REJECTED or SERVERCALL_RETRYLATER value that was returned from the recipient's HandleInComingCall. SERVERCALL_REJECTED is usually the final word—the caller shouldn't try the call again unless it has special knowledge about the state of the remote object, which is rare.

If the caller decides to give up, either because the object rejected the call or because a time-out occurred, it should return the value -1 from this function. As a result, the original calling code will see the error RPC_E_CALL_REJECTED.

Otherwise, the caller returns the number of milliseconds that COM should wait before trying the call again. Any value below 100, being too small for the resolution of the Windows timer (which is 55 ms), effectively means, "try again immediately," so COM doesn't bother to wait. Otherwise, COM sits in the message loop until the given time has elapsed and then tries the call again. If that call goes through, all is well; if the remote process rejects or delays the call again, COM will call RetryRejectedCall once again.

Eventually the caller's filter will run out of patience—usually after about 5 seconds (dwTickCount > 5000), the patience of a typical end user—at which time the filter can pop up a dialog box, using the task handle in threadIDCallee, to retrieve information about the remote task. Granted, a task handle isn't all that useful in itself, but with it you can invoke the standard UI busy dialog box shown in Figure 6-4. A standard implementation of this dialog exists in the OLE UI Library, as we'll see a little later. From this dialog, the end user can instruct RetryRejectedCall to return either -1 or some other value to wait once more.

Default filter

Fails all delays or rejections by returning -1.

Figure 6-4.

The standard busy dialog box implemented in the OLE UI Library.

IMessageFilter::MessagePending

While a caller is waiting for one of its own outgoing calls to be completed (the sort described by dwPendingType), it must still handle some additional user input, such as keystrokes and mouse clicks. If the remote application being called has taken the input focus and come to the foreground, such messages will end up in that application's message queue and in its user interface. In many cases, however, the focus does not change, so the end user continues to work in the caller application. Even if the remote application does take the focus, the end user can switch back to the caller. This introduces some complications—namely, what to do with the Windows messages that appear in the caller's queue while the caller is waiting for the remote call to finish. One way to address this is with the MessagePending member, as shown in the following:


DWORD MessagePending (HTASK threadIDCallee, DWORD dwTickCount
, DWORD dwPendingType);

typedef enum tagPENDINGTYPE
{
PENDINGTYPE_TOPLEVEL = 1,
PENDINGTYPE_NESTED = 2
} PENDINGTYPE;

//Return values
typedef enum tagPENDINGMSG
{
PENDINGMSG_CANCELCALL = 0,
PENDINGMSG_WAITNOPROCESS = 1,
PENDINGMSG_WAITDEFPROCESS = 2
} PENDINGMSG;

Usually the caller will want to dispatch WM_PAINT and WM_MOUSEMOVE messages but leave others in the queue for a short length of time, about 2 or 3 seconds. This is enough time for an end user's pending keyboard actions to be processed after the call returns. However, after 2 to 3 seconds (which MessagePending calculates from GetTickCount() - dwTickCount again), end-user input is no longer considered to be of the type-ahead variety but more along the lines of this-application-seems-dead-so-perhaps-slamming-the-keyboard-and-mouse-will-fix-it. In this case, the caller should flush the message queue and show the busy dialog box once again (using threadIDCallee in the same manner) to give the end user a chance to do something about the problem.

Now COM is spinning in a message loop inside the caller's process while waiting for the remote call to become complete. If a Windows message appears during this time, COM will check whether the connection has failed, returning appropriate error codes such as RPC_E_CONNECTIONTERMINATED and RPC_E_SERVER_DIED (connection is still valid but the thing sure is taking forever, like more than an hour). If the connection is still valid, COM calculates the elapsed time and calls MessagePending without removing the message from the queue.

Within MessagePending, you check the message and return one of the PENDINGMSG values to tell COM what to do with it. Returning PENDINGMSG_WAITDEFPROCESS tells COM to dispatch messages related to task switching or window activation, dispatch WM_PAINT and WM_TIMER messages, discard input messages, and continue to wait for a reply. Returning PENDINGMSG_WAITNOPROCESS tells COM to simply wait some more. Returning PENDINGMSG_CANCELCALL tells COM to drop out of its loop and return RPC_E_CALL_CANCELED from the outgoing call.

Default filter

Returns PENDINGMSG_WAITDEFPROCESS.