COM in Wine
Writing COM Components for Wine
This section describes how to create your own natively
compiled COM components.
Macros to define a COM interface
The goal of the following set of definitions is to provide a
way to use the same header file definitions to provide both
a C interface and a C++ object oriented interface to COM
interfaces. The type of interface is selected automatically
depending on the language but it is always possible to get
the C interface in C++ by defining CINTERFACE.
It is based on the following assumptions:
all COM interfaces derive from IUnknown, this should not
be a problem.
the header file only defines the interface, the actual
fields are defined separately in the C file implementing
the interface.
The natural approach to this problem would be to make sure
we get a C++ class and virtual methods in C++ and a
structure with a table of pointer to functions in C.
Unfortunately the layout of the virtual table is compiler
specific, the layout of g++ virtual tables is not the same
as that of an egcs virtual table which is not the same as
that generated by Visual C++. There are work arounds to make
the virtual tables compatible via padding but unfortunately
the one which is imposed to the Wine emulator by the Windows
binaries, i.e. the Visual C++ one, is the most compact of
all.
So the solution I finally adopted does not use virtual
tables. Instead I use in-line non virtual methods that
dereference the method pointer themselves and perform the
call.
Let's take Direct3D as an example:
#define ICOM_INTERFACE IDirect3D
#define IDirect3D_METHODS \
ICOM_METHOD1(HRESULT,Initialize, REFIID,) \
ICOM_METHOD2(HRESULT,EnumDevices, LPD3DENUMDEVICESCALLBACK,, LPVOID,) \
ICOM_METHOD2(HRESULT,CreateLight, LPDIRECT3DLIGHT*,, IUnknown*,) \
ICOM_METHOD2(HRESULT,CreateMaterial,LPDIRECT3DMATERIAL*,, IUnknown*,) \
ICOM_METHOD2(HRESULT,CreateViewport,LPDIRECT3DVIEWPORT*,, IUnknown*,) \
ICOM_METHOD2(HRESULT,FindDevice, LPD3DFINDDEVICESEARCH,, LPD3DFINDDEVICERESULT,)
#define IDirect3D_IMETHODS \
IUnknown_IMETHODS \
IDirect3D_METHODS
ICOM_DEFINE(IDirect3D,IUnknown)
#undef ICOM_INTERFACE
#ifdef ICOM_CINTERFACE
// *** IUnknown methods *** //
#define IDirect3D_QueryInterface(p,a,b) ICOM_CALL2(QueryInterface,p,a,b)
#define IDirect3D_AddRef(p) ICOM_CALL (AddRef,p)
#define IDirect3D_Release(p) ICOM_CALL (Release,p)
// *** IDirect3D methods *** //
#define IDirect3D_Initialize(p,a) ICOM_CALL1(Initialize,p,a)
#define IDirect3D_EnumDevices(p,a,b) ICOM_CALL2(EnumDevice,p,a,b)
#define IDirect3D_CreateLight(p,a,b) ICOM_CALL2(CreateLight,p,a,b)
#define IDirect3D_CreateMaterial(p,a,b) ICOM_CALL2(CreateMaterial,p,a,b)
#define IDirect3D_CreateViewport(p,a,b) ICOM_CALL2(CreateViewport,p,a,b)
#define IDirect3D_FindDevice(p,a,b) ICOM_CALL2(FindDevice,p,a,b)
#endif
Comments:
The ICOM_INTERFACE macro is used in the ICOM_METHOD macros
to define the type of the 'this' pointer. Defining this
macro here saves us the trouble of having to repeat the
interface name everywhere. Note however that because of the
way macros work, a macro like ICOM_METHOD1 cannot use
'ICOM_INTERFACE##_VTABLE' because this would give
'ICOM_INTERFACE_VTABLE' and not 'IDirect3D_VTABLE'.
ICOM_METHODS defines the methods specific to this
interface. It is then aggregated with the inherited methods
to form ICOM_IMETHODS.
ICOM_IMETHODS defines the list of methods that are
inheritable from this interface. It must be written manually
(rather than using a macro to generate the equivalent code)
to avoid macro recursion (which compilers don't like).
The ICOM_DEFINE finally declares all the structures
necessary for the interface. We have to explicitly use the
interface name for macro expansion reasons again. Inherited
methods are inherited in C by using the IDirect3D_METHODS
macro and the parent's Xxx_IMETHODS macro. In C++ we need
only use the IDirect3D_METHODS since method inheritance is
taken care of by the language.
In C++ the ICOM_METHOD macros generate a function prototype
and a call to a function pointer method. This means using
once 't1 p1, t2 p2, ...' and once 'p1, p2' without the
types. The only way I found to handle this is to have one
ICOM_METHOD macro per number of parameters and to have it
take only the type information (with const if necessary) as
parameters. The 'undef ICOM_INTERFACE' is here to remind
you that using ICOM_INTERFACE in the following macros will
not work. This time it's because the ICOM_CALL macro
expansion is done only once the 'IDirect3D_Xxx' macro is
expanded. And by that time ICOM_INTERFACE will be long gone
anyway.
You may have noticed the double commas after each parameter
type. This allows you to put the name of that parameter
which I think is good for documentation. It is not required
and since I did not know what to put there for this example
(I could only find doc about IDirect3D2), I left them blank.
Finally the set of 'IDirect3D_Xxx' macros is a standard set
of macros defined to ease access to the interface methods in
C. Unfortunately I don't see any way to avoid having to
duplicate the inherited method definitions there. This time
I could have used a trick to use only one macro whatever the
number of parameters but I preferred to have it work the same
way as above.
You probably have noticed that we don't define the fields we
need to actually implement this interface: reference count,
pointer to other resources and miscellaneous fields. That's
because these interfaces are just that: interfaces. They may
be implemented more than once, in different contexts and
sometimes not even in Wine. Thus it would not make sense to
impose that the interface contains some specific fields.
Bindings in C
In C this gives:
typedef struct IDirect3DVtbl IDirect3DVtbl;
struct IDirect3D {
IDirect3DVtbl* lpVtbl;
};
struct IDirect3DVtbl {
HRESULT (*fnQueryInterface)(IDirect3D* me, REFIID riid, LPVOID* ppvObj);
ULONG (*fnAddRef)(IDirect3D* me);
ULONG (*fnRelease)(IDirect3D* me);
HRESULT (*fnInitialize)(IDirect3D* me, REFIID a);
HRESULT (*fnEnumDevices)(IDirect3D* me, LPD3DENUMDEVICESCALLBACK a, LPVOID b);
HRESULT (*fnCreateLight)(IDirect3D* me, LPDIRECT3DLIGHT* a, IUnknown* b);
HRESULT (*fnCreateMaterial)(IDirect3D* me, LPDIRECT3DMATERIAL* a, IUnknown* b);
HRESULT (*fnCreateViewport)(IDirect3D* me, LPDIRECT3DVIEWPORT* a, IUnknown* b);
HRESULT (*fnFindDevice)(IDirect3D* me, LPD3DFINDDEVICESEARCH a, LPD3DFINDDEVICERESULT b);
};
#ifdef ICOM_CINTERFACE
// *** IUnknown methods *** //
#define IDirect3D_QueryInterface(p,a,b) (p)->lpVtbl->fnQueryInterface(p,a,b)
#define IDirect3D_AddRef(p) (p)->lpVtbl->fnAddRef(p)
#define IDirect3D_Release(p) (p)->lpVtbl->fnRelease(p)
// *** IDirect3D methods *** //
#define IDirect3D_Initialize(p,a) (p)->lpVtbl->fnInitialize(p,a)
#define IDirect3D_EnumDevices(p,a,b) (p)->lpVtbl->fnEnumDevice(p,a,b)
#define IDirect3D_CreateLight(p,a,b) (p)->lpVtbl->fnCreateLight(p,a,b)
#define IDirect3D_CreateMaterial(p,a,b) (p)->lpVtbl->fnCreateMaterial(p,a,b)
#define IDirect3D_CreateViewport(p,a,b) (p)->lpVtbl->fnCreateViewport(p,a,b)
#define IDirect3D_FindDevice(p,a,b) (p)->lpVtbl->fnFindDevice(p,a,b)
#endif
Comments:
IDirect3D only contains a pointer to the IDirect3D
virtual/jump table. This is the only thing the user needs to
know to use the interface. Of course the structure we will
define to implement this interface will have more fields but
the first one will match this pointer.
The code generated by ICOM_DEFINE defines both the structure
representing the interface and the structure for the jump
table. ICOM_DEFINE uses the parent's Xxx_IMETHODS macro to
automatically repeat the prototypes of all the inherited
methods and then uses IDirect3D_METHODS to define the
IDirect3D methods.
Each method is declared as a pointer to function field in
the jump table. The implementation will fill this jump table
with appropriate values, probably using a static variable,
and initialize the lpVtbl field to point to this variable.
The IDirect3D_Xxx macros then just dereference the lpVtbl
pointer and use the function pointer corresponding to the
macro name. This emulates the behavior of a virtual table
and should be just as fast.
This C code should be quite compatible with the Windows
headers both for code that uses COM interfaces and for code
implementing a COM interface.
Bindings in C++
And in C++ (with gcc's g++):
typedef struct IDirect3D: public IUnknown {
private: HRESULT (*fnInitialize)(IDirect3D* me, REFIID a);
public: inline HRESULT Initialize(REFIID a) { return ((IDirect3D*)t.lpVtbl)->fnInitialize(this,a); };
private: HRESULT (*fnEnumDevices)(IDirect3D* me, LPD3DENUMDEVICESCALLBACK a, LPVOID b);
public: inline HRESULT EnumDevices(LPD3DENUMDEVICESCALLBACK a, LPVOID b)
{ return ((IDirect3D*)t.lpVtbl)->fnEnumDevices(this,a,b); };
private: HRESULT (*fnCreateLight)(IDirect3D* me, LPDIRECT3DLIGHT* a, IUnknown* b);
public: inline HRESULT CreateLight(LPDIRECT3DLIGHT* a, IUnknown* b)
{ return ((IDirect3D*)t.lpVtbl)->fnCreateLight(this,a,b); };
private: HRESULT (*fnCreateMaterial)(IDirect3D* me, LPDIRECT3DMATERIAL* a, IUnknown* b);
public: inline HRESULT CreateMaterial(LPDIRECT3DMATERIAL* a, IUnknown* b)
{ return ((IDirect3D*)t.lpVtbl)->fnCreateMaterial(this,a,b); };
private: HRESULT (*fnCreateViewport)(IDirect3D* me, LPDIRECT3DVIEWPORT* a, IUnknown* b);
public: inline HRESULT CreateViewport(LPDIRECT3DVIEWPORT* a, IUnknown* b)
{ return ((IDirect3D*)t.lpVtbl)->fnCreateViewport(this,a,b); };
private: HRESULT (*fnFindDevice)(IDirect3D* me, LPD3DFINDDEVICESEARCH a, LPD3DFINDDEVICERESULT b);
public: inline HRESULT FindDevice(LPD3DFINDDEVICESEARCH a, LPD3DFINDDEVICERESULT b)
{ return ((IDirect3D*)t.lpVtbl)->fnFindDevice(this,a,b); };
};
Comments:
In C++ IDirect3D does double duty as both the virtual/jump
table and as the interface definition. The reason for this
is to avoid having to duplicate the method definitions: once
to have the function pointers in the jump table and once to
have the methods in the interface class. Here one macro can
generate both. This means though that the first pointer,
t.lpVtbl defined in IUnknown, must be interpreted as the
jump table pointer if we interpret the structure as the
interface class, and as the function pointer to the
QueryInterface method, t.fnQueryInterface, if we interpret
the structure as the jump table. Fortunately this gymnastic
is entirely taken care of in the header of IUnknown.
Of course in C++ we use inheritance so that we don't have to
duplicate the method definitions.
Since IDirect3D does double duty, each ICOM_METHOD macro
defines both a function pointer and a non-virtual inline
method which dereferences it and calls it. This way this
method behaves just like a virtual method but does not
create a true C++ virtual table which would break the
structure layout. If you look at the implementation of these
methods you'll notice that they would not work for void
functions. We have to return something and fortunately this
seems to be what all the COM methods do (otherwise we would
need another set of macros).
Note how the ICOM_METHOD generates both function prototypes
mixing types and formal parameter names and the method
invocation using only the formal parameter name. This is the
reason why we need different macros to handle different
numbers of parameters.
Finally there is no IDirect3D_Xxx macro. These are not
needed in C++ unless the CINTERFACE macro is defined in
which case we would not be here.
This C++ code works well for code that just uses COM
interfaces. But it will not work with C++ code implement a
COM interface. That's because such code assumes the
interface methods are declared as virtual C++ methods which
is not the case here.
Implementing a COM interface.
This continues the above example. This example assumes that
the implementation is in C.
typedef struct _IDirect3D {
void* lpVtbl;
// ...
} _IDirect3D;
static ICOM_VTABLE(IDirect3D) d3dvt;
// implement the IDirect3D methods here
int IDirect3D_fnQueryInterface(IDirect3D* me)
{
ICOM_THIS(IDirect3D,me);
// ...
}
// ...
static ICOM_VTABLE(IDirect3D) d3dvt = {
ICOM_MSVTABLE_COMPAT_DummyRTTIVALUE
IDirect3D_fnQueryInterface,
IDirect3D_fnAdd,
IDirect3D_fnAdd2,
IDirect3D_fnInitialize,
IDirect3D_fnSetWidth
};
Comments:
We first define what the interface really contains. This is
the _IDirect3D structure. The first field must of course be
the virtual table pointer. Everything else is free.
Then we predeclare our static virtual table variable, we
will need its address in some methods to initialize the
virtual table pointer of the returned interface objects.
Then we implement the interface methods. To match what has
been declared in the header file they must take a pointer to
an IDirect3D structure and we must cast it to an _IDirect3D
so that we can manipulate the fields. This is performed by
the ICOM_THIS macro.
Finally we initialize the virtual table.
A brief introduction to DCOM in Wine
This section explains the basic principles behind DCOM remoting as used by InstallShield and others.
Basics
The basic idea behind DCOM is to take a COM object and make it location
transparent. That means you can use it from other threads, processes and
machines without having to worry about the fact that you can't just
dereference the interface vtable pointer to call methods on it.
You might be wondering about putting threads next to processes and
machines in that last paragraph. You can access thread safe objects from
multiple threads without DCOM normally, right? Why would you need RPC
magic to do that?
The answer is of course that COM doesn't assume that objects actually
are thread-safe. Most real-world objects aren't, in fact, for various
reasons. What these reasons are isn't too important here, though; it's
just important to realize that the problem of thread-unsafe objects is
what COM tries hard to solve with its apartment model. There are also
ways to tell COM that your object is truly thread-safe (namely the
free-threaded marshaller). In general, no object is truly thread-safe if
it could potentially use another not so thread-safe object, though, so
the free-threaded marshaller is less used than you'd think.
For now, suffice it to say that COM lets you "marshal" interfaces into
other "apartments". An apartment (you may see it referred to as a
context in modern versions of COM) can be thought of as a location, and
contains objects.
Every thread in a program that uses COM exists in an apartment. If a
thread wishes to use an object from another apartment, marshalling and
the whole DCOM infrastructure gets involved to make that happen behind
the scenes.
So. Each COM object resides in an apartment, and each apartment
resides in a process, and each process resides in a machine, and each
machine resides in a network. Allowing those objects to be used
from any of these different places is what DCOM
is all about.
The process of marshalling refers to taking a function call in an
apartment and actually performing it in another apartment. Let's say you
have two machines, A and B, and on machine B there is an object sitting
in a DLL on the hard disk. You want to create an instance of that object
(activate it) and use it as if you had compiled it into your own
program. This is hard, because the remote object is expecting to be
called by code in its own address space - it may do things like accept
pointers to linked lists and even return other objects.
Very basic marshalling is easy enough to understand. You take a method
on a remote interface (that is a COM interface that is
implemented on the remote computer), copy each of its
parameters into a buffer, and
send it to the remote computer. On the other end, the remote server
reads each parameter from the buffer, calls the method, writes the
result into another buffer and sends it back.
The tricky part is exactly how to encode those parameters in the buffer,
and how to convert standard stdcall/cdecl method calls to network
packets and back again. This is the job of the RPCRT4.DLL file - or the
Remote Procedure Call Runtime.
The backbone of DCOM is this RPC runtime, which is an implementation
of DCE
RPC. DCE RPC is not naturally object oriented, so this
protocol is extended with some new constructs and by assigning new
meanings to some of the packet fields, to produce ORPC or Object
RPC. You might see it called MS-RPC as well.
RPC packets contain a buffer containing marshalled data in NDR format.
NDR is short for "Network Data Representation" and is similar
to the XDR
format used in SunRPC (the closest native equivalent on Linux to DCE
RPC). NDR/XDR are all based on the idea of graph serialization and were
worked out during the 80s, meaning they are very powerful and can do
things like marshal doubly linked lists and other rather tricky
structures.
In Wine, our DCOM implementation is not
currently based on the
RPC runtime, as while few programs use DCOM even fewer use
RPC directly so it was developed some time after
OLE32/OLEAUT32 were. Eventually this will have to be fixed,
otherwise our DCOM will never be compatible with
Microsoft's. Bear this in mind as you read through the code
however.
Proxies and Stubs
Manually marshalling and unmarshalling each method call using the NDR
APIs (NdrConformantArrayMarshall etc) is very tedious work, so the
Platform SDK ships with a tool called "midl" which is an IDL compiler.
IDL or the "Interface Definition Language" is a tool designed
specifically for describing interfaces in a reasonably language neutral
fashion, though in reality it bears a close resemblence to C++.
By describing the functions you want to expose via RPC in IDL therefore,
it becomes possible to pass this file to MIDL which spits out a huge
amount of C source code. That code defines functions which have the same
prototype as the functions described in your IDL but which internally
take each argument, marshal it using Ndr, send the packet, and unmarshal
the return.
Because this code proxies the code from the client to the server, the
functions are called proxies. Easy, right?
Of course, in the RPC server process at the other end, you need some way
to unmarshal the RPCs, so you have functions also generated by MIDL
which are the inverse of the proxies; they accept an NDR buffer, extract
the parameters, call the real function and then marshal the result back.
They are called stubs, and stand in for the real calling code in the
client process.
The sort of marshalling/unmarshalling code that MIDL spits out can be
seen in dlls/oleaut32/oaidl_p.c - it's not exactly what it would look
like as that file contains DCOM proxies/stubs which are different, but
you get the idea. Proxy functions take the arguments and feed them to
the NDR marshallers (or picklers), invoke an NdrProxySendReceive and
then convert the out parameters and return code. There's a ton of goop
in there for dealing with buffer allocation, exceptions and so on - it's
really ugly code. But, this is the basic concept behind DCE RPC.
Interface Marshalling
Standard NDR only knows about C style function calls - they
can accept and even return structures, but it has no concept
of COM interfaces. Confusingly DCE RPC does have a
concept of RPC interfaces which are just convenient ways to
bundle function calls together into namespaces, but let's
ignore that for now as it just muddies the water. The
primary extension made by Microsoft to NDR then was the
ability to take a COM interface pointer and marshal that
into the NDR stream.
The basic theory of proxies and stubs and IDL is still here, but it's
been modified slightly. Whereas before you could define a bunch of
functions in IDL, now a new "object" keyword has appeared. This tells
MIDL that you're describing a COM interface, and as a result the
proxies/stubs it generates are also COM objects.
That's a very important distinction. When you make a call to a remote
COM object you do it via a proxy object that COM has constructed on the
fly. Likewise, a stub object on the remote end unpacks the RPC packet
and makes the call.
Because this is object-oriented RPC, there are a few complications: for
instance, a call that goes via the same proxies/stubs may end up at a
different object instance, so the RPC runtime keeps track of "this" and
"that" in the RPC packets.
This leads naturally onto the question of how we got those proxy/stub
objects in the first place, and where they came from. You can use the
CoCreateInstanceEx API to activate COM objects on a remote machine, this
works like CoCreateInstance API. Behind the scenes, a lot of stuff is
involved to do this (like IRemoteActivation, IOXIDResolver and so on)
but let's gloss over that for now.
When DCOM creates an object on a remote machine, the DCOM runtime on
that machine activates the object in the usual way (by looking it up in
the registry etc) and then marshals the requested interface back to the
client. Marshalling an interface takes a pointer, and produces a buffer
containing all the information DCOM needs to construct a proxy object in
the client, a stub object in the server and link the two together.
The structure of a marshalled interface pointer is somewhat complex.
Let's ignore that too. The important thing is how COM proxies/stubs are
loaded.
COM Proxy/Stub System
COM proxies are objects that implement both the interfaces needing to be
proxied and also IRpcProxyBuffer. Likewise, COM stubs implement
IRpcStubBuffer and understand how to invoke the methods of the requested
interface.
You may be wondering what the word "buffer" is doing in those interface
names. I'm not sure either, except that a running theme in DCOM is that
interfaces which have nothing to do with buffers have the word Buffer
appended to them, seemingly at random. Ignore it and don't let it
confuse you :) This stuff is convoluted enough ...
The IRpc[Proxy/Stub]Buffer interfaces are used to control the proxy/stub
objects and are one of the many semi-public interfaces used in DCOM.
DCOM is theoretically an internet RFC
[2] and is
specced out, but in reality the only implementation of it apart from
ours is Microsoft's, and as a result there are lots of interfaces
which can be used if you want to customize or
control DCOM but in practice are badly documented or not documented at
all, or exist mostly as interfaces between MIDL generated code and COM
itself. Don't pay too much attention to the MSDN definitions of these
interfaces and APIs.
COM proxies and stubs are like any other normal COM object - they are
registered in the registry, they can be loaded with CoCreateInstance and
so on. They have to be in process (in DLLs) however. They aren't
activated directly by COM however, instead the process goes something
like this:
COM receives a marshalled interface packet, and retrieves the IID of
the marshalled interface from it
COM looks in
HKEY_CLASSES_ROOT/Interface/{whatever-iid}/ProxyStubClsId32
to retrieve the CLSID of another COM object, which
implements IPSFactoryBuffer.
IPSFactoryBuffer has only two methods, CreateProxy and CreateStub. COM
calls whichever is appropriate: CreateStub for the server, CreateProxy
for the client. MIDL will normally provide an implementation of this
object for you in the code it generates.
Once CreateProxy has been called, the resultant object is QueryInterfaced to
IRpcProxyBuffer, which only has 1 method, IRpcProxyBuffer::Connect.
This method only takes one parameter, the IRpcChannelBuffer object which
encapsulates the "RPC Channel" between the client and server.
On the server side, a similar process is performed - the PSFactoryBuffer
is created, CreateStub is called, result is QId to IRpcStubBuffer, and
IRpcStubBuffer::Connect is used to link it to the RPC channel.
RPC Channels
Remember the RPC runtime? Well, that's not just responsible for
marshalling stuff, it also controls the connection and protocols between
the client and server. We can ignore the details of this for now,
suffice it to say that an RPC Channel is a COM object that implements
IRpcChannelBuffer, and it's basically an abstraction of different RPC
methods. For instance, in the case of inter-thread marshalling (not
covered here) the RPC connection code isn't used, only the NDR
marshallers are, so IRpcChannelBuffer in that case isn't actually
implemented by RPCRT4 but rather just by the COM/OLE DLLS.
On this topic, Ove Kaaven says: It depends on the Windows version, I
think. Windows 95 and Windows NT 4 certainly had very different models
when I looked. I'm pretty sure the Windows 98 version of RPCRT4 was
able to dispatch messages directly to individual apartments. I'd be
surprised if some similar functionality was not added to Windows
2000. After all, if an object on machine A wanted to use an object on
machine B in an apartment C, wouldn't it be most efficient if the RPC
system knew about apartments and could dispatch the message directly
to it? And if RPC does know how to efficiently dispatch to apartments,
why should COM duplicate this functionality? There were, however, no
unified way to tell RPC about them across Windows versions, so in that
old patch of mine, I let the COM/OLE dlls do the apartment dispatch,
but even then, the RPC runtime was always involved. After all, it
could be quite tricky to tell whether the call is merely interthread,
without involving the RPC runtime...
RPC channels are constructed on the fly by DCOM as part of the
marshalling process. So, when you make a call on a COM proxy, it goes
like this:
Your code -> COM proxy object -> RPC Channel -> COM stub object -> Their code
How this actually works in Wine
Right now, Wine does not use the NDR marshallers or RPC to implement its
DCOM. When you marshal an interface in Wine, in the server process a
_StubMgrThread thread is started. I haven't gone into the stub manager
here. The important thing is that eventually a _StubReaderThread is
started which accepts marshalled DCOM RPCs, and then passes them to
IRpcStubBuffer::Invoke on the correct stub object which in turn
demarshals the packet and performs the call. The threads started by our
implementation of DCOM are never terminated, they just hang around until
the process dies.
Remember that I said our DCOM doesn't use RPC? Well, you might be
thinking "but we use IRpcStubBuffer like we're supposed to ... isn't
that provided by MIDL which generates code that uses the NDR APIs?". If
so pat yourself on the back, you're still with me. Go get a cup of
coffee.
Typelib Marshaller
In fact, the reason for the PSFactoryBuffer layer of indirection is
because not all interfaces are marshalled using MIDL generated code.
Why not? Well, to understand that
you have to see that one of the
driving forces behind OLE and by extension DCOM was the development of
Visual Basic. Microsoft wanted VB developers to be first class citizens
in the COM world, but things like writing IDL and compiling them with a
C compiler into DLLs wasn't easy enough.
So, type libraries were invented. Actually they were invented as part of
a parallel line of COM development known as "OLE Automation", but let's
not get into that here. Type libraries are basically binary IDL files,
except that despite there being two type library formats neither of them
can fully express everything expressable in IDL. Anyway, with a type
library (which can be embedded as a resource into a DLL) you have
another option beyond compiling MIDL output - you can set the
ProxyStubClsId32 registry entry for your interfaces to the CLSID of the
"type library marshaller" or "universal marshaller". Both terms are
used, but in the Wine source it's called the typelib marshaller.
The type library marshaller constructs proxy and stub objects on the
fly. It does so by having generic marshalling glue which reads the
information from the type libraries, and takes the parameters directly
off the stack. The CreateProxy method actually builds a vtable out of
blocks of assembly stitched together which pass control to _xCall, which
then does the marshalling. You can see all this magic in
dlls/oleaut32/tmarshal.c
In the case of InstallShield, it actually comes with typelibs for all
the interfaces it needs to marshal (fixme: is this right?), but they
actually use a mix of MIDL and typelib marshalling. In order to cover up
for the fact that we don't really use RPC they're all forced to go via
the typelib marshaller - that's what the 1 || hack is for and what the
"Registering non-automation type library!" warning is about (I think).
Apartments
Before a thread can use COM it must enter an apartment. Apartments are
an abstraction of a COM objects thread safety level. There are many types
of apartment but the only two we care about right now are single threaded
apartments (STAs) and the multi-threaded apartment (MTA).
Any given process may contain at most one MTA and potentially many STAs.
This is because all objects in MTAs never care where they are invoked from
and hence can all be treated the same. Since objects in STAs do care, they
cannot be treated the same.
You enter an apartment by calling CoInitializeEx() and
passing the desired thread model in as a parameter. The default if you use
the deprecated CoInitialize() is a STA, and this is the
most common type of apartment used in COM.
An object in the multi-threaded apartment may be accessed concurrently by
multiple threads: eg, it's supposed to be entirely thread safe. It must also
not care about thread-affinity, the object should react the same way no matter
which thread is calling it.
An object inside a STA does not have to be thread safe, and all calls upon it
should come from the same thread - the thread that entered the apartment in
the first place.
The apartment system was originally designed to deal with the disparity between
the Windows NT/C++ world in which threading was given a strong emphasis, and the
Visual Basic world in which threading was barely supported and even if it had
been fully supported most developers would not have used it. Visual Basic code
is not truly multi-threaded, instead if you start a new thread you get an entirely
new VM, with separate sets of global variables. Changes made in one thread do
not reflect in another, which pretty much violates the
expected semantics of multi-threading entirely but this is Visual Basic, so what
did you expect? If you access a VB object concurrently from multiple threads,
behind the scenes each VM runs in a STA and the calls are marshaled between the
threads using DCOM.
In the Windows 2000 release of COM, several new types of apartment were added, the
most important of which are RTAs (the rental threaded apartment) in which concurrent
access are serialised by COM using an apartment-wide lock but thread affinity is
not guaranteed.
Structure of a marshaled interface pointer
When an interface is marshaled using CoMarshalInterface(),
the result is a serialized OBJREF structure. An OBJREF actually contains a union,
but we'll be assuming the variant that embeds a STDOBJREF here which is what's
used by the system provided standard marshaling. A STDOBJREF (standard object
reference) consists of the magic signature 'MEOW', then some flags, then the IID
of the marshaled interface. Quite what MEOW stands for is a mystery, but it's
definitely not "Microsoft Extended Object Wire". Next comes the STDOBJREF flags,
identified by their SORF_ prefix. Most of these are reserved, and their purpose
(if any) is unknown, but a few are defined.
After the SORF flags comes a count of the references represented by this marshaled
interface. Typically this will be 5 in the case of a normal marshal, but may be 0
for table-strong and table-weak marshals (the difference between these is explained below).
The reasoning is this: In the general case, we want to know exactly when an object
is unmarshaled and released, so we can accurately control the lifetime of the stub
object. This is what happens when cPublicRefs is zero. However, in many cases, we
only want to unmarshal an object once. Therefore, if we strengthen the rules to say
when marshaling that we will only unmarshal once, then we no longer have to know when
it is unmarshaled. Therefore, we can give out an arbitrary number of references when
marshaling and basically say "don't call me, except when you die."
The most interesting part of a STDOBJREF is the OXID, OID, IPID triple. This triple
identifies any given marshaled interface pointer in the network. OXIDs are apartment
identifiers, and are supposed to be unique network-wide. How this is guaranteed is
currently unknown: the original algorithm Windows used was something like the current
UNIX time and a local counter.
OXIDs are generated and registered with the OXID resolver by performing local RPCs
to the RPC subsystem (rpcss.exe). In a fully security-patched Windows system they
appear to be randomly generated. This registration is done using the
ILocalOxidResolver interface, however the exact structure of
this interface is currently unknown.
OIDs are object identifiers, and identify a stub manager. The stub manager manages
interface stubs. For each exported COM object there are multiple interfaces and
therefore multiple interface stubs (IRpcStubBuffer implementations).
OIDs are apartment scoped. Each ifstub is identified by an IPID, which identifies
a marshaled interface pointer. IPIDs are apartment scoped.
Unmarshaling one of these streams therefore means setting up a connection to the
object exporter (the apartment holding the marshaled interface pointer) and being
able to send RPCs to the right ifstub. Each apartment has its own RPC endpoint and
calls can be routed to the correct interface pointer by embedding the IPID into the
call using RpcBindingSetObject. IRemUnknown, discussed below, uses a reserved IPID.
Please note that this is true only in the current implementation. The native version
generates an IPID as per any other object and simply notifies the SCM of this IPID.
Both standard and handler marshaled OBJREFs contains an OXID resolver endpoint which
is an RPC string binding in a DUALSTRINGARRAY. This is necessary because an OXID
alone is not enough to contact the host, as it doesn't contain any network address
data. Instead, the combination of the remote OXID resolver RPC endpoint and the OXID
itself are passed to the local OXID resolver. It then returns the apartment string binding.
This step is an optimisation: technically the OBJREF itself could contain the string
binding of the apartment endpoint and the OXID resolver could be bypassed, but by using
this DCOM can optimise out a server round-trip by having the local OXID resolver cache
the query results. The OXID resolver is a service in the RPC subsystem (rpcss.exe) which
implements a raw (non object-oriented) RPC interface called IOXIDResolver.
Despite the identical naming convention this is not a COM interface.
Unmarshaling an interface pointer stream therefore consists of
reading the OXID, OID and IPID from the STDOBJREF, then reading
one or more RPC string bindings for the remote OXID resolver.
Then RpcBindingFromStringBinding is used
to convert this remote string binding into an RPC binding handle
which can be passed to the local
IOXIDResolver::ResolveOxid implementation
along with the OXID. The local OXID resolver consults its list
of same-machine OXIDs, then its cache of remote OXIDs, and if
not found does an RPC to the remote OXID resolver using the
binding handle passed in earlier. The result of the query is
stored for future reference in the cache, and finally the
unmarshaling application gets back the apartment string binding,
the IPID of that apartments IRemUnknown
implementation, and a security hint (let's ignore this for now).
Once the remote apartments string binding has been located the
unmarshalling process constructs an RPC Channel Buffer
implementation with the connection handle and the IPID of the
needed interface, loads and constructs the
IRpcProxyBuffer implementation for that
IID and connects it to the channel. Finally the proxy is passed
back to the application.
Handling IUnknown
There are some subtleties here with respect to IUnknown. IUnknown
itself is never marshaled directly: instead a version of it
optimised for network usage is used. IRemUnknown is similar in
concept to IUnknown except that it allows you to add and release
arbitrary numbers of references at once, and it also allows you to
query for multiple interfaces at once.
IRemUnknown is used for lifecycle management, and for marshaling
new interfaces on an object back to the client. Its definition can
be seen in dcom.idl - basically the IRemUnknown::RemQueryInterface
method takes an IPID and a list of IIDs, then returns STDOBJREFs
of each new marshaled interface pointer.
There is one IRemUnknown implementation per apartment, not per
stub manager as you might expect. This is OK because IPIDs are
apartment not object scoped (In fact, according to the DCOM draft
spec, they are machine-scoped, but this implies apartment-scoped).
Table marshaling
Normally once you have unmarshaled a marshaled interface pointer
that stream is dead, you can't unmarshal it again. Sometimes this
isn't what you want. In this case, table marshaling can be used.
There are two types: strong and weak. In table-strong marshaling,
selected by a specific flag to CoMarshalInterface(),
a stream can be unmarshaled as many times as you like. Even if
all the proxies are released, the marshaled object reference is
still valid. Effectively the stream itself holds a ref on the object.
To release the object entirely so its server can shut down, you
must use CoReleaseMarshalData() on the stream.
In table-weak marshaling the stream can be unmarshaled many times,
however the stream does not hold a ref. If you unmarshal the
stream twice, once those two proxies have been released remote
object will also be released. Attempting to unmarshal the stream
at this point will yield CO_E_DISCONNECTED.
RPC dispatch
Exactly how RPC dispatch occurs depends on whether the exported
object is in a STA or the MTA. If it's in the MTA then all is
simple: the RPC dispatch thread can temporarily enter the MTA,
perform the remote call, and then leave it again. If it's in a
STA things get more complex, because of the requirement that only
one thread can ever access the object.
Instead, when entering a STA a hidden window is created implicitly
by COM, and the user must manually pump the message loop in order
to service incoming RPCs. The RPC dispatch thread performs the
context switch into the STA by sending a message to the apartments
window, which then proceeds to invoke the remote call in the right
thread.
RPC dispatch threads are pooled by the RPC runtime. When an incoming
RPC needs to be serviced, a thread is pulled from the pool and
invokes the call. The main RPC thread then goes back to listening
for new calls. It's quite likely for objects in the MTA to therefore
be servicing more than one call at once.
Message filtering and re-entrancy
When an outgoing call is made from a STA, it's possible that the
remote server will re-enter the client, for instance to perform a
callback. Because of this potential re-entrancy, when waiting for
the reply to an RPC made inside a STA, COM will pump the message loop.
That's because while this thread is blocked, the incoming callback
will be dispatched by a thread from the RPC dispatch pool, so it
must be processing messages.
While COM is pumping the message loop, all incoming messages from
the operating system are filtered through one or more message filters.
These filters are themselves COM objects which can choose to discard,
hold or forward window messages. The default message filter drops all
input messages and forwards the rest. This is so that if the user
chooses a menu option which triggers an RPC, they then cannot choose
that menu option *again* and restart the function from the beginning.
That type of unexpected re-entrancy is extremely difficult to debug,
so it's disallowed.
Unfortunately other window messages are allowed through, meaning that
it's possible your UI will be required to repaint itself during an
outgoing RPC. This makes programming with STAs more complex than it
may appear, as you must be prepared to run all kinds of code any time
an outgoing call is made. In turn this breaks the idea that COM
should abstract object location from the programmer, because an
object that was originally free-threaded and is then run from a STA
could trigger new and untested codepaths in a program.
Wrapup
Theres are still a lot of topics that have not been covered:
Format strings/MOPs
IRemoteActivation
Complex/simple pings, distributed garbage collection
Marshalling IDispatch
ICallFrame
Interface pointer swizzling
Runtime class object registration (CoRegisterClassObject), ROT
Exactly how InstallShield uses DCOM
Further Reading
Most of these documents assume you have knowledge only contained in
other documents. You may have to reread them a few times for it all to
make sense. Don't feel you need to read these to understand DCOM, you
don't, you only need to look at them if you're planning to help
implement it.
http://www-csag.ucsd.edu/individual/achien/cs491-f97/projects/dcom-writeup.ps
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/com/htm/cmi_n2p_459u.asp
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/com/htm/cmi_q2z_5ygi.asp
http://www.microsoft.com/msj/0398/dcom.aspx
http://www.microsoft.com/ntserver/techresources/appserv/COM/DCOM/4_ConnectionMgmt.asp
http://www.idevresource.com/com/library/articles/comonlinux.asp
(unfortunately part 2 of this article does not seem to exist anymore, if it was ever written)