Custom Interfaces and Standard Marshaling

We saw earlier how the proxy and stub objects, as COM implements them internally, use registry entries to locate the necessary facelet and stublet implementations for any interface, which are assumed to be found inside an in-process server or handler. The server itself exposes a factory that implements IPSFactory, whereas each facelet implements IRpcProxyBuffer and each stublet implements IRpcStubBuffer.

OLE itself provides these implementations for all of its own remotable interfaces, so Local/Remote Transparency works automatically for only those interfaces. If you want to have a client and an object communicate through an interface of your own design, therefore, you must implement and register your own server for Local/Remote Transparency and plug it into the rest of COM's standard remoting facilities. There are hard ways and easy ways to accomplish this, as discussed in the following two sections. Both ways require the same registries, of the form:


\
Interface
{<IID>} = <Name of interface>
NumMethods = <Total number of interface members>
BaseInterface = <{IID} of base interface>
ProxyStubClsid32 = <{CLSID} of a server for the marshaler>

The NumMethods entry describes the total number of members in this interface, and BaseInterface identifies the IID of the interface from which the interface is derived. If no BaseInterface is given, COM assumes the IID of IUnknown. Finally, ProxyStubClsid32 provides the CLSID of the server whose class factory implements IPSFactoryBuffer, through which COM's standard marshaling can create new facelets and stublets for this interface, as described earlier.

The Hard Way: Manual Facelet and Stublet Implementation

As we learned in Chapter 5, implementing a DLL server is not a big deal. Implementing a server with a PSFactory is no more involved than writing a server with a class factory: you merely change the interface. Nor is it a big deal to create some skeletal facelet and stublet implementations that expose the correct interfaces and that behave properly through QueryInterface. All that remains is to implement the functions in IRpcProxyBuffer and IRpcStubBuffer along with the facelet's implementation of its marshaled interface. When a client calls the interface to marshal, the facelet places arguments in the RPC Channel and calls IRpcChannelBuffer::SendReceive. From here, COM calls the stublet's IRpcStubBuffer::Invoke, which extracts the arguments, calls the object, and sends all the return values back over. (This is described in APPB.WRI on the companion CD.)

How you decide to represent marshaled data for a custom interface is entirely up to you. In COM's remoting architecture, only the facelet and the stublet care about the format of the marshaled argument: therefore, you can write such code in whatever way you want. This works well for remoting within the same machine; in this case, the same server is used for facelet and stublet. When remoting occurs across a network, however, relying on the simultaneous installation of a compatible facelet/stublet server on all the different machines is no longer reasonable. Instead, facelets and stublets that support cross-network marshaling must conform to published standards for data representation. These standards are published along with Microsoft's distributed services for COM. They may or may not be available by the time you read this book.

Well, all of this doesn't sound so bad—only a few simple object implementations, right? Why do I call it the "hard way"? The truth of the matter is that for a reasonably useful and interesting interface, member functions deal with nontrivial arguments, especially when you start dealing with pointers to structures, pointers to strings, and other interface pointers that require you to create a new proxy and a new stub as well. Using the functions CoMarshalInterface and CoUnmarshalInterface along with CoLetMarshalSizeMax, CoMarshalHResult, and CoUnmarshalHResult is described in the OLE Programmer's Reference.

That said, implementing custom interface support for standard marshaling manually is entirely possible; it just takes more work.

The Easy Way: The MIDL Compiler

Instead of you implementing all the code yourself, Microsoft provides the Microsoft Interface Definition Language, or MIDL, compiler, which can take a simple text description of an interface (including any data structures you need in that interface) and churn out C code for both facelet and stublet, along with the header file that contains the interface definition itself.7 All you have to do afterwards is provide a DEF file and a make file, compile, link, register the server, and BOOM! Instant standard marshaling support! You even have the correct header file to include with any client or object code that you want to write using this interface.

The Microsoft Interface Definition Language itself is a set of extensions to the Open Software Foundation Distributed Computing Environment Interface Definition Language (OSF DCE IDL), the basic industry-standard RPC interface language. IDL is simply a language, and the MIDL compiler a tool, to relieve the tedium of manually creating marshaling support. The IDL documentation, the MIDL compiler, and the header files and import libraries that you need to work with MIDL are officially part of Microsoft RPC, which you'll find in the Win32 SDK. Your specific development environment (for example, Visual C++) might not include Microsoft RPC, so you might need the Win32 SDK to work with MIDL.

The IDL is similar to the Object Description Language (ODL) we saw in Chapter 3, and the two have a similar syntax. However, many keywords available in one are not available in the other to address specific problems. ODL is geared specifically to describing OLE servers and objects. IDL, on the other hand, is geared to cross-process and cross-network RPC and is capable of describing much more than is necessary to create marshaling code for some OLE interface. When describing a custom interface in IDL, you end up using only a small subset of its capabilities. For example, the following is an IDL file for the interface IAnimal, which we'll see later in a custom interface sample:


[uuid(0002114a-0000-0000-c000-000000000046),
object
]
interface IAnimal : IUnknown
{
import "unknwn.idl";

HRESULT Eat([in] LPTSTR pszFoodRecommended
, [in, out] LPTSTR pszFoodEaten, [in] short cchEaten);
HRESULT Sleep([in, out] short *pcMinutes);
HRESULT Procreate([out] short *pcOffspring);
HRESULT WhatKindOfAnimal([out] IID *pIID);
}

Describing an interface is mostly a matter of setting its IID and listing the member functions, their arguments, and the attributes for those arguments. In some cases, you'll also need to describe a data type that you use in the interface. Such structures, as well as any other argument types, must be unambiguous, meaning that the size of the data must be known and pointers must have a known type. See "Marshaling Considerations for Custom Interfaces" on the facing page.

Otherwise, the most important question is whether the arguments are in-parameters, out-parameters, or in/out-parameters. These directional attributes determine what sort of marshaling code is necessary for such an argument. An in-parameter is put into the channel by the facelet and is consumed in the stublet. The opposite is true for an out-parameter. With an in/out-parameter, the facelet sends it through the channel, the stublet sends new contents back, and the facelet extracts those new contents. Of course, the MIDL compiler hides all the details from you, as long as you describe the interface accurately.

Eventually, skipping the compilation step altogether might be possible. Instead, the system could dynamically create facelets and stublets at run time directly from an IDL script. This would be especially useful in a distributed environment. COM could then pass the IDL script across the network so that you don't have to worry about installing the custom interface marshalers on every system. It would certainly be a nice feature!

Marshaling Considerations for Custom Interfaces

The process of marshaling, especially for data structures and other pointers to data, means that a function's arguments must be re-created inside the object's address space exactly as they appear in the client's. Doing this on a single machine is relatively easy; when the process involves two different machines with potentially different operating systems and hardware architectures, implementation becomes more complicated, and it demands the unambiguous specification of your interface and associated data structures. In general, you should avoid any hardware-dependent types, such as int, as well as void * pointers, which do not specify exactly what is being pointed to.

To address this, Microsoft has listed a few rules of thumb that you should follow to best support marshaling across network boundaries:

7 The Win32 SDK header files for OLE-defined interfaces are defined in this manner. We saw the output in Chapter 2 for IUnknown.