COM Message Filters
By Fritz Onion
Published in C++ Report, October 1998 issue.
Components built in COM advertise themselves as being either thread-safe or not. If a component has advertised itself as not being thread safe, COM will go to great lengths to prevent multiple threads from calling methods on that object concurrently - it serializes access to the component. If a component claims that it is multi-thread safe, then COM will pass all method calls made to the object directly through, letting it deal with concurrent access issues. This scheme allows developers to choose what type of threading support they want to add to their objects, and still allow all components to interact together without harm. Building a component that is thread-safe gives the developer more control in handling concurrent access, and if written well, can improve the scalability of the component. On the other hand, building a component that is not thread-safe is usually much easier, and in many cases, sufficient for the task at hand. Most visual components, for example, are built without regard for thread-safety because it is not common for a visual component to be accessed by many clients simultaneously.
Interestingly, COM provides reentry capabilities for both types of components. That is, it is possible to make a call on a method of an object while it is sitting waiting for an outbound call it has made to complete. Reentry is naturally supported in multithreaded components because inbound calls can just be dispatched on a separate thread, but reentry into a single-threaded component requires some intervention to ensure correct behavior. In this column, I will look at the issue of reentry, discuss COM's solution to providing reentry to single-threaded components, and review the role of message filters in preventing unwanted reentry.
Apartments
COM defines the concept of an apartment to support the interaction of thread safe and non-thread safe components. An apartment is an execution context where objects with similar concurrency constraints can reside. All objects within an apartment share the same concurrency and reentry constraints, and can interact with other objects in the apartment with no intervention. However, access between objects in different apartments must be intercepted and dealt with properly.
Currently, there are two types of apartments in COM - multithreaded apartments (MTA) and single-threaded apartments (STA). For any given process, there is never more than one MTA. All components which reside in this apartment must be safe in the context of concurrent thread access. There may be one or more single-threaded apartments in a process. All components within an STA will never be accessed concurrently, and thus do not have to deal with synchronization primitives to protect their state from multi-threaded access. All serialization to an STA is performed by posting messages to a message queue, which are then translated as method invocations to the objects.
To join the MTA of a process, a given thread must call CoInitializeEx(0, COINIT_MULTITHREADED)
. Any objects created within this thread must be thread-safe. To join an STA, a thread can call CoInitialize(0)
or CoInitializeEx(0, COINIT_APARTMENTTHREADED)
, and any objects created within this thread will be protected from concurrent access, so the objects need not be thread safe. STA threads must also run a message loop to service incoming calls posted as messages, even if the thread has no associated window.
Reentry
Objects in both single-threaded and multithreaded apartments can be reentered.
To demonstrate reentry, consider the following interface which could be used to simply call back into an object:
[ uuid(26B94820-1003-11d1-8729-00A024D1F008), object ] interface INotify : IUnknown { HRESULT Notify(void); }
The object that supported calling back on this interface would need to provide some way for clients to register themselves to receive the callback. One approach for doing this is to provide an Advise()
method which registers a callback pointer, and an Unadvise()
method which revokes the pointer. We will keep our interfaces simple by supporting only one client to attach a callback interface pointer at a time.
[ uuid(120B2F50-F920-11d0-8707-00A024D1F008), object ] interface IObj : IUnknown { HRESULT Advise ([in] INotify* pN); HRESULT Unadvise(); }
The following code shows how the object might support these methods, providing generic callback capability.
class CoObj : public IObj { INotify* m_pNotify; ... STDMETHODIMP CoObj::Advise(INotify* pN) { if (m_pNotify) // Only allow one client return E_FAIL; if (!pN) return E_POINTER; m_pNotify = pN; m_pNotify->AddRef(); return S_OK; } STDMETHODIMP CoCalc::UnadviseMagicNumber() { if (!m_pNotify) return E_FAIL; m_pNotify->Release(); m_pNotify= NULL; return S_OK; }
To generate the actual notification, the object would simply invoke the Notify()
method of the cached interface pointer.
STDMETHODIMP CoObj::DoSomething(void) { // some code m_pNotify->Notify(); // Invoke callback // some other code... return S_OK; }
Client
Now, to be notified through this callback, a client must call the Advise()
method of IObj
with a pointer to an INotify
implementation. Suppose the client had implemented a COM object supporting INotify
as follows:
class CoNotify : public INotify { ULONG m_cRef; public: // IUnknown methods would go here // INotify methods STDMETHODIMP Notify() { // do something in response to notification } };
Registering an instance of this notification object might look like:
// Requesting object (pObj) to call our notify method CoNotify* pNotify = new CoNotify; pObj->Advise(pNotify);
Elsewhere in the client code, the client makes a call to the DoSomething()
method of the object:
pObj->DoSomething(); ...
During the synchronous call to DoSomething()
, the object will fire the Notify()
method of the client's notification object. This callback into the client's code must happen while the client is blocked waiting for the DoSomething()
method to return. If the client is executing in the MTA, the reentrant call will just be made on a separate thread, and the callback will be serviced immediately. However, if the client is executing in an STA, it can not be accessed on another thread, and it appears to be a deadlock.
Because of COM's support for reentry into single-threaded apartments, it is not a deadlock. What happens is, when the client calls the DoSomething()
method of the object, COM blocks its thread by placing it into a message loop. This message loop sits waiting for a special remote-procedure-call (RPC) message to return, indicating call completion. (COM has in fact set up an invisible window for the client's STA to which it can post messages and for which it has defined a special window procedure.) While your app is waiting in th message loop, another RPC request may come in (when Notify() is called, for example) and request to be serviced, which by default, your application does. In this case, it is critical that your app service the request (or completely deny it) so that you may continue execution. Most of the time, what you want to do is accept the default behavior of COM, and let the callback be serviced immediately so that things continue as scheduled. It is important to understand that servicing the callback will result in code being executed on your server perhaps in an order you did not expect, but there is no danger of concurrent access - this is guaranteed by the fact that you have advertised yourself as an STA. All code in the STA will be executed on the same thread, even when servicing a callback.
Callbacks into an STA will only be serviced when a message loop is dispatching messages. This can happen when you explicitly run a message loop in your application (through your main message loop, or perhaps a modal dialog message loop) or implicitly when you make an outbound COM call on an object (in which case, COM sets up a message loop for you, similar to a modal dialog box).
There may be occasions when you really don't want to be interrupted by a callback because it would jeopardize the correctness of your application. If this is really the case, there is a way to change this default behavior of callback interruption through an object called a message filter.
Message Filters
Message filters are COM objects that implement the IMessageFilter
interface:
interface IMessageFilter : IUnknown { DWORD HandleInComingCall ([in] DWORD dwCallType, [in] HTASK htaskCaller, [in] DWORD dwTickCount, [in] LPINTERFACEINFO lpInterfaceInfo); DWORD RetryRejectedCall([in] HTASK htaskCallee, [in] DWORD dwTickCount, [in] DWORD dwRejectType); DWORD MessagePending ( [in] HTASK htaskCallee, [in] DWORD dwTickCount, [in] DWORD dwPendingType); }
Message filters are registered with COM by calling CoRegisterMessageFilter()
. So the task of an application that wants to control reentrancy is to create a COM object supporting IMessageFilter
, implement HandleIncomingCall()
to reject or retry the call when circumstances require it, and to register the message filter when the program starts up. Message filters can also be used to prevent top-level calls from being serviced by an object. This is sometimes necessary in applications with user interfaces, when user interaction may prevent COM calls from being serviced.
Message filters are used on both the client and server sides of the picture. If the COM server has registered its own message filter, it is possible for it to control what happens when you reject or request a delay to occur. The two methods RetryRejectedCall()
and MessagePending()
are both used on the server side to determine what to do when clients have rejected your calls.
As an example of preventing reentry into our client, let's build a message filter that rejects any reentrant calls. To make it flexible, we will add a Boolean flag to let the message filter know when we want to block:
class MyMessageFilter : public IMessageFilter { ULONG m_cRef; bool m_bBlock; public: MyMessageFilter() : m_cRef(0), m_bBlock(false) {} // Request blocking of calls void Block() { m_bBlock = true; } // Allow calls to come through again void Unblock() { m_bBlock = false; } // IUnknown methods would go here // IMessageFilter methods STDMETHODIMP_(DWORD) HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo) { if (m_bBlock) return SERVERCALL_REJECTED; else return SERVERCALL_ISHANDLED; } STDMETHODIMP_(DWORD) RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType) { return -1; } STDMETHODIMP_(DWORD) MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType) { return PENDINGMSG_WAITDEFPROCESS; } };
Now, we register the message filter and set block to true whenever we don't want to receive notifications:
IMessageFilter* pOldMf; MyMessageFilter* gpMessageFilter; gpMessageFilter = new MyMessageFilter; gpMessageFilter->AddRef(); CoRegisterMessageFilter(gpMessageFilter, &pOldMf); // Don't want callbacks here gpMessageFilter->Block(); pObj->DoSomething(); gpMessageFilter->Unblock();
When the call to DoSomething()
on the object generates a Notify()
call, our message filter will be queried to see if our applicatin is ready to receive the callback. It will respond with a reject message, and the callback will die in its place. Notice that we now completely lost the notification because we rejected it.
Another approach would be to ask the server to try and send the notification later. In our implementation of HandleIncomingCall()
, we could return SERVERCALL_RETRYLATER
instead of SERVERCALL_REJECTED
. This would request the server to wait for some period of time and then retry the callback. However, this can lead to deadlock if not used properly. For example, if you used the same code above to perform the blocking and we asked the server to retry later, we would be in a deadlock because the server would not return from the DoSomething()
invocation until it had successfully retried the notification, and the client would continue requesting a retry until the DoSomething()
method returned. A good server takes this into account, and retries a fixed number of times, at which point it just gives up and no longer retries the call. If the server has not registered its own message filter, COM's default message filter will not retry the call, even though we requested it, so the default case will not cause a deadlock, but will lose the message.
One place where it is sometimes appropriate to use the retry later return value from HandleIncomingCall()
is if there is some transient activity that will stop in the near future which prevents the callback from coming through. For example, an application with a user interface may be interacting with the user in such a way that a callback while the user is doing something specific would result in incorrect behavior. Asking the server to retry later is reasonable, because once the user stops doing whatever he was doing, you can stop blocking the calls and process them.
There is quite a bit of flexibility in the definition of HandleIncomingCall()
. Notice that the last parameter to the method is a pointer to an INTERFACEINFO
structure. This structure in turn contains the IID
of the interface that is being called into. Thus the application can make individual decisions based on what interface is being called about whether to accept or reject the call.
MFC Message Filters
MFC implements a default message filter whenever its OLE functionality is used. MFC constructs and registers its message filter in its COM initialization call, AfxOleInit()
, usually placed in the application class' InitInstance90 method. MFC's implementation of IMessageFilter is found in its COleMessageFilter class, which provides a pair of methods (BeginBusyState()/EndBusyState()) instructing the message filter to either process incoming calls or request that they be tried again later. Setting the message filter to th ebusy state will block any incoming calls, but will not block nested (reentrant) calls into the object - they are always passed through by default:
class COleMessageFilter : public CCmdTarget { public: BOOL Register(); void Revoke(); virtual void BeginBusyState(); virtual void EndBusyState(); // IMessageFilter implementation };The message filter that MFC registers for you is accessible through the global function AfxOleGetMessageFilter(). Thus, an example of temporarily blocking any incoming COM methods to an MFC server would look like:
COleMessageFilter* pOmf = ::AfxOleGetMessageFilter(); pOmf->BeginBusyState(); // Do some uninterrupted work pOmf->EndBusyState();If you find that you need to block reentrant calls as well in an MFC application, you will still need to create and register your own message filter, as MFC's message filter deals only with top-level COM calls.
Conclusion
COM's support for reentry into any component, regardless of its threading model, grealy simplifies most designs. Calls can be made into any object at any time, from any thread, and COM will handle the details. If reentry could cause semantic problems in a single-threaded component, it's possible to change the default behavior and prevent top-level and/or reentrant calls from bein gserviced by registering a custom message filter.
'COM, ATL' 카테고리의 다른 글
How to Use IMessageFilter (1) | 2008.08.02 |
---|---|
Single Threaded Apartment(STA)에서 고려해야 할 몇가지 것들 (0) | 2008.08.02 |
Apartment Types (0) | 2008.07.30 |
TLS(Thread Local Storage) (0) | 2008.07.30 |
Safe Initialization and Scripting for ActiveX Controls (0) | 2008.07.29 |
COM+ 소개 - MTS(Microsoft Transaction Server) 후속작 (2) | 2008.07.28 |
분산객체 시스템(COM,COm+,DCOM,MTS) 에 대한 개념-2 (2) | 2008.07.28 |