Cool code: eight lessons learned from the com experience released on: 5/20/2004 | updated on: 5/20/2004
Jeff prosise
In my daily work, I have seen a lot of COM code written by Different developers. I was surprised by many creative ways of working with COM. Some clever code that made com work may not even come to Microsoft's mind. Similarly, seeing some mistakes repeated over and over again makes me easy to get bored. Many of these errors are related to thread and security, which is totally out of proportion. This is exactly the two most missing fields in the com documentation. If you do not plan carefully, they are also the most likely to be encountered and may hold your two areas.
This month's "cool code" column is different from most previous columns. It does not provide a piece of cool code that can be used in your own applications. On the contrary, it describes the correct methods and Error Methods for implementing com-based applications. It will introduce some lessons from difficult practices and how to avoid falling into the trap that has left many com developers suffering.
In the following sections, you will read the descriptions of eight programmers, all of which come from their painful experiences. Every story is true, but the name is hidden to protect the innocent. My goal is to use these real com stories to prevent you from repeating the same mistakes of other COM programmers. They may also help you find potential problems in your written code. Regardless of the situation, I think you will enjoy a pleasant reading experience.
Content on this page
|
Always call coinitialize (Ex) |
|
Do not pass the original interface pointer between threads |
|
The STA thread needs message loop |
|
The Unit model object must protect shared data. |
|
Start users with caution |
|
DCOM is not suitable for Firewall |
|
Use threads or asynchronous calls to avoid too long DCOM timeout settings |
|
Sharing objects is not easy |
|
Contact me |
Always call coinitialize (Ex)
A few months ago, I received an email from a friend who worked in a famous hardware company. His company has compiled a very complex com-based application that uses many COM components inside and outside the process. At the beginning, the application created a COM object to serve various client threads running in the multi-threaded unit (MTA. This object can also be hosted on MTA, which means that interface pointers can be freely exchanged between client threads. During the test, my friend found that everything went well before the application was ready to be closed. Then, for some reason, the call to release (This call must be executed so that the interface pointer occupied by the client can be correctly released) is locked. His question is: "What is the problem ?"
In fact, the answer is very simple. The developers of the application do everything right, with only one exception, and this is very important: they did not call coinitialize or coinitializeex in all client threads. One of the basic principles of modern COM is that every thread using COM should first call coinitialize or coinitializeex to initialize COM. This principle cannot be waived. Except for other things, coinitialize (Ex) should put the thread into the unit and initialize important information about the status of each thread (this is required for the correct operations on com ). A failed call to coinitialize (Ex) is usually expressed in the form of failed com api functions early in the application life cycle. The most common is to activate a request. But sometimes the problem is hidden until everything is too late (for example, the call to release is gone forever. When the development team adds the coinitialize (Ex) call to all threads that are exposed to com, their problem is solved.
It is ironic that Microsoft is one of the reasons why com Programmers sometimes do not call coinitialize (Ex. In some documents contained in the Microsoft Knowledge Base, calling coinitialize (Ex) is not required for MTA-based threads (For examples, see q150777 ). Yes. In many cases, we can skip coinitialize (Ex) without any problems. However, this is not the case unless you know what you are doing and you are sure that you will not be negatively affected. Coinitialize (Ex) is harmless, so I suggest that com programmers always call it from a com-related thread.
Back to Top
Do not pass the original interface pointer between threads
One of the first com projects I consulted involved a distributed application containing 100,000 lines of code, which was compiled 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 these objects from the background thread started by the client process. When the development team encounters a problem, the call either disappears without a trace or fails without obvious reasons. The most striking symptom they showed me was that when a call fails to return, other com-enabled applications (including Microsoft Paint) are started on the same machine) these applications will be locked frequently.
Check their code and find that they violate a basic rule of COM concurrency, that is, if a thread wants to share an interface pointer with another thread, it should first mail the interface pointer. If necessary, the mail interface pointer allows com to create a new proxy (and a new channel object that pairs the proxy and the stub) to allow calls from another unit. Passing the original interface pointer (a 32-bit address in the memory) to another thread without sending messages will bypass the concurrency mechanism of COM, and if the sending and receiving threads are in different units, various undesirable behaviors will occur. (In Windows 2000, because two objects can share one unit but are in different contexts, if the thread is in the same unit, you may be in trouble .) Typical symptoms include call failure and rpc_e_wrong_thread_error.
For Windows NT 4.0 and later versions, you can use a pair of API functions named coexternalinterthreadinterfaceinstream and cogetinterfaceandreleasestream to easily mail interface pointers between threads. Assume that a thread (thread a) in your application creates a COM Object, receives an ifoo interface pointer, and another thread (thread B) in the same process wants to call this object. When you want to pass the interface pointer to thread B, thread a should mail the interface pointer as follows:
CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &pStream);
After codecomalinterthreadinterfaceinstream is returned, thread B can safely unseal the interface pointer:
IFoo* pFoo;CoGetInterfaceAndReleaseStream (pStream, IID_IFoo, (void**) &pFoo);
In these examples, pfoo is an ifoo interface pointer, and pstream is an istream interface pointer. Com initializes the istream interface pointer when calling coexternalinterthreadinterfaceinstream, and then uses and releases the interface pointer inside cogetinterfaceandreleasestream. In fact, you usually need to use an event or other synchronization primitive to coordinate the actions of the two threads-for example, to let thread B know that the interface pointer is ready, you can cancel sending.
Please note that this method will not cause any problems, because COM is smart enough and will not be sent (or re-sent) when you do not need to mail the interface pointer) pointer. If you do this when passing interface pointers between threads, it is much easier to use Com.
If calling coexternalinterthreadinterfaceinstream and cogetinterfaceandreleasestream looks too troublesome, you can also put the interface pointer in the global interface table (GIT) and ask other threads to retrieve them there, in this way, the interface pointer is passed between threads. The interface pointer retrieved from git is automatically sent when it is retrieved. For more information, see the document in iglobalinterfacetable. Note that git only exists in Windows NT 4.0 Service Pack 4 and later versions.
Back to Top
The STA thread needs message loop
The application described in the previous section has another fatal defect. Check whether you can point it out.
This special application happens to be written in MFC. At the beginning, it used the afxbeginthread function of MFC to start a series of auxiliary threads. Each auxiliary thread either calls coinitialize or afxoleinit (a function similar to coinitialize in MFC) to initialize COM. Some auxiliary threads call cocreateinstance to create a COM Object and mail the returned interface pointer to other auxiliary threads. Calling objects from the thread where these objects are created will be smooth, but calls from other threads will never be returned. Do you know why?
If you think the question is related to the message loop (or the message loop is missing), the answer is completely correct. This is true. When a thread calls coinitialize or afxoleinit, it is placed in a single thread unit (STA. When com creates a STA, it creates a hidden window that is included with it. Method calls with objects in the sta as the target will be converted to messages and placed in the message queue of the window associated with the STA. When the thread running in this sta retrieves messages that represent method calls, the window hiding process will convert the messages back to method calls. Com uses the sta to execute the call serialization. Objects in the sta cannot receive more than one call at a time, because each call must be passed to one and the only thread running in the object unit.
What if the Stas-based thread cannot process messages? What if it does not have a message loop? Inter-unit method calls for objects in the STA will not be returned; they will be permanently shelved in the message queue. There is no message loop in the MFC guides. Therefore, if the objects hosted in these Stas need to receive method calls from clients of other units, the MFC guides and Stas cannot work properly together.
What is the meaning of this story? The STA threads need message loops, unless you are sure they do not contain objects to be called from other threads. Message loops can be as simple as this:
MSG msg;while (GetMessage (&msg, 0, 0, 0))DispatchMessage (&msg);
Another solution is to move the com thread to the MTA (or in Windows 2000, to the neutral thread unit, that is, NTA), where there is no message queue dependency.
Back to Top
The Unit model object must protect shared data.
Another common problem for com developers is the in-process objects marked as threadingmodel = apartment. This parameter indicates that the object instance must be created only in the STA. It also allows com to freely place these object instances in the Stas of any host process.
Assume that the client application has five sta threads, and each thread uses cocreateinstance to create an instance of the same object. If the thread is based on the STA and the object is marked as threadingmodel = apartment, the five object instances will be created in the STA of the Object Creator. Because each object instance runs on the thread that occupies its STA, all five object instances can run concurrently.
So far, everything is good. Consider what happens if these object instances share data. Because all objects are executed on concurrent threads, two or more objects may attempt to access the same data at the same time. Unless all these accesses are read access, it will lead to a disaster. Problems may not appear soon; they may appear in time-related error forms, making it difficult to diagnose and reproduce. This explains the reasons for the following fact: threadingmodel = apartment object should include code that can synchronize access to shared data, unless you are sure that the client of the object does not call the methods that execute the access.
The problem is that too many com developers believe that threadingmodel = apartment can protect them from coding thread-safe code. This is not the case-at least not completely. Threadingmodel = apartment does not mean that the object must be completely thread-safe. It represents a commitment to com, that is, access to the data shared by two or more object instances (or the data shared by this object and other object instances) is conducted in a thread-safe manner. You are responsible for the tasks that provide thread security, that is, the object implementer. The types and sizes of shared data are diverse, but they are mostly in the form of global variables, static member variables in the C ++ class, and static variables declared in functions. Even the following harmless statements may cause problems in the sta:
static int nCallCount = 0;nCallCount++;
Because all instances of this object will share a ncallcount instance, the correct way to write these statements is as follows:
static int nCallCount = 0;InterlockIncrement (&nCallCount);
Note: You can use the critical section, interlock function, or any method you want, but do not forget to synchronize the data shared by the object based on the sta!
Back to Top
Start users with caution
There is another problem that has made many com developers suffer. Last spring, a company called me for help. Their developers used com to build a distributed application, the client process runs on the network workstation connected to the singleton object of the remote server. During the test, they encountered some very strange behavior. In a test scenario, the client calls cocreateinstanceex to connect them to the singleton object normally. In another scenario, multiple object instances and multiple server processes are generated for the same call to cocreateinstanceex, so that the client cannot connect to the same object instance, thus affecting the application. In these two scenarios, the hardware and software are identical.
This issue seems to be related to security. When the COM Service Control Manager (SCM) that processes the remote activation request starts a process on another machine, it assigns an identifier to the process. Unless otherwise specified, the selected identifier is the startup User Identifier. In other words, the identifier assigned to the server process is the same as that of the client process that started it. In this case, if Bob logs on to machine A and uses cocreateinstanceex to connect to the singleton object on machine B, Alice also works like this on machine C, two different server processes (at least two different winstations) will be started. In fact, the client cannot connect to the shared object instance with Singleton semantics.
The two test scenarios produce very different results, because in one scenario (the one that can work, all testers use a special account that is only set for the test to log on as the same person. In another scenario, testers use their normal user accounts to log on. When two or more client processes have the same identity, they can be successfully connected to the server process configured to start the user identity. However, if the client has different identifiers, SCM uses multiple server processes (each unique client identifies one) to separate the identifiers allocated to different object instances.
Figure 1 user account in dcomcnfg
After finding the problem, it is easy to solve: configure the COM server to use a specific user account instead of assuming the identity of the startup user. One way to complete this task is to run dcomcnfg (Microsoft's DCOM Configuration tool) on the server machine ), change "Launching user" to "this user" (see figure 1 ). If you prefer to change it programmatically (possibly starting with the installation program), add the RunAs value to the COM server entry in the hkey_classes_root \ appid section of the host registry (seeFigure 2). You also need to use lsastoreprivatedata to store the password of the RunAs account as the LSA key, and use lsaaddaccountrights to ensure that the account has the "Logon As Batch Job" permission. (For specific operation examples, see the dcomperm example in Platform SDK. Note the functions named setrunaspassword and setaccountrights .)
Back to Top
DCOM is not suitable for Firewall
A common problem about DCOM features and functions is: "Can it work across the Internet ?" DCOM can work well across the Internet, as long as it is configured to use TCP or UDP, and by granting anyone the start and access permissions, the server can be configured to allow anonymous method calls. After all, internet is a huge IP network. However, if you change an existing DCOM application (working well in the company's internal network or intranet) to work across the Internet, it is likely to fail badly. What is the possible cause? Firewall.
The relationship between DCOM and firewall is like the relationship between oil and water. One of the reasons is that the SCM of COM uses port 135 to communicate with SCM on other machines. The firewall limits the ports and protocols it can use, and may reject incoming traffic through port 135. But the bigger problem is that, in order to avoid conflicts with applications using sockets, pipelines, and other IPC Mechanisms, DCOM does not necessarily use ports of a specific range, it selects the port used during running. By default, it can use any port from 1,024 to 65,535.
One way to allow DCOM applications to use the firewall is to enable ports 135 and 1,024-65,535 for the protocol to be used by DCOM. (By default, Windows NT 4.0 is UDP and Windows 2000 is TCP .) However, this is not much better than removing all firewalls. Your company's IT staff may have to comment on this.
Another safer and more realistic solution is to limit the port range used by DCOM and open only a group of small ports for DCOM traffic. According to the practice, you should allocate a port for each server process to export the connection to a remote com client (not each interface pointer has one port or each object has one port, but one for each server process ). Configuring DCOM to use TCP instead of UDP is a good method, especially when the server calls back the client.
DCOM allows you to configure the port range and protocol used for remote connection through the registry. On Windows 2000 and Windows NT 4.0 Service Pack 4 or later, you can use dcomcnfg to apply these configuration changes. The following describes how to configure DCOM to work through the firewall.
Figure 3 protocol selection
• |
On the server (the machine that stores the remote object after the firewall), configure DCOM to use TCP as the selected protocol, as shown in 3. |
• |
On the server, restrict the port range that DCOM will use. Remember to assign at least one port to each server process. The example in Figure 4 limits DCOM to ports 8,192 to 8,195. |
• |
Open the port you selected in step 2 so that TCP traffic can pass through the firewall. Open port 135 at the same time. |
Figure 4 select a port
By performing these steps, DCOM can work well across firewalls. If you want to, SP4 and later also allow you to specify an endpoint for a separate COM server. For more information, see Michael Nelson's excellent paper on DCOM and firewall, which can be found at 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 Service (IIS) on the server and using COM Internet Service (CIS) to route DCOM traffic through port 80, SP4 and later versions can also use CIS to provide DCOM compatible with the firewall. For more information about this topic, see http://msdn.microsoft.com/library/en-us/dndcom/html/cis.asp
Back to Top
Use threads or asynchronous calls to avoid too long DCOM timeout settings
Some people always ask me the problem that the timeout setting is too long when DCOM cannot complete the remote instantiation request or method call. A typical scenario is as follows: the client calls cocreateinstanceex to instantiate an object on a remote machine, but this machine is temporarily offline. On Windows NT 4.0, activation requests do not immediately fail. DCOM may take one minute or longer to return the failed hresult. DCOM may also take a long time to make the method call to a remote object that no longer exists or its host is offline fail. If possible, how should developers avoid these long timeout settings?
The answer to this question is unclear. DCOM is highly dependent on the basic network protocol and RPC subsystem. There is no magical setting to limit the duration of DCOM timeout settings. However, I often use two tips to avoid the negative effect of long timeout settings.
In Windows 2000, when a call is suspended on the com channel, you can use an asynchronous method call to release the call thread. (For more information about Asynchronous Method calls, seeMsdn magazine"Windows April 2000: Asynchronous Method calleliminate the wait for COM clients and servers alike" published in 2000 ". If the asynchronous call does not return within a reasonable period of time, you can cancel it by calling icancelmethodcils: cancel on the call object used for initialization.
Windows NT 4.0 does not support asynchronous method calls, or even Windows 2000 does not support asynchronous activation requests. How can this problem be solved? Call a remote object from the background thread (or request to instantiate the object ). Blocks the main thread on the event object and specifies the timeout value to reflect the length of time you are willing to wait. When the call returns, let the background thread set the event. Assume that the main thread uses waitforsingleobject to block requests. When waitforsingleobject is returned, the returned value indicates whether the request is returned because of method call or activation, or because the timeout setting specified in the waitforsingleobject call expires. You cannot cancel a pending call in Windows NT 4.0, but at least the main thread can freely execute its own tasks.
Figure 5The code in demonstrates how clients based on Windows NT 4.0 can call objects from the background thread. In this example, thread a sends an ifoo interface pointer and starts thread B. Thread B unseals the interface pointer and calls ifoo: bar. No matter how long it takes to return a call, thread A does not block for more than five seconds because it passes 5,000 (in microseconds) in the second parameter of waitforsingleobject ). This is not a good solution, but if it is important that thread a will not be suspended no matter what happens at the other end of the line, it is worthwhile to endure such troubles.
Back to Top
Sharing objects is not easy
Judging from the emails I received and the questions I was asked at the meeting, one of the problems that plague many com programmers is how to connect two or more clients to an object instance. To answer this question, it is easy to write a long article (or a booklet), but it is enough to explain that the connection with the existing object is neither easy nor automated. Com provides a large number of object creation methods, including the Popular cocreateinstance (Ex) functions. However, com lacks a generic naming service that allows the use of names or guids to Identify object instances. In addition, it does not provide a built-in method to create an object, and then identifies it as the calling Target to retrieve the interface pointer.
Does this mean that it is impossible to connect multiple clients to 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 operations. Note that these technologies are not interchangeable in the general sense. Generally, environmental factors determine the method (if any) suitable for the tasks at hand: A singleton object is an object that is instantiated only once. There may be 10 clients that call cocreateinstance to "CREATE" Singleton objects, but in fact they all receive interface pointers pointing to the same object. The atl com class can be converted to singleton by adding the declare_classfactory_singleton statement to its class declaration.
File Name object if an object implements ipersistfile and uses the file name object (which encapsulates the ipersistfile passed to the object) in the running object table (ROT :: the file name of the load method. In fact, object names can be used to name object instances. Objects can store persistent data in these file names. They can even work across machines.
The COM clients that codecomalinterface and counexternalinterface save interface pointers can share these interface pointers with other clients as long as they are willing to mail pointers. Com provides Optimization for threads that are willing to block interface pointers to other threads in the same process (see Lesson 2). However, if the client thread belongs to another process, codecomalinterface and counexternalinterface are the key ways to achieve interface sharing. For the discussion and sample code, seeMSJ?? My cool code column was published in August 1999.
All "externally available" COM objects of custom class objects are accompanied by independent objects, called class objects. They are used to create instances of so-called COM objects. Most class objects must implement an interface named iclassfactory, including a method that can create an object instance named createinstance. (At the underlying layer, com converts a cocreateinstance (Ex) call to a call to iclassfactory: createinstance ). The problem with iclassfactory is that it is useless to retrieve the interface pointer to the previously created object instance. A custom class object is a class object that replaces the custom activation interface of iclassfactory. Because you have defined interfaces, you can also define methods to retrieve references to objects created by such objects. For more information and examples written in ATL, see the cool code column in February 1999.
The cool code column of the November 1999 custom name object describes the custom name object class that allows you to use a C style string to name an object instance. Pass an Instance name to mkparsedisplayname, and you get a name object connected to the object instance. One disadvantage of these types of name objects is that they cannot work across machines in Windows NT 4.0.
Before using any of these methods to share an object instance between clients, may I ask myself: is sharing necessary? If a method call retrieves data from an external data source (Database, hardware device, or even a global variable) to respond to client requests, why not assign an object instance to 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 custom class objects, custom name objects, and other means. This is how applications built using Microsoft Transaction Service (MTS) work. For various reasons, it has proved to be a compelling programming model, it is not just a simple implementation and performance improvement.
Back to Top
Contact me
Do you have any difficult questions about Win32, MFC, MTS, or COM (+) programming? If yes, please send me an email with the address of jeffpro@msn.com. Add "Wicked code" to the subject of the email. Forgive me for not allowing me to answer these questions individually. However, I will consider adding these questions to future columns.
Jeff prosiseYesProgramming windows with MFC(Microsoft Press, 1999) author. He is also one of the founders of wintellect, a software consulting and education company. His website is http://www.wintellect.com /.
From the November 2000 issue of msdn magazine.
This magazine can be purchased either through a newsstand orSubscription.
Go to the original English page