In my daily work, I have seen many COM codes written by different developers. I was amazed by the many creative ways of working with COM, and some of the clever code that made COM work might not even have occurred to Microsoft. Also, seeing some mistakes repeated again and again made me frustrated. Many of these errors are related to thread and security, and this is exactly the two areas that are missing from COM documentation. If you do not plan carefully, they are also the most likely to encounter and may stumble on your two areas.
This month's "Cool Code" column is different from most previous columns. It doesn't provide a piece of cool code that you can use in your own application. Instead, it describes the correct way and the wrong way to implement a COM-based application. It will tell you some lessons from tough practices and how to avoid falling into traps that have left many COM developers suffering.
In the following pages, you will read eight of programmers who have learned from their painful experiences. Every story is true, but the name is hidden to protect the innocent. My goal is to pass these true COM stories so that you no longer repeat the mistakes of other COM programmers. They may also help you identify potential problems in the code you write. No matter what the situation is, I think you will have a pleasant reading experience.
Always call CoInitialize (Ex)
A few months ago, I received an email from a friend who worked for a famous hardware company. His company has written a very complex, COM-based application that uses a number of in-process and local (out-of-process) COM components. At the beginning, the application creates a COM object to serve the various client threads running in the multithreaded apartment (MTA). The object can also be managed to the MTA, which means that the interface pointer can be exchanged freely between client threads. In the test, my friend found that everything was going well before the application was ready to shut down. Then, for some reason, the call to release (which must execute this call so that the interface pointer occupied by the client is properly freed) is locked. His question is: "What the hell is going on?" ”
In fact, the answer is very simple. The developers of the application are doing the right thing, with one exception, and this is important: they don't call CoInitialize or CoInitializeEx in all client threads. One of the basic principles of modern COM is that every thread that uses COM should first invoke CoInitialize or CoInitializeEx to initialize COM. This principle is not exempt from the law. In addition to other things, CoInitialize (Ex) should put the thread into the cell and initialize important per-thread state information (which is required for the proper operation of COM). Calling CoInitialize (Ex) failure is usually manifested in the form of a failed COM API function early in the application life cycle, most commonly the activation request. But sometimes the problem is hidden until it's too late (for example, the call to release is gone) to show. When the development team adds CoInitialize (Ex) calls to all threads that touch COM, their problems are solved.
Ironically, Microsoft was one of the reasons that COM programmers do not sometimes invoke CoInitialize (Ex). Some documents included in the Microsoft Knowledge Base say that calling CoInitialize (Ex) is not required for an MTA-based thread (for an example, see article Q150777). Yes, in many cases, we can skip CoInitialize (Ex) without having a problem. But that's not supposed to happen unless you know what you're doing and you can be absolutely certain that you're not going to be adversely affected. Calling CoInitialize (Ex) is harmless, so I recommend that COM programmers always call it from a COM-related thread.
Do not pass the original interface pointer between thread threads
One of the first COM projects I consulted involved a distributed application that contained 100,000 lines of code, written by a large software company on the west coast of the United States. The application creates dozens of COM objects on multiple machines and calls them from the background thread that the client process starts. The development team encountered a problem, the call either disappeared without a trace, or failed with no obvious cause. The most striking symptom they showed me was that when a call cannot be returned, launching other COM-enabled applications on the same machine (including Microsoft Paint, etc.) frequently causes these applications to be locked.
After examining their code, they found that they violated a basic rule of COM concurrency, that is, if a thread wants to share an interface pointer with another thread, it should first marshal the interface pointer. If necessary, the marshaling interface pointer enables COM to create a new proxy (and a new channel object, and a proxy and stub pair) to allow calls from another cell to be called out. Passing the original interface pointer (a 32-bit address in memory) to another thread without marshaling bypasses COM's concurrency mechanism, and if the sent and received threads are in different cells, a variety of undesirable behavior occurs. (In Windows 2000, because two objects can share a single cell, but also in different contexts, if the thread is in the same unit, you may get into trouble.) Typical symptoms include a call failure and a return to Rpc_e_wrong_thread_error.
Windows NT 4.0 and later can use a pair of API functions named Comarshalinterthreadinterfaceinstream and Cogetinterfaceandreleasestream, Easily marshal interface pointers between threads. Suppose a thread (thread A) in your application creates a COM object and then receives a IFOO interface pointer, and another thread (thread B) in the same process wants to invoke the object. When you are ready to pass the interface pointer to thread B, thread A should marshal the interface pointer as follows:
CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &pStream);
After Comarshalinterthreadinterfaceinstream returns, thread B can safely suppress the marshaling of this interface pointer:
IFoo* pFoo;
CoGetInterfaceAndReleaseStream (pStream, IID_IFoo, (void**) &pFoo);