Cool code: eight lessons from COM experience
Release Date: 5/20/2004
| Update Date: 5/20/2004
Jeff Prosise
In daily work, I have seen many COM code written by different developers. I am surprised by many creative use of COM, there are some clever code that makes COM may not even think of Microsoft. Similarly, I saw some mistakes once again and again, so I can't help but be lazy. Many of these errors are related to threads and security, completely unprofit, and this is the most lack of two fields in COM documentation. If you don't have a careful plan, they are most likely to have two areas that may be to strengthen it.
The "Cool Code" column of this month is different from most of the previous columns. It does not provide a cool code that can be used in your own application. Instead, it will tell the correct way and wrong way to implement COM-based applications. It will tell some lessons from difficult practices, and how to avoid falling into a trap that has made many COM developers.
In the following space, you will read the eight programmers, these lessons come from their pain experience. Every story is true, but in order to protect the innocent, the name has been hidden. My purpose is to make you no longer repeat the mistakes of other COM programmers through these real COM stories. They may also help you find a place where there is a potential problem in the code written. No matter how the situation, I think you will get a pleasant reading experience.
This page
Always call Coinitialize (EX) Do not pass the original interface pointer between the threads Remove the message loop unit model object must protect shared data cautious start user DCOM is not suitable for firewalls to use threads or asynchronous calls to avoid DCOM timeout setting too long shared objects It's not easy to contact me.
Always call Coinitialize (EX)
A few months ago, I received a friend's email, he worked in a famous hardware company. His company has written a very complex COM-based application that uses many processes and local (processes) COM components. At the beginning, the application creates a COM object to serve various client threads running in multi-threaded units (MTAs). This object can also hosted to MTA, which means that the interface pointer can be freely exchanged between the client thread. In the test, my friend found that everything is good before the application is ready to close. Then, I don't know why, the call to Release (you must do this, to correctly release the client-side interface pointer) is locked. His question is: "Where is the problem?"
In fact, the answer is very simple. The application's developers are all right, only one exception, and this is very important: they do not call Coinitialize or CoinitializeEx in all client threads. One of the basic principles of modern COM is that each thread using COM should call Coinitialize or CoinitializeEx first to initialize COM. This principle is unable to avoid. In addition to other things, Coinitialize (EX) should put the thread into the unit and initialize the important per-thread status information (which is required for COM's correct operation). Calling Coinitialize (EX) Failed usually exhibits early COM API functions early in the application life period, the most common is to activate the request. But sometimes the problem is very hidden until everything is too late (for example, it is not returned to Release calls). When the development team adds Coinitialize (EX) to all threads that come into contact with COM, their problems are solved.
Ironically, Microsoft is actually one of the COM programmers sometimes not calling Coinitialize (EX). Some documents contained in the Microsoft Knowledge Base that call Coinitialize (EX) is not required for MTA-based threads (for example, see Articles Q150777). Yes, in many cases, we can skip Coinitialize (EX) without problems. However, this should not, unless you know what you are doing, and you can absolutely do you will not be negatively affected. Calling Coinitialize (ex) is no harmful, so I suggest COM programmers always call it from a COM-related thread. Back to top
Do not pass the original interface pointer between the thread
One of the first batch of COM projects I have involved involving a distributed application containing 100,000 lines of code, which is written by a large software company in the US West Coast. The application creates dozens of COM objects on multiple machines and calls these objects from the background thread that started from the client process. The development team encountered a problem, calling either disappeared without a trace, or fail without obvious reasons. The most amazing symptoms they give me: When a call cannot return, launch other applications that support COM on the same machine (including Microsoft Paint, etc.) will cause these applications to be locked.
After checking their code, they violated a basic rule for Com concurrent, that is, if a thread is to share an interface pointer with another thread, it should first package the interface pointer. If necessary, the encapsulated interface pointer allows COM to create a new agent (and a new channel object, nodules the proxy and stubbing) to allow for adjustment from another unit. Do not pass the original interface pointer (one 32-bit address in memory) to another thread, will bypass the COM, and if the transmitted and received thread is in different units, there will be various adverse behavior. (In Windows 2000, since the two objects can share a unit, it is in different contexts, so if the thread is located in the same unit, you may be caught in a dilemma.) Typical symptoms include call failed and returned RPC_E_Wrong_thread_ERROR.
Windows NT 4.0 and later You can use a pair of API functions called ComarshalinteRFACEADERFACEINSTREAM and COGETINTERFACEANDRELEASTREAM, and easily block interface pointers between threads. Suppose one of your applications (thread a) created a COM object, which relates to an IFOO interface pointer, and another thread in the same process (thread b) wants to call this object. When preparing to pass the interface pointer to thread B, thread a should be sealed by the interface pointer, as shown below:
ComarshalinterthreadInterfaceInstream (IID_IFOO, PFOO, & PStream);
After ComarshalinterthreadInterfaceInstream returns, thread b can safely cancel the interface pointer:
IFoo * pfoo;
COGETINTERFACEANDRELEASTREAM (Pstream, IID_IFOO, (Void **) & pfoo);
In these examples, the PFOO is an IFO interface pointer, and PStream is an ISTREAM interface pointer. CoM initializes the IStream interface pointer when calling ComarshalinterthreadInterfaceInstream, then uses and releases the interface pointer inside COGETISTERFACEANDRELEASTREAM. In fact, you usually use an event or other synchronization primitive to coordinate the behavior of these two threads - for example, let thread b know that the interface pointer is ready, can be canceled. Please note that the interface pointer does not have any problems in this way, because COM has sufficient intelligence, which is not to be sealed (or reinfert) pointers when it is not necessary to be sealed. If you do this, use COM, you can easily use COM.
If COMARSHALINTERTHREADITERFACEINSTREAM and COGETINTERFACEANDRELEASSTREAM look too much, you can also deliver interface pointers between threads by placing the interface pointer in the global interface table (Git) and let other threads retrieve them there. The interface pointer retrieved from the git will be automatically sealed during being retrieved. For more information, see the documentation in IGLOBALINTERFACETABLE. Note that Git is only in Windows NT 4.0 Service Pack 4 and later.
Back to top
STA thread requires a message loop
The application described in the previous section also has another deadly defect. See if you can point out.
This special application is exactly written in MFC. At first, it used the MFC's AFXBEGINTHREAD function to launch a series of secondary threads. Each auxiliary thread either calls Coinitialize either call AFXoleinit (a function similar to Coinitialize in MFC) to initialize COM. Some auxiliary threads call CocreateInstance to create a COM object and send the returned interface pointer to other auxiliary threads. Calling objects from creating these objects will be very smooth, but the call from other threads is never returned. Do you know why?
If you think the problem is related to the message loop (or missing message loop), then the answer is completely correct. The fact is indeed true. When a thread calls Coinitialize or AFXoleinit, it is placed in a single-line unit (STA). When COM creates a STA, it creates an attached hidden window. Method calls for objects in STA will be converted to messages and placed in a message queue associated with the STA. When the thread running in the STA is retrieved to the message that represents the method call, the window process of the hidden window will call the message conversion method. COM uses STA to perform call serialization. Objects in STA cannot receive more than one call once, because each call is passed to one and is the only thread running in the object cell.
What if the STA-based thread cannot handle the message? What if it doesn't have a message loop? The cell method call for the object in this STA will no longer return; they will be put on the message queue. There is no message cycle in the MFC assist thread, so if you are hosting objects in these STAs to receive methods from the client of other units, the MFC auxiliary threads and STAs are not good.
What is the meaning of this story? The STA thread requires a message loop unless you are sure they don't contain objects to call from other threads. Message loops can be simpler:
MSG msg;
While (GetMessage (& MSG, 0, 0, 0))
DispatchMessage (& MSG);
Another solution is to move the COM thread into the MTA (or moving to the neutral thread unit, i.e., NTA), where there is no message queue dependencies. Back to top
Unit model objects must protect shared data
Another common problem that plasmunications COM developers is within the process within the process of ThreadingModel = Apartment. This specification tells COM, the instance of the object must only be created in STA. It also allows COM freely place these object instances in the STA of any host process.
Suppose the client application has five STA threads, each thread uses CocreateInstance to create an instance of the same object. If the thread is based on STA, and the object is marked as threadingModel = APARTMENT, the five object instances will be created in the object's STA. Because each object instance is running on the thread of its STA, all five object instances can run in parallel.
So far, everything is good. Now consider what happens if these object instances share data. Because objects are executed concurrent threads, two or more objects may attempt to access the same data at the same time. Unless all these accesss are read, it will be disaster. The problem may not appear quickly; they will appear in a closed-related error form, so it is difficult to diagnose and reproduce. This explains the reason why the following is: ThreadingModel = Apartment object should include code synchronously access to shared data unless you can determine that the client's client does not overlap the method of performing access.
The problem is that too many COM developers believe that ThreadingModel = Apartment enables them to freely from code to write threads. The fact is not that - at least not exactly. ThreadingModel = Apartment does not mean that the object must be fully threaded. It represents a commitment to the COM, that is, the data shared by two or more object instances (or the data shared by this object and other objects) It is carried out in a way that thread is safe. The task of providing this thread security should be responsible by you, ie object implementors. Sharing data is a variety of sizes, but most of them appear in the form of static variables declared in a static member variable in the C class in a global variable. Even if this is the harmless statement, the problem is also in the STA:
Static int ncallcount = 0;
NCallCount ;
Because all instances of this object will share a nCallCount instance, write the correct way to write these statements is as follows:
Static int ncallcount = 0;
InterlockInCrement (& nCallCount);
Note: You can use critical regions, interlock functions, or any way you want, but don't forget to access STA-based object-sharing data to synchronize!
Back to top
Carefully launch users
There is still a problem here to have a lot of COM developers. Last spring, a company was burst with me, and their developers built a distributed application using COM, where the client process runs on the network workstation connected to the remote server's Singleton object. During the test, they have encountered some very strange behavior. In a test scenario, the client calls to CocreateInstanceex allows them to connect to the Singleton object. In another scene, the same call to COCREATEINSTANCEEX generates multiple object instances and multiple server processes, so that the client cannot connect to the same object instance, thereby actually affecting the application. In these two scenarios, hardware and software are identical. This problem seems to be related to safety. When the COM Service Control Manager (SCM) that is processed remotely, it assigns an identifier to the process when starting a process on another machine. Unless otherwise specified, the identifier it is to start the user's identity. In other words, the identity assigned to the server process is the same as the identity of the client process that starts it. In this case, if the BOB logs in machine A and connects the Singleton object on the machine B using the CoCreateInstanceEx, and Alice is also in the machine C, which will start two different server processes (at least two different WinStation). ), In fact, make the client unable to connect with the Singleton semantic to the shared object instance.
The reason why the two test scenarios will produce a large phase of the trunks, whose reasons are in a scene (that can work), all testers use a special account set to test with the same person's identity. In another scene, testers use their ordinary user account login. When two or more client processes have the same identity, they can be successfully connected to the server process configured to assume the launch user identifier. However, if the client has a different identifier, the SCM uses multiple server processes (each unique client ID) to assign the identity of different object instances.
Figure 1 User account in DCMCNFG
After finding the problem, it is very simple to solve: Configure the COM server, let it use a specific user account instead of assumed the identity of the user. One way to accomplish this is to run DCOMCNFG (Microsoft's DCOM Configuration Tool) on the server machine and change "Launching User" to "THIS USER" (see Figure 1). If you prefer to make changes (possibly from the installer), add a RunAs value in the COM server entry of the HKEY_CLASSES_ROOT / AppID section of the host registry (see Figure 2). You also need to use LSAStorePrivatedata to store the password of the RunAs account as the LSA key and use LsaAdDaccounts to ensure that the account has the "logon as batch job" permission. (For example, see the DCOMPERM example in the Platform SDK. Special attention to the function named SetrunasPassword and SetaccounTrights.)
Back to top
DCOM is not suitable for firewall
One common problem with DCOM features and features is: "Is it working across the Internet?" DCOM can work well with the Internet, just configure it to use TCP or UDP, and by granting anyone starts and access, Configure the server to allow an anonymous method to call. After all, Internet is a huge IP network. But contradictions, if you will change an existing DCOM app (good work in the company's internal network or intranet) to work across the Internet, it is very likely that it is very miserable. What can be caused? Firewall. The relationship between DCOM is born with the firewall is like the relationship between oil and water. One reason is that the SCM communication of the COM uses port 135 and SCM communication on other machines. The firewall limits the ports and protocols it can use, which may reject traffic passed through port 135. But a bigger problem is that in order to avoid application conflicts with a socket, pipelines, and other IPC mechanisms, DCOM does not fix the specific range of ports, which is the opposite, which is selected at runtime. By default, it can use any ports from 1,024 to 65, 535.
Allowing a DCOM application to open port 135 and ports 1,024-65,535 for the protocol to use for DCOM. (By default, Windows NT 4.0 is a UDP protocol. Windows 2000 is a TCP protocol.) However, this is much better than removing all firewalls. In this regard, your company's IT personnel may wish.
Another safer and more realistic solution is to limit the port range used by DCOM and only open a small range of ports for DCOM traffic. According to the principle of practice, you should assign a port for each server process, export the connection to the remote COM client (not a port of each interface pointer or one port of each object, but a server process). Configuring DCOM to use TCP instead of UDP is a good way, especially when the server performs a callback on its client.
The DCOM is used to remotely connect the protocol to be configured via the registry. On Windows 2000 and Windows NT 4.0 Service Pack 4 or later, you can use DCMCNFG to apply these configuration changes. The following is a way to configure DCOM to work through firewall.
Figure 3 Select the protocol
• On the server (in the firewall, a remote object is stored, the DCOM is configured to use TCP as its selected protocol, as shown in Figure 3. • On the server, restrict the port range of the DCOM will use. Remember to allocate at least one port for each server process. The example in Figure 4 limits DCOM as ports 8, 192 to 8, 195. • Open the port you select in step 2 so that the TCP traffic can pass through the firewall. At the same time, open port 135.
Figure 4 Select the port
Executing these steps, DCOM can work well across firewalls. If you prefer, SP4 and later allow you to specify endpoints for separate COM servers. For more information, please read the excellent papers of Michael Nelson About DCOM and firewalls, which can be found on the MSDN Online site (see http://msdn.microsoft.com/library/en-us/dndcom/html/msdn_dcomfirewall. ASP).
It should also be noted that by installing Internet Information Services (IIS) on the server and uses COM Internet Services (CIS) to use CIS to provide CIS to provide CIS to provide CIS to provide CIS to provide compatible with firewall DCOM. For more information on this topic, please refer to http://msdn.microsoft.com/library/en-us/dndcom/html/cis.asp. Back to top
Use thread or asynchronous call to avoid DCOM timeout setting too long
Always ask me to make a long-term setting too long when DCOM can't complete the remote instantiation request or method call. Typical scenes are as follows: The client calls CocreateInstanceex to instantiate an object on the remote machine, but this machine is temporarily offline. On Windows NT 4.0, the activation request will not fail immediately, and the DCOM may take a minute or more to return the failure of HRESULT. DCOM may also take a long time to fail to call the way to call the remote object that has no longer existed or its host has offline. If possible, how should developers avoid these longer timeout settings?
To answer this question, a few words are unclear. DCOM is highly dependent on the underlying network protocol and the RPC subsystem. There is no magical setting to let you limit the duration of the DCOM timeout setting. However, I often use two techniques to avoid longer overtime settings.
In Windows 2000, you can use asynchronous method calls to release the call thread when you are running in the COM channel. (For the introduction of asynchronous methods, please refer to "Windows 2000: Asynchronous Method Calls Eliminate the Wait For Com Clients and Servers Alike" in MSDN Magazine, 2000. If the asynchronous call does not return within a reasonable time, you can use IcancelMethodCalls :: Cancel on the invocation of the initialization call is canceled.
Windows NT 4.0 does not support asynchronous method calls, even in Windows 2000, asynchronous activation requests are not supported. How to solve it? Call the remote object (or instantiate the request for the object) from the background thread. Enable the main thread to block on the event object and specify the timeout set value to reflect the length of time you are willing to wait. When the call is returned, let the background thread set the event. Suppose the main thread uses WaitForsingleObject blocking. When WaitForsingleObject returns, the return value can tell you, return because method calls or activation requests return, or because you specified in the WaitForsingleObject call, the timeout setting expires in the waitforsingleObject call. You cannot unannounced calls in Windows NT 4.0, but at least the main thread can freely perform your own tasks.
The code in Figure 5 demonstrates how the client based on Windows NT 4.0 can call the object from the background thread. In this example, thread a encloses an IFOO interface pointer and starts thread B. Thread B cancels the interface pointer and calls iFoo :: bar. Regardless of the time spent returned to the call, thread A will not block more than 5 seconds because it passes 5,000 (microseconds) in the second parameter of WaitForsingleObject. This is not a good way, but if "no matter what happens in the other end of the line, thread a will not hang", it is important to endure this trouble.
Back to top
Shared object is not easy
Judging from the message I received and I have asked at the meeting, a problem that many COM programmers who plague many COM programmers is to connect two or more clients to an object instance. To answer this question, it is easy to write a long story (or a booklet), but in fact, as long as it explains the connection with the existing object, it is not easy to automate, it is sufficient. COM provides a large number of ways to create objects, including very popular CoCreateInstance (EX) functions. However, COM lacks a generic naming service that allows the name or GUID to identify the object instance. And it does not provide a built-in way to create an object, then identify it as a call to the interface pointer. Is this impossible to connect multiple clients with a single object instance? of course not. There are five ways to achieve this. In these resource links, you can find more information or even sample code to guide your operation. Please note that these technologies can not be interchangeable in general; usually, environmental factors will decide which method (if any) is applicable to hand: Singleton object Singleton object is only an object that is only instantiated. There may be 10 clients to call CocreateInstance to "create" Singleton objects, but in fact, they are all receiving interface pointers to the same object. The ATL COM class can be converted to Singleton by adding a Declare_ClassFactory_singleton statement in its class.
File Name Object If an object has implemented IPERSISTFILE, use the file name object in the Run Object (ROT) (which encapsulates the file name of the IPERSISTFILE :: LOAD method of the object), then the client can Use the file name object to connect an existing instance of the object. In fact, the file name object allows the file name to name the object instance, and the object can store their persistence data in these file names. They can even work across the machine.
ComarshalInterface and Counmarshalinterface Save the COM client for the interface pointer to share these interface pointers as long as they are willing to block pointers. COM is willing to provide an optimization of threads to other threads in the same process (see lesson 2), but if the client thread belongs to other processes, Comarshalinterface and CounmarshalinterFace are key ways to implement interface sharing. For discussion and sample code, please refer to MSJ ?? In the July 1999, I am a cool code column.
Custom Class Objects All "Create" COM objects are accompanied by independent objects, called class objects, and their role is an instance of the so-called COM object. Most class objects are implemented in an interface called IClassFactory, including a method of creating an object instance called CreateInstance. (In the underlying, COM converts COCREATEINSTANCE (EX) to call to iClassFactory :: CreateInstance). IclassFactory's problem is that when retrieving interface pointers that point to the previously created object instance, it is not available everywhere. Custom class object is a class object that implements a custom activation interface that replaces the iClassFactory. Since the interface is defined, you can define methods to retrieve references to objects that have been created by this class object. For more information, please refer to the Cool code column in February 1999 in ATL.
Custom Name Object In November 1999, the Cool Code Column introduces a custom name object class that allows the use of C style string named object instance. Pass an instance name to MkParsedisplayName, and you get a name object that is connected to the object instance. One disadvantage of these types of objects is that they cannot work across the Windows NT 4.0. Before using any of these methods Share an object instance between the client: Is it necessary to share? If the method calls from an external data source (database, hardware device, or even global variable) to retrieve data to respond to the client's request, why not assign an object instance for each client, and allow each instance to access the data source? ? You may have to synchronize access to the data source, but do not have to use a custom class object, custom name object, etc. This is how the application constructed by Microsoft Services (MTS), from various reasons, it has proven to be an intellectual programming model, not just the implementation of simple and performance improvement.
Back to top
Contact me
Do you have any difficult WIN32, MFC, MTS or COM ( ) programming problem needs to answer? If there is, please send me an email, the address is jeffpro@msn.com. Please add the words "WICKED CODE" in the subject. Please forgive time does not allow me to anize these questions, but for each problem, I will consider adding to the future column.
Jeff Prosise is the author of Programming Windows With MFC (Microsoft Press, 1999). He is one of the founders of Wintellect (a software consultation and education company), the company's website is http://www.wintellect.com.
From MSDN Magazine, November 2000. This magazine can be purchased through newsstands from all over the country or subscribe.
Go to the original English page