February 1996
OLE Q & A
Don Box
Don Box is a principal scientist at DevelopMentor, where he builds tools and courseware for developers using OLE and COM. Don can be reached at dbox@braintrust.com.
Click to open or copy the SHARE project files.
QI'd like to put an object in shared memory to allow multiple processes to access it simultaneously. What is the best way to do this?
Posted Frequently to comp.object
ABefore you attempt to place an object into shared memory, it is important to note that simply by implementing your class as an out-of-process server in COM, your objects will automatically be sharable across multiple processes, provided that all of the interfaces that your object exports have proxy/stub pairs installed on the user's machine. This is inherent in the standard marshaling architecture of COM. But there are still valid reasons for placing an object into shared memory, the most common being efficiency. As noted in the December 1995 OLE Q&A, the cost of invoking methods on out-of-process objects is considerable. This cost can be attributed to the overhead of context switching, parameter marshaling, and message queuing under Windows NT' and Windows" 95. Placing the data members of an object into shared memory allows them to be accessed directly from within the client process without context switching, marshaling, or queuing, as long as the code that implements the methods has been mapped into each client's address space. This is the role of object handlers in COM.
Before I explain how handlers work, let's review some terms. The act of passing an object (or anything) from one process to another is called marshaling. COM supports two types of marshaling: standard and custom. Standard marshaling refers to passing an object from process A to process B by reference. If the object resides in process A, a proxy is created in process B that refers to the object via an RPC channel that is connected to a stub in the object's process. Fortunately, if that proxy (reference) is later passed to yet another process, a new proxy is created in the target process that is directly connected to the stub (see Figure 1), avoiding the unnecessary level of indirection that would result in creating a proxy to a proxy. This is similar to the way object references and pointers behave in traditional C++.
Figure 1 Object Sharing Using Standard Marshaling
Ultimately, objects/proxies are passed from one process to another via marshaling packets that contain whatever state is required to connect the client to the object. These packets are created by the marshaling code used to remote a given method call, and are populated with object references by calling the COM API function CoMarshalInterface:
HRESULT CoMarshalInterface(IStream *pstm
REFIID riid,
IUnknown *pUnk,
DWORD dwDestContext,
void *pvDestContext,
DWORD mshlflags);
If the marshaled object uses standard marshaling, CoMarshalInterface first examines the object (referred to by pUnk) to verify that it is not a handler or proxy. If pUnk points to an actual object, CoMarshalInterface then uses CoGetStandardMarshal to create the stub that will be used to manage the object side of the connection and fills the marshaling packet referred to by pstm with the unique identifier of the stub. This identifier, along with the CLSID of the handler that knows how to interpret it (CLSID_StdMarshal), is then transmitted to the receiver where it is ultimately unmarshaled from the packet and used to initialize and connect the proxy in the client's address space to the new stub. The COM function CoUnmarshalInterface creates and unmarshals the proxy.
HRESULT CoUnmarshalInterface(IStream *pstm
REFIID riid,
void **ppvObject);
CoUnmarshalInterface is called by the remoting code in the client's address space. CoUnmarshalInterface first reads the CLSID of the handler to be instantiated (here, CLSID_StdMarshal) from the stream and passes this value to CoCreateInstance to instantiate the handler (proxy) that will act as the client's reference to the object. CoUnmarshalInterface then instructs the handler to unmarshal what remains of the marshaling packet (in this example, the identifier of the stub) to establish the connection to the stub.
When the interface pointer that is passed to CoMarshalInterface points to a proxy and not an actual object implementation, the marshaling packet is formed by simply extracting the stub's unique identifier from the proxy. When this packet is unmarshaled in the remote process, it creates a second direct connection to the original stub, not an indirect connection through the original proxy.
To allow objects greater control over their distributional characteristics, COM allows an object to bypass the normal proxy/stub connection management used by standard marshaling. The object instead establishes a private subcontract for communications between the client and the object. To implement this private subcontract, the object must provide an inprocess handler that will be used in the client in lieu of the generic proxy used in standard marshaling. This is custom marshaling.
In contrast to standard marshaling, custom marshaling is implemented on an object-by-object basis, and must be implemented more or less by hand. The lack of tool support is due to the fact that when an object is custom marshaled from process A to process B, it is being passed by value. The actual value being passed is not necessarily a linear representation of the object's data members, but rather a serialized version of whatever state the object needs on the client side to "connect" to the object in the originating process. To give objects control over the transmission and reception of this state, COM specifies the IMarshal interface, which must be implemented by all objects that implement custom marshaling (see Figure 2). Given this, you can see in detail how objects are transmitted across marshaling contexts, and how to create and connect the handler based on the received packet (see Figure 3).
The implementations of CreatePacket and CreateHandler are very similar to the implementations of the COM API functions CoMarshalInterface and CoUnmarshalInterface. The fundamental difference is that if the initial QueryInterface for IMarshal fails, the API functions use CoGetStandardMarshal to attach the standard marshaler to the object, establishing a default proxy/stub connection.
Now let's place an object into shared memory. A reasonable strategy is to place the shared data members into a Win32" section object, and protect it from concurrent access using a mutex. The shared data members can be defined in a separate struct or class, and the handles to the mutex and file mapping objects can be stored in the handler along with the pointer to the shared memory. Assuming that the object's lifetime does not need to be bound to any one process, you can safely implement only an InprocHandler for our object that will support custom marshaling (see Figure 4).
Figure 4 Object Sharing Using Inproc Handlers
As much of the code for implementing the handler is boilerplate and not dependent on the shared state of the object (except for its size and CLSID), I chose to implement a generic shared object using templates, as is shown in Figures 5 and 6. The class CoSharedObjectBase contains most of the core code and is where IMarshal is implemented. As is shown in Figure 7, the handler maintains a pointer to a shared memory section where the actual object state is kept. Prepended to this is a shared reference count that keeps track of how many handlers are currently connected. When the section is initially created (in AttachToSection), the reference count is set to one and a virtual function call is made (OnInitializeSection) to allow the derived class to initialize the user-area of the section. When the final handler goes away, it makes a different virtual function call (OnDestroySection) to allow the derived class to clean up any state that may be associated with the shared members. As this implementation does not assume that the process identity of the object is fixed, it is entirely possible that the section will be initialized in one process and destroyed in another. For some applications, this is preferable; for others, it is not. Note that the template class CoSharedObject provides default implementations of OnInitializeSection and OnDestroySection that use placement to construct and destroy the shared state in place.
Figure 5 CoSharedObject Hierarchy
Figure 7 Sharing State Between Handlers
To identify the section and the mutex that protects it, there needs to be a way to uniquely identify the Win32 kernel objects that the initial handler creates. Since you're fairly immersed in COM, it makes sense to use-you guessed it-a GUID. You can easily create GUIDs at run time by calling the API function CoCreateGuid. You can use the GUID as an object ID. As is shown in Figure 6, CoSharedObjectBase uses CoCreateGuid in AccessSharedData (from SHAREDOB.CPP) to generate the shared object's ID the first time the object is accessed prior to marshaling. CoSharedObjectBase's implementation of MarshalInterface simply transmits the object ID to the receiving client. CoSharedObjectBase's implementation of UnmarshalInterface then reads the object ID in the client's process and opens the section and mutex by calling AttachToSection.
Figure 8 demonstrates the CoSharedObject template by implementing a simple object that keeps two ints and a string in shared memory. Note that the implementation of each of the member functions defines an instance of a class SharedThis at the beginning of the method. SharedThis is a nested class in CoSharedObject that provides a typed pointer to the shared state. It also acquires the mutex in its constructor and releases it in its destructor. Declaring an instance of SharedThis in each method guarantees that at most one thread will be accessing the shared object at any time. Figure 9 shows several clients accessing a single shared object simultaneously.
Figure 9 Multiple clients access a single shared object simultaneously.
The implementation described here achieves performance close to that of an inprocess server for method invocation, as no context switching or marshaling needs to be performed. However, there are some tradeoffs. First, this implementation is extremely memory hungry. While the handler is very small and lightweight, the shared section winds up consuming at least 4KB due to page size granularity. This could be minimized by implementing a shared memory allocator (perhaps using CoSharedObject) and implementing malloc and free on top of the section. A more problematic tradeoff is the fact that the template supports sharing objects that have only instance data members (no pointer members or handles). If you want to share a linked list or file handle as a shared data member, you can look forward to a nontrivial job using the TypingWizard in Visual C++ to implement the pointer chasing and/or handle duplication required to get things to work properly.
Have a question about programming in OLE? You can mail it directly to Q&A, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re:OLE Q&A) via:
Internet:
Don Box
dbox@braintrust.com
Eric Maffei
ericm@microsoft.com