Tips & Tricks
by Binh Ly
The following is a list of tips and
tricks that might be useful to COM developers of all levels. If you have an
interesting tip that's not listed here, please submit
it to me and I'll add it to the list if I think it's useful.
Index
|
1. Prefer
early binding over late binding
2. Use Connection Points
judiciously
3. Initialize
threads that interact with COM
4. Marshal
interface pointers across apartments
5. Don't
call AddRef and Release unless necessary
6. Implement error
handling correctly
7. Know how to
implement multiple interfaces
8. Know which classes do what
9. Know
the 3 most important things in COM security
10. Get rid
of that nagging DCOM callback problem
11. Understand marshaling
concepts
12. Understand COM identity
concepts
13. Design simple and
efficient interfaces
14. IDispatch,
dispinterfaces, vtable interfaces, dual interfaces, etc.
15. Know
how to implement objects that support Visual Basic's For Each construct
16. Know
how to implement clients that iterate IEnumVARIANT-based collections (ala
Visual Basic's For Each construct)
17. Know how to use
aggregation and containment
18. Understand the
class factory Instancing property (SingleInstance, MultiInstance)
19. Know
how to implement servers that support GetActiveObject
20. Know
how to implement an object property that supports the automation
default-property syntax
|
|
1. Prefer
early binding over late binding
|
Late binding is a facility
that enables script-based client applications to access and
manipulate COM objects. Late binding is based on the concept of
runtime discovery and execution of an object's methods through the COM IDispatch
interface. When you declare and use object variables as variants,
you are using late-binding. Unfortunately, each late bound method
call incurs quite a bit of overhead and may slow down your client
application considerably. To
avoid this, use early (vtable) binding instead.
Assuming that you have a
coclass Foo contained in a FooServer server. The following shows how
to early bind to Foo from your client:
In Delphi
Use the Project | Import
Type Library menu and import FooServer from the list of
registered servers. Delphi generates a unit named FooServer_TLB.pas.
Include that unit into your project and instantiate Foo as follows:
uses FooServer_TLB;
var Foo : IFoo;
begin
Foo := CoFoo.Create;
Foo.Bar; //call Bar method
end;
In C++ Builder
Use the Project | Import
Type Library menu and import FooServer from the list of
registered servers. CBuilder generates a module named
FooServer_TLB.h (and FooServer_TLB.cpp). Include that module into
your project and instantiate Foo as follows:
#include "FooServer_TLB.h"
TCOMIFoo Foo = CoFoo::Create ();
Foo->Bar (); //call Bar method |
2. Use
Connection Points judiciously
|
COM Connection Points is not
the only way to enable server-to-client callbacks. In fact, a simple
interface handed from the client down to the server will do the
trick. However, connection points are widely supported by a lot of
applications (especially Microsoft applications), which might force you to
eventually deal with it one way or another.
Know these when implementing
connection points:
- A lot of connection point
implementations are used to trigger server-to-client dispinterface
callbacks. A dispinterface is simply a specification for
IDispatch.Invoke. Because of this, a server will need to know
how to make an IDispatch.Invoke call and a client will need to
know how to implement IDispatch.Invoke. Implementing
IDispatch.Invoke is not for the faint of heart - trust me! If
you're implementing connection points in your server for
non-scripting clients, forget dispinterfaces and simply
implement vtable-based connection points. That would make it
a lot easier for you to implement both the client and the
server. In addition, a vtable-interface eliminates some of the
overhead involved in dispinterface-based calls.
- When using connection
points, a client that connects to the server will require at
least 3 roundtrip calls:
a) Server.QueryInterface (IConnectionPointContainer),
b) IConnectionPointContainer.FindConnectionPoint (CP), and
c) IConnectionPoint.Advise (IUnknown)
Since IConnectionPoint.Advise takes an IUnknown, the server will
also make at least 1 extra roundtrip QueryInterface call back to the client
(to obtain IDispatch when using dispinterfaces, or
ICustomCallback when using a custom vtable interface)
Also, unless the client caches the connection point interface,
disconnecting from the server will again require at least 3
roundtrip calls:
a) Server.QueryInterface (IConnectionPointContainer),
b) IConnectionPointContainer.FindConnectionPoint (CP), and
c) IConnectionPoint.Unadvise (Cookie)
Therefore, a single client negotiating with the server using
connection points will normally require at least 7 roundtrip
calls between client and server. For in-process (or maybe local
out-of-process) servers, this is normally Ok. However, for
remote servers, this is a little bit too much network traffic
for a single client - what if a lot of clients connect to the
remote server?
In short, use connection points only where appropriate. For
remote callbacks, prefer a hand-coded mechanism of passing a
client's callback interface down to the server through a custom
server interface.
- Connection points are
designed in such a way that it's difficult to distinguish among
the connected clients. It is difficult, if not impossible, to
selectively "filter" certain clients that you may want
to call back depending on certain circumstances. In other words,
if you don't care about filtering or identifying which client is
which, then connection points maybe a good choice for you.
Otherwise, resort to a hand-coded mechanism.
|
3. Initialize
threads that interact with COM
|
Ever get the "CoInitialize has not been
called" (800401F0 hex) error?
Each thread in your
application that interacts with COM (i.e. creates COM objects, calls
COM APIs, etc.) must initialize itself into an apartment. A thread
can either join a single threaded apartment (STA) or the
multithreaded apartment (MTA).
The STA is
system-synchronized based on a windows message queue. Use the STA if
your object or thread relies on thread-relative resources such as UI
elements. The following shows how to initialize a thread into an STA:
procedure FooThreadFunc; //or TFooThread.Execute
begin
CoInitializeEx (NIL, COINIT_APARTMENTTHREADED);
... do your stuff here ...
CoUninitialize;
end;
The MTA is system-guaranteed
to be ruthless. Objects in the MTA will receive incoming calls from
anywhere anytime. Use the MTA for non-UI related objects, but
synchronize carefully! The following shows how to initialize a
thread into the MTA:
procedure FooThreadFunc;
//or TFooThread.Execute
begin
CoInitializeEx (NIL, COINIT_MULTITHREADED);
... do your stuff here ...
CoUninitialize;
end;
|
4. Marshal
interface pointers across apartments
|
Ever get the "The application called an interface that was
marshaled for a different thread" (8001010E hex) error?
When passing an interface
pointer from apartment to apartment, it is a violation of COM's
threading rules if you don't perform marshaling. This is because you
will bypass any necessary requirements that COM might need in order
to successfully make cross-apartment calls. Marshaling interface
pointers involve using CoMarshalInterface and CoUnmarshalInterface.
However for practical purposes, we tend to prefer the easier
CoMarshalInterThreadInterfaceInStream and
CoGetInterfaceAndReleaseStream API pair.
The following shows how to
marshal an interface pointer from Foo1Thread to Foo2Thread, both
from different apartments:
var MarshalStream : pointer;
//original thread
procedure Foo1ThreadFunc; //or TFoo1.Execute
var Foo : IFoo;
begin
//assuming Foo2Thread is currently suspended
CoInitializeEx (...);
Foo := CoFoo.Create;
//marshal
CoMarshalInterThreadInterfaceInStream (IFoo, Foo, IStream (MarshalStream));
//tell Foo2Thread that MarshalStream is ready
Foo2Thread.Resume;
CoUninitialize;
end;
//user thread
procedure Foo2ThreadFunc; //or TFoo2.Execute
var Foo : IFoo;
begin
CoInitializeEx (...);
//unmarshal
CoGetInterfaceAndReleaseStream (IStream (MarshalStream), IFoo,
Foo);
MarshalStream := NIL;
//use Foo
Foo.Bar;
CoUninitialize;
end;
The marshaling technique
shown above is also described as marshal once-unmarshal once. If you
want to marshal once and unmarshal as many times as you wish, use
the COM (NT 4 SP3) provided Global Interface Table (GIT). The GIT
allows you to marshal in interface pointer into a cookie and the
unmarshaling threads can use this cookie to unmarshal how ever many
times they want. Using the GIT, the above example can be written:
const
CLSID_StdGlobalInterfaceTable : TGUID =
'{00000323-0000-0000-C000-000000000046}';
type
IGlobalInterfaceTable = interface(IUnknown)
['{00000146-0000-0000-C000-000000000046}']
function RegisterInterfaceInGlobal (pUnk : IUnknown; const
riid: TIID;
out dwCookie : DWORD): HResult; stdcall;
function RevokeInterfaceFromGlobal (dwCookie: DWORD): HResult; stdcall;
function GetInterfaceFromGlobal (dwCookie: DWORD; const riid: TIID;
out ppv): HResult; stdcall;
end;
function GIT : IGlobalInterfaceTable;
const
cGIT : IGlobalInterfaceTable = NIL;
begin
if (cGIT = NIL) then
OleCheck (CoCreateInstance (CLSID_StdGlobalInterfaceTable, NIL,
CLSCTX_ALL,
IGlobalInterfaceTable, cGIT));
Result := cGIT;
end;
var MarshalCookie : dword;
//original thread
procedure Foo1ThreadFunc; //or TFoo1.Execute
var Foo : IFoo;
begin
//assuming Foo2Thread is currently suspended
CoInitializeEx (...);
Foo := CoFoo.Create;
//marshal
GIT.RegisterInterfaceInGlobal (Foo, IFoo, MarshalCookie)
//tell Foo2Thread that MarshalCookie is ready
Foo2Thread.Resume;
CoUninitialize;
end;
//user thread
procedure Foo2ThreadFunc; //or TFoo2.Execute
var Foo : IFoo;
begin
CoInitializeEx (...);
//unmarshal
GIT.GetInterfaceFromGlobal (MarshalCookie, IFoo, Foo)
//use Foo
Foo.Bar;
CoUninitialize;
end;
And don't forget to remove the interface from the GIT when you not
longer wish to use it:
GIT.RevokeInterfaceFromGlobal
(MarshalCookie);
MarshalCookie := 0;
If you dislike the low-level
GIT gunk, you can use the friendlier TGIP class from my ComLib
library.
|
5. Don't
call AddRef and Release unless necessary
|
With the advent of smart
compilers and smart pointers, explicitly calling IUnknown.AddRef and
IUnknown.Release is a thing of the past.
In Delphi
var Foo, AnotherFoo : IFoo;
Foo := CoFoo.Create;
AnotherFoo := Foo;
The assignment to AnotherFoo
implicitly calls AddRef on the Foo instance, compliments of the
Delphi compiler. Furthermore, when Foo and AnotherFoo go out of
scope (or if you assign NIL to them), Delphi will also implicitly
call Release for you.
In C++ Builder
TCOMIFoo Foo = CoFoo::Create
();
IFooPtr AnotherFoo = Foo;
The assignment to AnotherFoo
implicitly calls AddRef on the Foo instance, compliments of the
TComInterface smart pointer class (note though that a lot of other
assignment operators in TComInterface don't AddRef the source).
Furthermore, when Foo and AnotherFoo go out of scope, TComInterface
will implicitly call Release for you.
|
6. Implement
error handling correctly
|
In COM, every interface method
must return an error code to the client. An error code is a standardized 32-bit value called an HRESULT. The 32 bits in an HRESULT are
actually divided into parts as follows: a bit that indicates success
or failure (severity), a few bits that indicate the classification of
the error (facility), and another few bits that indicate the actual
error number (code). What we're most interested in is how to get the
error number part into the HRESULT. In addition, COM suggests that our
error numbers should be in the range of 0200 hex to FFFF hex.
Unfortunately, an HRESULT is rather limiting because in addition to the error number, we might also
want the server to tell the client what the error is (description),
where it happened (source), and where the client can possibly get more
help on the error (help file and help context). For this, COM
introduces another interface, IErrorInfo, that the client can use to
obtain additional information on an error, if any. In simple terms,
IErrorInfo can be thought of as a storage for the error description,
source, help file, and help context. If the server passes error
information to the client thru IErrorInfo, COM also suggests that the
server implement ISupportErrorInfo. Although this is not required, it
is a good idea to do so because some clients, like Visual Basic, will
ask the server for this interface.
In Delphi
Delphi has something called the
safecall calling convention. What this means is that if you raise an
internal exception within your object's implementation, Delphi will
automatically "trap" the exception and convert it to a COM
HRESULT, and populate an IErrorInfo structure ready for shipping to
the client. This is all done in the HandleSafeCallException function
in ComObj. In addition, the VCL classes also implement
ISupportErrorInfo for you already.
Here's the fun part: When you
raise an EWhatever exception in the server, it will always be seen by
the client as EOleException. EOleException contains all the goodies in
HRESULT and IErrorInfo combined, i.e. the error number, description, source,
help file, and help context. The thing is, in order to set up the
goodies for the client, the server must raise EOleSysError instead of
EWhatever. More specifically, when you raise an EOleSysError, you
should make sure the error number you give it is a conventionally
formatted HRESULT. To see what I mean, let's say we have an object
named FooServer.Foo that has a Bar method. In Bar, we want to raise an
error whose number=5, description="Error Message", help
file="HelpFile.hlp", help context = 1, and the obvious
source="FooServer.Foo". This is how we do it:
uses ComServ;
const
CODE_BASE = $200; //recommended codes are from 0x0200 - 0xFFFF
procedure TFoo.Bar;
begin
//can be assigned once (globally) from somewhere
ComServer.HelpFileName := 'HelpFile.hlp'; //help file
//raise error: message='Error Message', number=5 + CODE_BASE, help context=1
raise EOleSysError.Create (
'Error Message', //error message
ErrorNumberToHResult (5 + CODE_BASE), //HRESULT
1 //help context
);
end;
function ErrorNumberToHResult (ErrorNumber : integer) : HResult;
const
SEVERITY_ERROR = 1;
FACILITY_ITF = 4;
begin
Result := (SEVERITY_ERROR shl 31) or (FACILITY_ITF shl 16) or word
(ErrorNumber);
end;
If you look at the highlighted
line closely, the ErrorNumberToHResult call simply converts our error
number into a standard HRESULT. Also, we add CODE_BASE (200 hex) to
our error number so that we follow COM's suggestion that custom error
numbers be in the range of 0200 hex to FFFF hex. Note that we didn't
specify the source="FooServer.Foo" value. That's because
Delphi "knows" in which object the exception was raised and
will automatically fill that in for you.
On the client side, this is how
we trap the error through EOleException:
const
CODE_BASE = $200; //recommended codes are from 0x0200 - 0xFFFF
procedure CallFooBar;
var
Foo : IFoo;
begin
Foo := CoFoo.Create;
try
Foo.Bar;
except
on E : EOleException do
ShowMessage ('Error message: ' + E.Message + #13 +
'Error number: ' + IntToStr
(HResultToErrorNumber (E.ErrorCode) - CODE_BASE) + #13 +
'Source: ' + E.Source + #13 +
'HelpFile: ' + E.HelpFile + #13 +
'HelpContext: ' + IntToStr (E.HelpContext)
);
end;
end;
function HResultToErrorNumber (hr : HResult) : integer;
begin
Result := (hr and $FFFF);
end;
Again, I've highlighted the
only important line. We call HResultToErrorNumber to extract the error
number part from HRESULT and then we subtract CODE_BASE (200 hex) to
compensate for the conventional error base.
In C++
Builder
CBuilder uses ATL. On the
server, ATL provides the AtlReportError function that
an object can call to return the error number plus the IErrorInfo goodies to
the client. To see what I mean, let's say we have an object
named FooServer.Foo that has a Bar method. In Bar, we want to raise an
error whose number=5, description="Error Message", help
file="HelpFile.hlp", help context = 1, and the obvious
source="FooServer.Foo". This is how we do it:
const
CODE_BASE = 0x200; //recommended codes are from 0x0200 - 0xFFFF
STDMETHODIMP TFooImpl::Bar()
{
return AtlReportError (
GetObjectCLSID (), //which class generated the error
"Error Message", //error description
1, //help context
"HelpFile.hlp", //help file
IID_IFoo, //which interface generated the error
ErrorNumberToHRESULT (5 + CODE_BASE) //HRESULT
);
}
HRESULT ErrorNumberToHRESULT (int ErrorNumber)
{
return MAKE_HRESULT (SEVERITY_ERROR, FACILITY_ITF, ErrorNumber);
} Look at the highlighted
line closely. ErrorNumberToHRESULT converts our error number to a
standard HRESULT. Also, we add CODE_BASE (200 hex) to
our error number so that we follow COM's suggestion that custom error
numbers be in the range of 0200 hex to FFFF hex. Note that we didn't
specify the source="FooServer.Foo" value. That's because ATL
will convert the CLSID that you pass in (first parameter:
GetObjectCLSID ()) to its corresponding PROGID and
will automatically fill that in for you.
If you (normally)
derive from CComCoClass, you can also simply call the overloaded Error
methods in CComCoClass, which eventually call AtlReportError. |
The
second thing we need to do is to implement ISupportErrorInfo. In order
to do that, simply add the simple ATL-provided ISupportErrorInfoImpl
class to TFooImpl: class ATL_NO_VTABLE
TFooImpl :
public CComObjectRoot,
public CComCoClass<TFooImpl, &CLSID_Foo>,
public IDispatchImpl<IFoo, &IID_IFoo, &LIBID_FooServer>,
public ISupportErrorInfoImpl <&IID_IFoo>
{
...
BEGIN_COM_MAP(TFooImpl)
COM_INTERFACE_ENTRY(IFoo)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
...
};
On the client side, we can implement a standard mechanism for
extracting the error number out of the returned HRESULT value and the
extra error information from IErrorInfo. Fortunately, there exists an
EOleException exception class in the VCL that can hold all of this
error information. To do this, I've created a simple function,
CheckResult, that properly extracts all error information, puts them
into an EOleException, and then raises the exception. Here's how you'd
use this function: #include
<comobj.hpp>
const
CODE_BASE = 0x200; //recommended codes are from 0x0200 - 0xFFFF
void CallFooBar ()
{
TCOMIFoo Foo = CoFoo::Create ();
try
{
CheckResult (
Foo->Bar (), //invoke method
which returns the HRESULT
Foo, //specifies the IUnknown* of
the object we're extracting error info from
IID_IFoo //specifies the IID of
the interface that contains the method invoked above
);
}
catch (EOleException& e)
{
ShowMessage ("Error Message: " + e.Message + '\n' +
"Error Number: " + (HRESULTToErrorNumber
(e.ErrorCode) - CODE_BASE) + '\n' +
"Source: " + e.Source + '\n' +
"HelpFile: " + e.HelpFile + '\n' +
"HelpContext: " + e.HelpContext
);
}
}
int HRESULTToErrorNumber (int hr)
{
return HRESULT_CODE (hr);
} Again, I've highlighted the
only important line. We call HRESULTToErrorNumber to extract the error
number part from HRESULT and then we subtract CODE_BASE (200 hex) to
compensate for the conventional error base. Finally,
here's the ugly CheckResult
function that you don't need to mess with: #include
<comobj.hpp>
void CheckResult (
HRESULT hr, //HRESULT returned from method
IUnknown* Object = NULL, //Object in which we invoked the
method
REFIID ErrorIID = GUID_NULL //IID of the interface that
contains the method invoked
)
{
if (FAILED (hr) && (HRESULT_FACILITY (hr) == FACILITY_ITF))
{
bool HasErrorInfo = false;
if (Object && (!IsEqualGUID (ErrorIID,
GUID_NULL)))
{
//check ISupportErrorInfo
ISupportErrorInfo* SupportErrorInfo = NULL;
HRESULT hr = Object->QueryInterface (IID_ISupportErrorInfo, (void**)&SupportErrorInfo);
if (SUCCEEDED (hr))
{
if (SupportErrorInfo->InterfaceSupportsErrorInfo (ErrorIID) ==
S_OK)
HasErrorInfo = true;
SupportErrorInfo->Release ();
}
}
else
//assume caller don't care about ISupportErrorInfo!
HasErrorInfo = true;
if (HasErrorInfo)
{
int ErrorCode = hr;
WideString Description, Source, HelpFile;
ULONG HelpContext = 0;
//get error info
IErrorInfo* ErrorInfo = NULL;
if (SUCCEEDED (GetErrorInfo (0, &ErrorInfo)))
{
ErrorInfo->GetDescription (&Description);
ErrorInfo->GetSource (&Source);
ErrorInfo->GetHelpFile (&HelpFile);
ErrorInfo->GetHelpContext (&HelpContext);
ErrorInfo->Release ();
}
throw EOleException (Description, ErrorCode, Source, HelpFile, HelpContext);
}
}
}
For Visual
Basic
Clients If you're
wondering how this all fits into the Err object in a VB client, here's
how you'd trap the error raised above: Private Sub
CallFooBar()
On Error GoTo ErrorHandler
Dim Foo As New FooServer.Foo
Foo.Bar
Exit Sub
ErrorHandler:
MsgBox "Error Message: " & Err.Description & vbCr & _
"Error Number: " & Err.Number - vbObjectError - &H200 & vbCr & _
"Source: " & Err.Source & vbCr & _
"HelpFile: " & Err.HelpFile & vbCr & _
"HelpContext: " & Err.HelpContext
End Sub On the
highlighted line, subtracting vbObjectError extracts the error number
from the HRESULT, and then subtracting &H200 (200 hex) compensates
for the conventional error
base.
|
7. Know
how to implement multiple interfaces
|
The ability to implement any
interface in any object is one of COM's greatest strengths. Different
development environments provide different means of implementing COM
interfaces. Let's say you have a coclass named FooBar (that already
supports IFooBar) in which you want to implement 2 external
interfaces: IFoo and IBar. IFoo and IBar are defined as follows:
IFoo = interface
procedure Foo; //implicit HRESULT assumed
end;
IBar = interface
procedure Bar; //implicit HRESULT assumed
end;
In Delphi
type
TFooBar = class (TAutoObject, IFooBar, IFoo, IBar)
protected
//IFooBar
... IFooBar methods here ...
//IFoo methods
procedure Foo;
//IBar methods
procedure Bar;
...
end;
procedure TFooBar.Foo;
begin
end;
procedure TFooBar.Bar;
begin
end;
If IFooBar, IFoo, and IBar were
all IDispatch-based, TAutoObject will pick IFooBar (the
primary/default interface) to implement IDispatch, i.e. a script-based
client will only be able to see IFooBar's methods.
In C++
Builder
class ATL_NO_VTABLE TFooBarImpl
:
public CComObjectRoot,
public CComCoClass <TFooBarImpl, &CLSID_FooBar>,
public IFooBar,
public IFoo,
public IBar
{
BEGIN_COM_MAP(TFooBarImpl)
COM_INTERFACE_ENTRY(IFooBar)
COM_INTERFACE_ENTRY(IFoo)
COM_INTERFACE_ENTRY(IBar)
END_COM_MAP()
protected:
//IFoo methods
STDMETHOD (Foo) ();
//IBar methods
STDMETHOD (Bar) ();
}
STDMETHODIMP TFooBarImpl::Foo ()
{
}
STDMETHODIMP TFooBarImpl::Bar ()
{
}
If IFooBar, IFoo, and IBar were
all IDispatch-based, you'd probably want to implement TFooBarImpl this
way:
class ATL_NO_VTABLE TFooBarImpl
:
public CComObjectRoot,
public CComCoClass <TFooBarImpl, &CLSID_FooBar>,
public IDispatchImpl <IFooBar, &IID_IFooBar, &LIBID_FooBarServer>,
public IDispatchImpl <IFoo, &IID_IFoo, &LIBID_FooBarServer>,
public IDispatchImpl <IBar, &IID_IBar, &LIBID_FooBarServer>
{
BEGIN_COM_MAP(TFooBarImpl)
COM_INTERFACE_ENTRY(IFooBar)
COM_INTERFACE_ENTRY_IID(IID_IDispatch, IFooBar)
COM_INTERFACE_ENTRY(IFoo)
COM_INTERFACE_ENTRY(IBar)
END_COM_MAP()
protected:
//... implement
methods here ...
}
Note how I use
COM_INTERFACE_ENTRY_IID () for IDispatch. What this does is whenever
TFooBarImpl is QI'd for IDispatch, it will return the IFooBar part of
it, i.e. a script-based
client will only be able to see IFooBar's methods. This also gets rid
of a compile time error because the compiler cannot resolve which of
the 3 IDispatches (IFooBar, IFoo, or IBar) to use as its IDispatch
implementation.
|
8. Know
which classes do what
|
In Delphi
Delphi provides quite a number
of classes for COM development: TInterfacedObject, TComObject,
TTypedComObject, TAutoObject, TAutoIntfObject, TComObjectFactory,
TTypedComObjectFactory, TAutoObjectFactory, etc. How do we know which
one to use?
Here's the scoop. Ready?!
TInterfacedObject
TInterfacedObject provides you with an
implementation of IUnknown. If you want to create an
"internal" object that implements "internal"
interfaces that (usually) have nothing to do with COM,
TInterfacedObject is the best class to derive from. Of course, you can
still use TInterfacedObject to create an object that can be passed to
a COM client - just remember, the only support you get from it is
IUnknown, nothing more, nothing less.
TComObject
TComObject provides you
with implementations of IUnknown, ISupportErrorInfo, standard COM
aggregation support, and a matching coclass class factory support. If you want to create a lightweight
client-creatable COM object that implements IUnknown-based interfaces,
TComObject is the best class to derive from.
TComObjectFactory
TComObjectFactory works in
tandem with TComObject. It exposes its corresponding TComObject to the
outside world as a coclass. Among the goodies TComObjectFactory brings
are coclass registration (CLSIDs, ThreadingModel, ProgID, etc.),
IClassFactory & IClassFactory2 support, and standard COM object
licensing support. In short, if you have a TComObject, use
TComObjectFactory with it.
TTypedComObject
TTypedComObject is TComObject +
support for IProvideClassInfo. IProvideClassInfo is simply an
automation standard to expose an object's type information (available
names, methods, supported interfaces, etc.) stored in an associated
type library. In addition to TComObject, TTypedComObject is also
useful for objects that want to provide clients with the ability to
browse their type information at runtime. For instance, Visual Basic's TypeName
function expects an object to implement IProvideClassInfo so that it
can determine the object's "documented name" based
on type information stored in the type library.
TTypedComObjectFactory
TTypedComObjectFactory works in
tandem with TTypedComObject. It is TComObjectFactory + it provides a
cached type information (ITypeInfo) reference for TTypedComObject to
use. In short, if you have a TTypedComObject, use
TTypedComObjectFactory with it.
TAutoObject
TAutoObject is TTypedComObject
+ support for IDispatch. TAutoObject's IDispatch support is automatic
and is based on type information stored in the type library - ever
wonder why you never had to implement any of the 4 IDispatch methods
in your automation objects? If you want to create standard
client-creatable automation (supports IDispatch) objects, TAutoObject
is the best class to derive from. In addition, as of D4, TAutoObject
provides a built-in standard connection point mechanism support.
TAutoObjectFactory
TAutoObjectFactory works in
tandem with TAutoObject. It is TTypedComObjectFactory + it provides
cached type information (ITypeInfo) references for your TAutoObject's
default/primary (IDispatch-based) interface and it's connection
point's event interface (if any). In short, if you have a TAutoObject,
use TAutoObjectFactory with it.
TAutoIntfObject
TAutoIntfObject is
TInterfacedObject + support for IDispatch. More specifically
TAutoIntfObject's IDispatch support is type library-based similar to
how TAutoObject does it. In contrast to TAutoObject, TAutoIntfObject
has no corresponding class factory (coclass) support meaning that
external clients cannot directly instantiate a TAutoIntfObject-derived
class. However, TAutoIntfObject is excellent for (IDispatch-based)
sub-level objects or sub-properties (that are themselves objects) that
you want to expose to the clients.
In C++
Builder
C++ Builder uses ATL. Here's a
few important things to know about how your objects are
constructed using ATL: In
ATL, a COM object = Abstract Class + CComObject (or CComObject
variants) The
abstract class is what we're familiar with, you know... the class
with the infamous ATL_NO_VTABLE tag: class
ATL_NO_VTABLE TFooImpl ... TFooImpl
is an abstract class because we cannot directly instantiate TFooImpl
- try it an you'll see. ATL has this interesting concept that your
abstract class must first marry CComObject in order to
exist as one valid COM object: CComObject
<TFooImpl> *Foo;
CComObject <TFooImpl>::CreateInstance (&Foo);
//Foo is now an instance of our TFooImpl class Let's
backtrack a bit and dig deeper into the abstract class. First, we need
to support IUnknown. That's what CComObjectRoot/Ex is for: class
ATL_NO_VTABLE TFooImpl :
public CComObjectRoot/Ex <SomeThreadingModel>, ...
For
details on the differences between CComObjectRoot and CComObjectRootEx,
consult ATL Internals by Brent Rector and Chris Sells |
Next,
we decide if we need coclass support, i.e. do we want external clients
to be able to create our object. That's what CComCoClass is for: class
ATL_NO_VTABLE TFooImpl :
public CComObjectRoot,
public CComCoClass (TFooImpl, &CLSID_Foo), ... CComCoClass
takes care of IClassFactory and aggregation support, registration
gunk, some standard COM error handling stuff, etc. To
complete CComCoClass' coclass support, we also need an entry in
the global OBJECT_MAP structure: BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_Foo, TFooImpl)
END_OBJECT_MAP() Also, CComCoClass automatically does the CComObject <TFooImpl>::CreateInstance
call for us whenever it is asked to create an instance of our TFooImpl
class.
With IUnknown and coclass
support available, we can then implement our custom interfaces. Let's say
we want to implement an interface IFoo declared as follows: IFoo = interface
...
end;
To do this we need to add IFoo to our abstract class, both in the
inheritance chain and the interface map: class ATL_NO_VTABLE
TFooImpl
:
public CComObjectRoot,
public CComCoClass <TFooImpl, &CLSID_Foo>,
public IFoo
{
BEGIN_COM_MAP(TFooBarImpl)
COM_INTERFACE_ENTRY(IFooBar)
COM_INTERFACE_ENTRY(IFoo)
END_COM_MAP()
} The "public IFoo"
part means that we're implementing IFoo in our class. The
COM_INTERFACE_ENTRY (IFoo) part means that clients can get to our IFoo
implementation, i.e. clients can QI for IFoo. But what if we want IFoo
to be IDispatch-based, as it is "normally": IFoo = interface
(IDispatch)
...
end; Well, we can still
use the above class declaration but we'd also have to manually
implement the 4 IDispatch methods. But there's an easier way: define
IFoo in a type library (usually in your server's type library) and
then use ATL's IDispatchImpl class for built-in IDispatch support,
i.e. we don't need to bother with manually implementing the 4
IDispatch methods. Thus: class ATL_NO_VTABLE
TFooImpl
:
public CComObjectRoot,
public CComCoClass <TFooImpl, &CLSID_Foo>,
public IDispatchImpl <IFoo, &IID_IFoo, &LIBID_FooServer>
{
BEGIN_COM_MAP(TFooBarImpl)
COM_INTERFACE_ENTRY(IFooBar)
COM_INTERFACE_ENTRY(IFoo)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
} Note that IDispatchImpl
requires the &IID_IFoo and &LIBID_FooServer parts because it
will implement IDispatch for us based on the IFoo that we have defined
in the type library. Also note that we need an extra
COM_INTERFACE_ENTRY (IDispatch) entry in the interface map so that a
client that QIs for IDispatch will get our default IDispatch
implementation - IFoo. There's
a lot more to ATL than what I've shown you here. If you're up to it,
check out ATL Internals by Brent Rector and Chris Sells or ATL-A
Developer's Guide by Tom Armstrong.
|
9. Know
the 3 most important things in COM security
|
Nothing can be more frustrating
than not knowing why your (DCOM) server denies access to your clients.
In a nutshell, COM security can be described as follows:
Authentication
From the server's point of
view, your first job is to determine whether or not you want to
identify your clients, i.e. do you care who your clients are? This is
called authentication. In simple terms, when the server authenticates
the client, the server asks the client "show me your ID",
the client hands the server its ID, and the server verifies if
indeed the ID is legitimate or not. In COM, the ID is the client's
username and password account. The server's verification process will
involve contacting a domain controller (if any) to determine if the
client's account information is valid or not.
Authentication can be of
different levels ranging from no authentication (server doesn't care
to ask the client for its ID) to paranoid authentication (server will
ensure that the client is identified and in addition, all
communication between the client and server is encrypted to avoid
eavesdroppers from finding out what the client and the server are up
to). The authentication levels that you're probably most familiar with
(assuming you have used DCOMCNFG) are None and Connect. None, as the
name implies, means no authentication. Connect, as the name implies,
means authenticate only the first time the client connects to the
server. There are other levels of authentication but we won't talk
about them in detail here.
The most important thing to
remember about an authentication level is that they are ranked from lowest
authentication (None) to highest authentication (Packet Privacy).
Using this ranking, if a server specifies an authentication level of X,
any clients who try to connect (make calls) to the server at a lower
authentication level than X will automatically be rejected by COM. For
example: you specify an authentication level of Connect on your
server. An unknown client (who has no account on your network) such as
Joe Bum from the Internet tries to contact your server. Since your
server specified Connect, COM will perform authentication. COM won't
be able to identify Joe Bum and so therefore, COM will flag Joe Bum
right at the door with an "Access Denied" message.
Access Control
Being able to identify the
client is only part of the story. Once the server identifies the
client, it will need to decide whether or not the client can access
it. This concept is similar to that big dude at your local bar: you
might be able to produce an ID but he'll let you in only if you're of
legal drinking age. This is called access control.
There's really nothing much to
access control. Your server simply specifies that only users X, Y, and
Z have access to it. All other users, even though they have valid IDs,
are denied access. In DCOMCNFG terms, the "Use custom access
permissions" and "Use custom launch permissions"
options under the security tab is what I mean by access control.
Server Identity
Aside from authentication and
access control, another important thing is that a server should be
running before clients can connect to it. Before the server runs, there's one last thing that it
needs to decide: under which account should it run as? This is
called server identity.
There are 4 main options for a
server's identity:
- Use the account of the user
that's currently logged in to the server machine (Interactive
User)
- Use the account of the
client that connects to the server (Launching User)
- Use a predefined
account designed for use only by the server (This User)
- Use the SYSTEM account
(available only for an NT service COM server)
Opting for Interactive User has
some interesting aspects. First, the server will only have privileges
of the user that's currently logged in to the server machine - and
these privileges vary depending on who that user is. In addition, if
nobody is logged in, your server can't assume as "nobody" so
it will refuse to run. On the bright side, this option is the only one
where you get to see your server's forms (if any) and any message
boxes that your server pops up. Because of this, Interactive User is
excellent for debugging purposes.
Opting for the Launching User
also has some interesting aspects. First, since the server wants to
assume the identity of the client, each client will launch its own
separate copy of the server. This is logical because a single instance
of the server cannot assume the identity of multiple clients all at
the same time. Also, this option is rather crippled in that COM will
deny the server access to any remote/network resources from the server
machine, even including connecting back to the client machine.
Opting for This User is
recommended for production deployment. Using this option, you can
designate a distinct user account only for purposes of your server.
This way, you can easily give or deny permissions to this particular
account depending on what kinds of resources your server needs to
access.
Summary
These 3 aspects are the most
important in understanding COM security. However, my discussions here
are rather incomplete, at best. For more detailed information, consult
Don Box's Essential COM and the following excellent sites:
|
10. Get
rid of that nagging DCOM callback problem
|
If you've ever implemented
callbacks across machines, you might have discovered seemingly odd
and unexplainable errors. I some other cases,
the callback call just doesn't seem to work correctly.
Here's why:
First, read the Authentication
part of Tip #9 above.
When the server makes a call
back into the client, COM will also perform authentication at the
authentication level specified by the client (normally the Default
Authentication level in DCOMCNFG). If authentication fails, COM will
fail the call. Now here's an interesting aspect of the authentication
level. When a client connects to the server, COM will always pick the higher
authentication level between the client and the server as the
negotiated authentication level. This means that calls from the client
to the server and vice-versa will be authenticated at the
negotiated authentication level.
How is this important? Well if
you think that lowering the authentication level on the client side to
None (meaning that you want calls from the server back to the client
to be unauthenticated) fixes our DCOM callback problem, you're only
half right. This is because COM will always look at both the
client and the server to determine the negotiated authentication
level. If the client specifies None but the server specifies something
higher than None, say Connect, COM will use Connect as the negotiated
authentication level. This means that when the server calls back into
the client, COM will authenticate at Connect level, even if the client
didn't really need/want it. Because of this, the authentication must
succeed in order for the call to succeed. If authentication fails
(i.e. the client cannot verify who the server is), the call will fail.
Obviously, one way to solve
this problem is to 1) set both authentication levels for client and
server to None or 2) make sure the server's identity can be
authenticated on the client's domain. For debugging purposes,
lowering the authentication level to None for both client and server
is simply the easiest.
The
authentication level can be set in DCOMCNFG or by calling
the CoInitializeSecurity API. |
Another way is to somehow lower
the authentication level (to None) at runtime, before the server makes
its call back into the client. This can be done using the
IClientSecurity interface or the CoSetProxyBlanket API. I won't show
you the details of how to do this but you can check out Don Box's Essential
COM for a complete discussion.
|
11. Understand
marshaling concepts
|
Ever get the "Interface
not supported/registered" (80004002 hex) error?
Ever wonder why you have to register at least the type library on a
client machine for early binding to work?
COM's concept of location
transparency is made possible through behind-the-scene helper objects
known as proxies and stubs. When a client talks to an object on a
remote machine (or, technically, in another apartment), the client
really talks to the proxy, which talks to COM, which talks to the
stub, which finally talks to the object:

Whenever the client makes a
method call, the proxy packs the method parameters into a flat array
and gives it to COM. COM then transports the array to the stub, which
unpacks the array back to individual method parameters, which then
invokes the method on the server object. This process is called
marshaling.
One of the things you probably
never thought about is where do the proxy and the stub come from? The
simple answer is that proxies and stubs are also COM objects! In fact,
the proxy and stub objects are contained in COM DLLs (also called
proxy-stub DLLs) that get registered on your system.
An interesting thing about this
is that COM provides a built-in proxy-stub DLL that's capable of
creating proxy and stub objects on-the-fly based on information
contained in a type library. This DLL (oleaut32.dll) is called the
type library marshaler or the universal marshaler. Although this
marshaler is good enough for most purposes, it is only capable of
marshaling parameter data that can be represented using the automation
VARIANT data type.
In your type library, you must
annotate your interface definition with the [oleautomation] flag to
specify that you want your interface marshaled using the type library
marshaler. The [oleautomation]
flag can be used on any interface. A lot of COM newbies think
that it is used only for IDispatch-based interfaces. On the
contrary, it is perfectly Ok to annotate IUnknown-based
interfaces with it (of course, as long as your method parameters
are all VARIANT compatible). [oleautomation] simply tells COM to
use the type library marshaler for the associated interface and
has nothing to do with IDispatch or automation. |
Delphi and CBuilder takes
advantage of the type library marshaler and relies heavily on your
type library. If you come from a Visual C++ background, understand
that Delphi and CBuilder does not have the ability to easily create
custom proxy-stub DLLs useful for non-VARIANT parameter data
types.
Since the type library
marshaler relies on information in your type library, it's obvious
that your type library has to be registered on both client and
server machines for it work. If you forget to do this, you'll most
likely get the dreaded "Interface not supported/registered"
error!
Type
library registration is required only if you use early binding.
If you use late binding (i.e. variants or dispinterface
binding), COM uses the IDispatch interface which is already
registered on your system with a well-known proxy-stub DLL.
Therefore, late binding does not require registration of your
type library file. |
|
12. Understand
COM identity concepts
|
The COM specification is very
clear about a few important and elementary concepts of what
constitutes something called the "COM Identity".
Understanding COM identity is very important and is a must for every
COM developer.
First and foremost, COM
mandates that an object's implementation of QueryInterface for
IUnknown, and only IUnknown, must always return the same pointer
value. Consider an object FooBar that implements IFooBar, IFoo, and
IBar:
Foo = FooBar.QueryInterface (IFoo);
Bar = FooBar.QueryInterface (IBar);
Assert (Foo.QueryInterface (IUnknown) = Bar.QueryInterface (IUnknown))
The above assertion must always
hold true! This IUnknown requirement, among other things, enables a
client to compare 2 IUnknown values and definitively tell whether or
not they point to the same server object. Note that this requirement
is only for IUnknown. An object is not required to follow this
requirement for any other interface that it supports.
Second, COM mandates
that QueryInterface must be symmetric, reflexive, and transitive:
Symmetric means that if you got
a pointer from a QueryInterface call for a given IID, then calling
QueryInterface on that pointer using the same IID must
succeed:
Foo = FooBar.QueryInterface (IFoo);
Foo.QueryInterface (IFoo) must succeed
Reflexive means that if you got
a pointer from a QueryInterface call for a given IID, then calling
QueryInterface on that pointer using the IID of the original pointer must
succeed:
Foo = FooBar.QueryInterface (IFoo);
Bar = Foo.QueryInterface (IBar);
Bar.QueryInterface (IFoo) must succeed
Transitive means that if you
get a pointer that's 2 (or more) QueryInterface calls away from the
original pointer, you should be able to get to that pointer
directly from the original pointer:
FooBar = FooBar.QueryInterface
(IFooBar);
Foo = FooBar.QueryInterface (IFoo);
Bar = Foo.QueryInterface (IBar);
FooBar.QueryInterface (IBar) or Bar.QueryInterface (IFooBar) must
succeed
Third, COM mandates that
if a QueryInterface call succeeded (or failed) for a given IID,
calling QueryInterface for that same IID at a later time must
always succeed (or fail):
Foo = FooBar.QueryInterface (IFoo);
If the above call succeeds
(i.e. FooBar supports IFoo), then calling FooBar.QueryInterface (IFoo)
on the same FooBar instance must always succeed. Furthermore, if the
above call fails (i.e. FooBar doesn't support IFoo), then calling
FooBar.QueryInterface (IFoo) on the same FooBar instance must always
fail.
All these requirements are
important for COM to be able to properly implement it's runtime magic
and so that identity (or QueryInterface) semantics are predictable and
consistent. For example, you should not selectively accept (or deny) a
QueryInterface call for an IID based on the client's identity or the
time of the day because your object may never be asked to perform
that QueryInterface again. To illustrate, let's say you need
FooBar to expose IFoo only to Jack but not to Jill.
If Jack calls
FooBar.QueryInterface (IFoo), your object hands Jack its IFoo. If Jill
calls FooBar.QueryInterface (IFoo), your object fails the call with
E_NOINTERFACE. What happens if Jack calls FooBar.QueryInterface (IFoo)
and then hands the pointer to Jill? If, in this process, a
QueryInterface call never happens (most likely),
then Jill will get an IFoo, which is not what you wanted!
The symmetric, reflexive, and
transitive aspects of QueryInterface ensures that a client does not
have to know (or is not required to perform) a particular sequence of
QueryInterface calls to get to a particular interface. What this means
is that if FooBar implements 5 interfaces:
FooBar = class (TFooBar,
IFooBar, IFoo, IBar, IJack, IJill)
Given a FooBar instance, if the
client wants IJill, you must not require the client to go through a
particular sequence of QueryInterface calls to get to IJill, i.e. FooBar.QueryInterface (IJill)
should be all the client needs to call. Imagine if you required the
client to do this to get to IJill:
Foo = FooBar.QueryInterface (IFoo);
Bar = Foo.QueryInterface (IBar);
Jack = Bar.QueryInterface (IJack);
Jill = Jack.QueryInterface (IJill); //finally, my IJill
Not only would that be a burden
on the client, it'd also hard-code "internal knowledge" of
the exact sequence into the client application.
A
note on Delphi
Delphi 4 introduced the implements
keyword that enables us to implement "tear-off"
interfaces that can be used to optimize on resources (in cases
where the client never queries for the tear-off interface). For
instance, consider a FooBar class that implements IBar as a
tear-off:
type
TFooBar = class (TComObject, IFooBar, IBar)
...
public
property BarTearOff : IBar read GetBar implements
IBar;
end;
The above construct has
the advantage that if the client never queries for IBar, the
BarTearOff property will never be invoked and thus, any
resources involved in doing that is never wasted. However,
beware! If the client does this:
FooBar := CoFooBar.Create;
Bar := FooBar as IBar; //calls FooBar.QueryInterface (IBar)
BackToFooBar := Bar as IFooBar; //must succeed because of
QI reflexivity
The last line above looks
reasonably correct and should succeed. But, it will succeed only
if you implement your BarTearOff property/class correctly. In
particular, your BarTearOff class may need to AddRef and Release
TFooBar correctly, and forward QueryInterface calls to TFooBar
appropriately. If you don't do this extra work in BarTearOff,
you'll be violating the laws of COM identity. |
|
13. Design
simple and efficient interfaces
|
Designing interfaces can be
harder than designing object-oriented classes. This is because an
interface is a contract of interoperability between the client and
your object. Once you deploy your objects and interfaces, it will be
extremely difficult, if not impossible, to make changes to your
interfaces.
It's difficult to quantify
what's a simple or what's an efficient interface. However, I'll show
you some practices that can result in complex or inefficient
interfaces. Based on these, you'll know how to avoid them when
designing your own interfaces.
- An interface should consist
of methods that reflect its functionality. If you find yourself in
a situation where you need to publish a method in your object and
feel that the new method does not belong in an existing interface,
then create a new interface, put that method in there, and add
that interface to your object. It's very easy to create a
smorgasbord of (hundreds of) unrelated methods into 1 single
interface but it's extremely hard to maintain it on the object side
and extremely hard to use it on the client side. Nobody likes to use
(or maintain) an interface with 100+ methods!
- Minimize interface
inheritance. Being from an OO world, you might be led to think
that designing layers and layers of inherited interfaces would
impress your colleagues. Consider this simple inheritance chain:
IFoo = interface
procedure Foo;
end;
IBar = interface (IFoo)
procedure Bar;
end;
From an OO standpoint, the advantage of making IBar inherit IFoo
is so that given an IBar, a client can call IBar.Foo. Here's the
problem: Sooner or later, you might need to add additional
functionality to IFoo. If you already deployed IFoo to your
clients, you know that it is bad to go back in and simply change
IFoo (this would easily break existing clients). What you'd
normally do instead is to create a newer IFoo, say IFoo2 (stands
for IFoo version 2):
IFoo2 = interface (IFoo) //inherits all IFoo methods
procedure Foo2;
end;
Since your OO mentality dictates that you want IBar to inherit
IFoo's features, you'll also want to create a new IBar2 that
corresponds to IFoo2 (remember the old IBar inherits from the old
IFoo, which can't be changed):
IBar2 = interface (IFoo2) //inherits all IFoo2 methods
procedure Bar;
end;
Note that since IBar2 can't inherit from both IFoo2 and the old
IBar (COM interface inheritance is based on a single inheritance
chain), we have to retype method Bar (or more specifically, all
IBar methods) into IBar2. If we had 10 IBar methods, we'd have to
manually copy all 10 methods into IBar2. If we later
evolve the interfaces (IFoo3, IFoo4, IBar3, etc.), you can see how
messy this can get.
But let's go back to the problem at hand. You wanted IBar to
inherit from IFoo because all you wanted to do was for the client
to be able to call any of IFoo's methods on an IBar pointer:
Bar = CoBar.Create;
Bar.Foo;
But that's not really necessary. For instance, the client can
simply make a QueryInterface call to get to the IFoo part of Bar
and invoke method Foo achieving the same result (assuming of
course that your object supports both IFoo and IBar):
Bar = CoBar.Create;
Foo = Bar.QueryInterface (IFoo);
Foo.Foo;
Furthermore, this second approach does not and will not require
nested interface inheritance chains. For instance, on the server
side, Bar can simply be implemented as follows:
IFoo = interface
procedure Foo;
end;
IBar = interface //no IFoo inheritance here!
procedure Bar;
end;
Bar = class (TComObject, IFoo, IBar);
And more importantly, as IFoo and IBar evolve, we simply evolve
Bar using a "flat" (instead of hierarchical)
implementation style:
IFoo2 = interface (IFoo)
...
end;
IBar2 = interface (IBar) //no need to inherit IFoo2 here!
...
end;
Bar = class (TComObject, IFoo, IBar, IFoo2, IBar2, ...)
- Be careful when implementing
collection interfaces for remote objects. The classic collection
interface looks something like this:
IItems = interface
property Count : integer;
property Item [Index : integer] : IItem;
end;
Given an IItems pointer, the client can easily iterate the
collection using the Count and Item [] properties like this:
Items = ServerObject.GetItems; //assume GetItems returns
IItems
for i = 1 to Items.Count do
begin
AnItem = Items.Item [i];
DoSomething (AnItem);
end;
This is simply classic collection iteration and, in fact, is very
object-oriented. What you probably don't realize is that if the
server object resides on a remote machine, the above iteration
will require at least Items.Count roundtrip calls across the
network (due to the IItems.Item [] call inside the for-loop). If
there are 100 elements in IItems, that would translate to 100
roundtrips across the wire - obviously not a very efficient
scenario.
What you can do instead is to packet groups of IItems elements
into an array and ship that array in chunks from the server to the
client. This way if 100 items were packed into groups of 50
elements, that would require only 2 roundtrips (2 x 50 = 100) to
bring the entire collection down to the client. In COM, arrays of
data can be sent using COM arrays, a very common example of which
is the automation safearray (or variant array). I won't go into
the details of how to do this but you can check out my DCOM
tutorial to see an implementation of this concept.
- In general, when designing
interfaces for remote objects, the more information you can
transfer in one method call, the more efficient your applications
are. Consider a simple interface with 4 properties:
IFoo = interface
property Foo;
property Bar;
property Jack;
property Jill;
end;
Given a Foo instance, a client will retrieve all 4 properties like
this:
Foo = CoFoo.Create;
FooProperty = Foo.Foo;
BarProperty = Foo.Bar;
JackProperty = Foo.Jack;
JillProperty = Foo.Jill;
That's 4 roundtrip calls across the wire if Foo is a remote
object. Put that in a loop and that could easily magnify as
trouble.
A more efficient approach would be to add a method that retrieves
(or sets) all properties in 1 shot:
IFoo = interface
property Foo;
property Bar;
property Jack;
property Jill;
procedure GetProperties (out Foo; out Bar; out Jack; out
Jill);
end;
The client would now simply make 1 roundtrip call to retrieve all
4 properties:
Foo = CoFoo.Create;
Foo.GetProperties (FooProperty, BarProperty, JackProperty,
JillProperty);
Note that we still might want to keep the 4 individual properties
in the interface in case we need granular control of which
properties to manipulate. However, by adding a GetProperties
method, we can sometimes reduce overhead that may not have been
apparent at the time we originally designed our interface.
|
14. IDispatch,
dispinterfaces, vtable interfaces, dual interfaces, etc.
|
If you, like me, are troubled
by the exact meanings of IDispatch, dispinterfaces, vtable interfaces,
dual interfaces, etc. at one time or another, here's the best I can do
to finally clarify things for you.
IDispatch is a widely
used COM interface. When you use the Delphi/CBuilder File | New |
Automation Object wizard, the IDE will create an automation object
whose interface is IDispatch-based (if you look in the library editor,
your new interface will have IDispatch as its parent interface). What
this means is you are allowing script-based clients to be able to call
methods of your object, nothing more, nothing less.
IDispatch contains 2 methods
that are most useful for script-based clients: GetIDsOfNames and
Invoke. Whenever a client calls a method on your object (through
IDispatch), it will, under-the-hood, call GetIDsOfNames followed by
Invoke, always. For a detailed discussion on this, check out the automation
chapter on my site.
Half of the IDispatch protocol
is invoking a method based on its associated numeric ID (dispid). In
fact, this is precisely what IDispatch.Invoke does. Because of this,
it is perfectly reasonable to create an interface that defines only
the dispid numbers (and method parameter signatures) as the actual
methods of the interface. In theory, a definition of this interface
looks like this:
DispFoo = interface
dispid_1 (param1);
dispid_2 (param1, param2);
dispid_n (param1, param2, param3, ...);
end;
Using this interface, the
client would make method calls directly using dispids:
FooDisp = CoFoo.Create;
FooDisp.dispid_1 (param1); //invoke method whose dispid = 1
FooDisp.dispid_2 (param1, param2); //invoke method whose dispid
= 2
In reality, each of the
dispid_n method calls above really boils down to an IDispatch.Invoke
call. Notice though, that using this new interface, we only make
IDispatch.Invoke calls. We can forget about IDispatch.GetIDsOfNames
simply because we already know the dispids up front. In
COM, this interface is what is called a dispinterface. In
simple terms, a dispinterface is an interface that specifies how to
make IDispatch.Invoke calls.
In contrast to the IDispatch
protocol, a more common way for non-script-based clients to call an
object's methods is through early binding (or vtable binding). Early
binding is based on the concept of low-level stack-based method
invocations very similar to how you invoke internal methods and
procedures within your application. The object's interface in which
you are making early bound method calls into is also known as a vtable
interface. In simple terms, if a client makes an early bound
method call into your object, it is using (one of) your object's
vtable interface.
It is possible for an interface
to be both IDispatch-based and vtable-based. This allows an interface
to be usable to both script-based and non-script-based clients. In
COM, such an interface is called a dual interface (contains a
dispinterface part and a vtable part). In simple terms, given a dual
interface, you can call its methods using either IDispatch (GetIDsOfNames
and Invoke) or early binding.
|
15. Know
how to implement objects that support Visual Basic's For Each
construct
|
If you or your colleagues
develop in Visual Basic, there might be a time where somebody has
asked how you can enable VB's For Each construct to work with your
Delphi or CBuilder COM collection objects.
For Each allows a VB client to
iterate a collection's elements in a standard manner. For example: Dim
Items as Server.IItems //declare variable that holds collection
Dim Item as Server.IItem //declare variable that holds element
Set Items = ServerObject.GetItems //retrieve IItems collection
from server object
//iterate Items in a For Each-loop
For Each Item in Items
Call DoSomething (Item) //do something to each item in
the collection
Next How do we make this
work? The answer lies in a COM interface called IEnumVARIANT: IEnumVARIANT =
interface (IUnknown)
function Next (celt; var rgvar; pceltFetched): HResult;
function Skip (celt): HResult;
function Reset: HResult;
function Clone(out Enum): HResult;
end; For Each is really nothing
but a construct that knows how to call the IEnumVARIANT methods
(particularly Next) to iterate through all elements in the collection.
Although it's really not that difficult to learn the semantics of
IEnumVARIANT, it's often easier to create a high-level reusable class
that encapsulates it because you might find yourself implementing
IEnumVARIANT a lot of times. A
specific mechanism dictates how we expose IEnumVARIANT to the
client. For instance, let's say you have a collection interface that
looks like this: //single
Foo item
IFooItem = interface (IDispatch);
//collection of Foo items
IFooItems = interface (IDispatch)
property Count : integer;
property Item [Index : integer] : IFoo;
end; First, to be able to
use IEnumVARIANT, your collection interface must support automation
(be IDispatch-based) and your individual collection item data type
must be VARIANT compatible (automation compatible). In simple terms,
IFooItems must be IDispatch-based and IFooItem must be VARIANT
compatible (e.g. byte, BSTR, long, IUnknown, IDispatch, etc.). Second,
we go into the type library and add a read-only property named _NewEnum
to the collection interface. _NewEnum must return IUnknown and must
have a dispid = -4 (DISPID_NEWENUM). Applying this to IFooItems: IFooItems
= interface (IDispatch)
property Count : integer;
property Item [Index : integer] : IFoo;
property _NewEnum : IUnknown; dispid -4;
end; Third, we
implement _NewEnum by returning our IEnumVARIANT pointer from that
property. In
Delphi I've created a
reusable class (TEnumVariantCollection in ComLib.pas)
that hides the details of IEnumVARIANT. In order to plug TEnumVariantCollection into your
collection object, you'll need to implement an interface with 3 simple methods: IVariantCollection = interface
//used by enumerator to lock list owner
function GetController : IUnknown; stdcall;
//used by enumerator to determine how many items
function GetCount : integer; stdcall;
//used by enumerator to retrieve items
function GetItems (Index : olevariant) : olevariant; stdcall;
end; To see this in action, let's try it on our
IFooItems interface: type
//Foo items collection
TFooItems = class (TSomeBaseClass, IFooItems, IVariantCollection)
protected
{ IVariantCollection }
function GetController : IUnknown; stdcall;
function GetCount : integer; stdcall;
function GetItems (Index : olevariant) : olevariant; stdcall;
protected
FItems : TInterfaceList; //internal list
of Foo items;
...
end;
function TFooItems.GetController: IUnknown;
begin
//always return Self/collection owner here
Result := Self;
end;
function TFooItems.GetCount: integer;
begin
//always return collection count here
Result := FItems.Count;
end;
function TFooItems.GetItems(Index: olevariant): olevariant;
begin
//always return collection item here
//cast as IDispatch because each Foo item is IDispatch-based
Result := FItems.Items [Index] as IDispatch;
end; Finally, we
implement _NewEnum as follows: function
TFooItems.Get__NewEnum: IUnknown;
begin
//use my TEnumVariantCollection helper class :)
Result := TEnumVariantCollection.Create (Self);
end; That's it! And say
goodbye to that nagging For Each problem! In
C++ Builder ATL
provides a slew of COM enumerator classes for almost any IEnumWhatever
that you can think of. In particular, CComEnum (and CComEnumImpl)
are good enough to produce an IEnumVARIANT enumerator that makes VB
happy. The general idea
when using CComEnum together with IEnumVARIANT is to first produce an
array of VARIANTs, then populate this array with our collection's
items, then pass this array to an instance of CComEnum, and finally
hand out CComEnum's IUnknown to the _NewEnum property. To see this in action, let's try it on our
IFooItems interface: //implementation
of property _NewEnum
STDMETHODIMP
TFooItemsImpl::get__NewEnum (LPUNKNOWN* Value)
{
//create VARIANT array
VARIANT* varray = new VARIANT [ItemCount - 1];
//populate array with each Foo item
for (int i = 0; i < ItemCount; i++)
{
VariantInit (&varray [i]);
VariantCopy (&varray [i], WhereverYouStoreFooItem [i]);
}
//initialize CComEnum
typedef CComEnum <IEnumVARIANT, &IID_IEnumVARIANT,
VARIANT,
_Copy <VARIANT> > MyEnumT;
//create enumerator
CComObject <MyEnumT> *Enum;
CComObject <MyEnumT>::CreateInstance (&Enum);
//initialize our enumerator with our array
Enum->Init (
&varray [0], //collection low bound
&varray [ItemCount], //1 + collection
high bound
GetUnknown (), //collection owner's IUnknown
AtlFlagTakeOwnership //means, Enum will be
responsible for releasing varray
);
//finally return enumerator's IUnknown to _NewEnum
return Enum->QueryInterface (&Value);
} If you've noticed, it
can be a pain in the neck to create a temporary array of VARIANTs
everytime we want to hand out IEnumVARIANT. This is because CComEnum
expects a contiguous array of the exact data type of each element that we're
dealing with (in this case VARIANT). ATL
3.0 alleviates this problem by allowing enumerator's to
"sit" directly on top of a container (especially STL
containers) eliminating the need for the temporary array. However, BCB
4 doesn't support ATL 3.0 yet, so we'll just have to make do with what
we have. That's it! And
say goodbye to that nagging For Each problem!
|
16. Know
how to implement clients that iterate IEnumVARIANT-based collections
(ala Visual Basic's For Each construct)
|
In Visual Basic, there's
something called a For Each construct that allows a client to easily
enumerate an IEnumVARIANT-based collection (see tip above). Assuming
that you have an object, Foo, that has an IEnumVARIANT-based property,
Items, this is how we use VB's For Each syntax to iterate Foo.Items: Dim
Foo as FooServer.Foo
Dim Item as Variant
Set Foo = CreateObject ("FooServer.Foo")
For Each Item in Foo.Items
call DoSomething (Item)
Next In
Delphi Is there an
equivalent to For Each in Delphi? The
answer is there's the hard way and the easy way. The hard way,
obviously, is to familiarize yourself with IEnumVARIANT, specifically
IEnumVARIANT.Reset and IEnumVARIANT.Next. The easy way is to use a
class, TEnumVariant, that I created for such purpose. Since, in
general, it is safe to assume that everybody wants the easy way, I'll
show you how to use TEnumVariant against Foo.Items: uses
ComLib;
var
Foo : IFoo;
Item : olevariant;
Enum : TEnumVariant;
begin
Foo := CreateOleObject ('FooServer.Foo') as IFoo; //or
CoFoo.Create
Enum := TEnumVariant.Create (Foo.Items);
while (Enum.ForEach (Item)) do
DoSomething (Item);
Enum.Free;
end; What could be easier
than this?! In
C++ Builder Is there
an equivalent to For Each in CBuilder? The
answer is there's the hard way and the easy way. The hard way,
obviously, is to familiarize yourself with IEnumVARIANT, specifically
IEnumVARIANT.Reset and IEnumVARIANT.Next. The easy way is to use a
class, TEnumVariant, that I created for such purpose. Since, in
general, it is safe to assume that everybody wants the easy way, I'll
show you how to use TEnumVariant against Foo.Items: {
TCOMIFoo
Foo = CoFoo::Create ();
TEnumVariant Enum (Foo->Items);
Variant Item;
while (Enum.ForEach (Item))
DoSomething (Item);
} And
here's the TEnumVariant class that you don't want to mess with: //supports
IEnumVARIANT for IDispatch-based DISPID_NEWENUM properties
class TEnumVariant
{
public:
TEnumVariant () : mEnum (0)
{
}
TEnumVariant (IDispatch *Collection) : mEnum (0)
{
Attach (Collection);
}
~TEnumVariant ()
{
Detach ();
}
void Attach (IDispatch *Collection)
{
Detach ();
bool ValidEnum = false;
if (Collection)
{
VARIANT Result;
VariantInit (&Result);
DISPPARAMS DispParamsEmpty;
memset (&DispParamsEmpty, 0, sizeof (DispParamsEmpty));
//get prop DISPID_NEWENUM
HRESULT hr = Collection->Invoke (
DISPID_NEWENUM, GUID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_PROPERTYGET, &DispParamsEmpty, &Result, NULL, NULL);
if (SUCCEEDED (hr))
{
//get IEnumVARIANT*
Result.punkVal->QueryInterface (IID_IEnumVARIANT, (void**)&mEnum);
VariantClear (&Result);
Reset ();
ValidEnum = (mEnum != NULL);
}
}
//raise exception if collection does not support IEnumVARIANT
if (!ValidEnum)
throw Exception ("Object does not support enumeration (IEnumVariant)");
}
void Detach ()
{
if (mEnum)
{
mEnum->Release ();
mEnum = NULL;
}
}
void Reset ()
{
if (mEnum) mEnum->Reset ();
}
bool ForEach (Variant &Data)
{
if (!mEnum) return false;
ULONG Fetched = 0;
VARIANT Item;
VariantInit (&Item);
HRESULT hr = mEnum->Next (1, &Item, &Fetched);
if (SUCCEEDED (hr))
{
if (Fetched > 0)
{
Data = Item;
VariantClear (&Item);
}
return (Fetched > 0);
}
else
return false;
}
protected:
IEnumVARIANT *mEnum;
};
|
17. Know
how to use aggregation and containment
|
COM aggregation and containment
are two techniques of reusing existing COM objects while still
preserving the concept of COM identity. To see why you'd want to use
aggregation or containment, consider this simple scenario: You bought
2 COM objects from a vendor, Foo (IFoo) and Bar (IBar). You then want
to create your own object, FooBar, that exposes the facilities of Foo
and Bar combined. In other words, your FooBar class will look
something like this: IFoo
= interface
procedure Foo;
end;
IBar = interface
procedure Bar;
end;
type
FooBar = class (BaseClass,
IFoo, //FooBar exposes IFoo
IBar //FooBar exposes IBar
)
end; What you want
to do is to (re)use Foo when implementing your IFoo methods and to (re)use
Bar when implementing your IBar methods. This is where aggregation and
containment can help. Containment Let's start
with containment first because that's easier. Containment is simply
the process of instantiating an inner object (object to reuse)
and then delegating
method calls into that inner object. This is how we do containment for
IFoo in FooBar: In
Delphi type
TFooBar = class (TComObject, IFoo)
protected
//IFoo methods
procedure Foo;
protected
FInnerFoo : IFoo;
function GetInnerFoo : IFoo;
end;
procedure TFooBar.Foo;
var
Foo : IFoo;
begin
//obtain internal Foo object
Foo := GetInnerFoo;
//delegate call to internal Foo object
Foo.Foo;
end;
function TFooBar.GetInnerFoo : IFoo;
begin
//create internal Foo object if not yet initialized
if (FInnerFoo = NIL) then
FInnerFoo := CreateComObject (Class_Foo) as IFoo;
//return internal Foo object
Result := FInnerFoo;
end; Doing
something like this is not delegation and, thus, is not considered
containment: type
TFooBar = class (TComObject, IFoo)
protected
function GetInnerFoo : IFoo;
property InnerFoo : IFoo read GetInnerFoo
implements IFoo;
end; The
difference between this and the prior class is that in the prior
class, TFooBar is the one exposing IFoo (and internally delegates implementation
method-by-method to InnerFoo). In this class, it is InnerFoo's IFoo that
the client actually sees, so no delegation is happening. In
C++ Builder class ATL_NO_VTABLE
TFooBar :
public CComObjectRoot,
public CComCoClass<TFooBar, &CLSID_FooBar>,
public IFoo
{
protected:
BEGIN_COM_MAP(TFooBar)
COM_INTERFACE_ENTRY(IFoo)
END_COM_MAP()
//IFoo methods
STDMETHOD (Foo) ();
protected:
IFoo *mInnerFoo;
IFoo* GetInnerFoo ();
public:
void FinalRelease ();
end;
STDMETHODIMP TFooBar::Foo ()
{
//obtain internal Foo object
IFoo * Foo = GetInnerFoo ();
//delegate call to internal Foo object
Foo->Foo ();
}
IFoo* TFooBar::GetInnerFoo ()
{
//create internal Foo object if not yet initialized
if (mInnerFoo == NULL)
{
HResult hr = CoCreateInstance (
CLSID_Foo, NULL, CLSCTX_INPROC,
IID_IFoo, (void**)&mInnerFoo);
ErrorCheck (hr);
}
//return internal Foo object
return mInnerFoo;
} void FinalRelease ()
{
//release inner Foo
if (mFoo)
{
mFoo->Release ();
mFoo = NULL;
}
} Note that the
concept of containment is delegation! You simply forward/delegate
all calls from the outer object into the inner object. Aggregation Implementing
containment can be tedious because if the inner object's interface has
a lot of methods, you'll have to do a lot of typing when delegating
from the outer object ("owner" object) to the inner object. In other words, if IFoo has
20 methods, then you'll have to type all 20 methods into TFooBar and
delegate each one of them to InnerFoo. Another thing about containment
is that you have to explicitly know the inner's interface up-front so
that you can delegate properly. This means that if the inner's
interface evolves, you might need to revisit the outer and rebuild it
in case you want to expose new functionality from the inner. These
are some of the reasons why you might want to look at aggregation.
Simply put, aggregation is the mechanism of directly exposing the
inner to the client, while correctly preserving COM identity. The
first rule about aggregation is that you can aggregate an inner object
*only* if it supports aggregation. This means that the inner must know
how to implement the delegating and the non-delegating QIs.
To learn
more about the details on the delegating and non-delegating QIs,
consult Inside COM by Dale Rogerson. |
The second rule of
aggregation is that when the outer constructs the inner, it
should
- Pass in it's (outer)
IUnknown into the inner as part of the CoCreateInstance call, and
- Ask for the inner's IUnknown,
and only IUnknown
In addition, it is recommended
that the outer forwards a QI call to the inner only if the client asks
for the inner's interface (some texts refer to this as planned
aggregation). Assuming that Foo is aggregatable, this is
how we aggregate Foo into TFooBar:
In
Delphi
The QI forwarding for IFoo is
easily implemented using Delphi's implements keyword.
type
TFooBar = class (TComObject, IFoo)
protected
function GetControllingUnknown : IUnknown;
function GetInnerFoo : IFoo;
property InnerFoo : IFoo read GetInnerFoo
implements IFoo; //exposes IFoo directly from InnerFoo
protected
FInnerFoo : IUnknown;
end;
function TFoo.GetControllingUnknown
: IUnknown;
begin
//returns the correct outer unknown for aggregation
if (Controller <> NIL) then
Result := Controller
else
Result := Self as IUnknown;
end;
function TFooBar.GetInnerFoo : IFoo;
begin
//create internal Foo object if not yet initialized
if (FInnerFoo = NIL) then
CoCreateInstance (
CLASS_Foo,
//Foo's CLSID
GetControllingUnknown,
//outer passes it's controlling IUnknown into inner
CLSCTX_INPROC,
//assume Foo is inproc
IUnknown,
//ask for Foo's IUnknown, and only IUnknown
FInnerFoo
//output inner Foo
);
//return internal Foo object
Result := FInnerFoo as IFoo;
end;
When
implementing the inner (aggregatable) object itself, Delphi's
TComObject (root of all COM objects) has the aggregation feature
built-in already. In simple terms, any COM object that
ultimately derives from TComObject is aggregatable (supports
aggregation). |
In C++
Builder You can
simply use ATL's COM_INTERFACE_ENTRY_AGGREGATE macro to aggregate an
inner into the outer's interface map. class ATL_NO_VTABLE
TFooBar :
public CComObjectRoot,
public CComCoClass<TFooBar, &CLSID_FooBar>
{
protected:
BEGIN_COM_MAP(TFooBar)
COM_INTERFACE_ENTRY(IFoo)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IFoo,
mInnerFoo)
END_COM_MAP()
//this is used for GetControllingUnknown!!!
DECLARE_GET_CONTROLLING_UNKNOWN()
protected:
IUnknown *mInnerFoo;
public:
HRESULT FinalConstruct ();
void FinalRelease ();
end;
HRESULT FinalConstruct ()
{
//create inner Foo
HRESULT hr = CoCreateInstance (
CLSID_Foo,
//Foo's CLSID
GetControllingUnknown (), //outer
passes it's controlling IUnknown into inner
CLSCTX_INPROC,
//assume Foo is inproc
IID_IUnknown,
//ask for Foo's IUnknown, and only IUnknown
(void**)&mInnerFoo
//output inner Foo
);
return hr;
} void FinalRelease ()
{
//release inner Foo
if (mFoo)
{
mFoo->Release ();
mFoo = NULL;
}
}
When
implementing the inner (aggregatable) object itself, ATL's
CComCoClass has the aggregation feature
built-in already. This is made possible because of the
DECLARE_AGGREGATABLE () macro in CComCoClass. For a detailed
discussion on ATL aggregation support, consult ATL Internals
by Brent Rector and Chris Sells.
In simple terms, it is
safe to assume that any COM object that
ultimately derives from CComCoClass is aggregatable (supports
aggregation) by default. |
That's it for aggregation. And
don't forget, aggregation only works if the inner is written to
support aggregation. If it's not, then aggregation is the wrong
choice, whereas containment would be a right choice.
|
18. Understand
the class factory Instancing property (SingleInstance,
MultiInstance)
|
A lot of folks get confused
with the class factory Instancing property. This is probably because of reading
incorrect documentation and/or listening to incorrect advise. In fact,
the COM documentation (on MSDN, for instance) is very clear about the
instancing property - and that's where everyone should be reading
about instancing from. Let's translate COM instancing into lay terms.
- The class factory Instancing
property applies *only* to EXE servers. For DLL servers,
Instancing is undefined and inapplicable!
- The class factory Instancing
property is not a property of the EXE server nor the COM object.
It does not dictate, per se, how EXE servers are launched
depending on client requests. So forget about those confusing
"one object per server or multiple objects per server"
rules.
This is what Instancing really
means:
Each object in your server that
a client can create has an associated object called a factory object
(or class
factory). If your server consists of 2 objects, Foo and Bar, there
will be a class factory for Foo and another class factory for Bar.
Whenever the client requests to create an object in your server, COM
will actually ask the associated class factory to create the object.
In effect, the class factory is a gatekeeper for object creation
through COM.
Class factories are registered
with the COM runtime when an EXE server runs (and they are revoked
when the server terminates). Registration allows, among others, COM to
locate and request any given registered class factory to create the
object that the client requests. COM allows each class factory to
register using 3 instancing modes: SingleUse, MultiUse, and
MultiSeparateUse. We'll only talk about SingleUse and MultiUse because
these are the 2 common ones.
SingleUse means that
COM will request the class factory to create *at most 1 instance* of
the it's associated object. After a SingleUse class factory has
created its one instance, COM will revoke it from runtime. Thus, when
the next client comes along and requests to create an object from this
class factory, COM sees that it's no longer registered and will launch
another instance of the EXE server to be able to obtain the class
factory again. Relaunching repeats the process: factories get
registered again, COM finds the factory, requests it to create the
object and then, if it's SingleUse, immediately revokes the factory from runtime.
This cycle just keeps on going and going.
MultiUse, on the
other hand, means that COM will request the class factory to create
*however many instances* of it's associated object. This means
that, unlike SingeUse, COM will not revoke the factory from runtime at
all. Thus, when the next client comes along and requests to create an
object from this class factory, COM will always see that it's still
registered and will happily create the object using the class factory
from within the currently running EXE instance.
In
Delphi
In Delphi terms:
ciSingleInstance = SingleUse
ciMultiInstance = MultiUse
And ciInternal has nothing to
do with COM. ciInternal simply means that your Delphi COM object
doesn't get registered into the registry nor does the class factory
get registered with the COM runtime. In effect, clients won't be able
to see (and create) COM objects marked with the ciInternal factory
instancing flag.
I've always used this
definition of class factory Instancing and I've never been confused!
Ever!
|
19. Know
how to implement servers that support GetActiveObject
|
If you've been working with MS
Office automation, you're probably familiar with the global
"Application" object per server. For instance, MS Word
allows you to connect to it's running Application (_Application
interface) instance as follows: var
Word : variant;
begin
//connect to running instance of Word if available
//GetActiveOleObject will raise an exception if there is no
active instance of Word
Word := GetActiveOleObject ('Word.Application');
end; This facility can
sometimes be useful when developing your own COM servers. Here's how
we can do this type of thing. First,
in your server, you'll need to register an instance of your global Application object with something called the COM
Running Object Table
(ROT). The ROT is nothing but a location where you register named
object instances to be accessible by client applications. We can
easily get our Application object into the ROT using the
RegisterActiveObject API: function
RegisterActiveObject (
unk: IUnknown;
//object to register
const clsid: TCLSID; //CLSID of
object to register
dwFlags: Longint;
//registration option flags, generally use ACTIVEOBJECT_STRONG
out dwRegister: Longint //handle returned by COM on
successful registration
): HResult; stdcall; And,
as you might have already expected, you can later revoke a registered
object from the ROT to make it unavailable to clients. Revoking is done using the
RevokeActiveObject API: function
RevokeActiveObject (
dwRegister: Longint; //registration handle obtained
from calling RegisterActiveObject
pvReserved: Pointer //set to NIL/NULL
): HResult; stdcall; Practically
speaking, registering an object into the ROT means that your server
should not terminate at least until after you revoke it from the ROT.
But the question is, who should (or when should you) revoke the object
from the ROT? The
practical convention seems to be that the object should revoke itself
in response to a Quit (or Exit) command call from the client. This
practice is apparent in the MS Office applications. In other words, in
your global Application object, you'll probably want to expose a Quit method
and in that method, call RevokeActiveObject to remove your global
object from the ROT.
The
actual conventions on when to call RegisterActiveObject and
RevokeActiveObject are documented by Microsoft. For more details
on this, check out the Automation Programmer's Reference.
On a different note, all
this stuff about registration into the ROT is only practical for
EXE servers. For a DLL server, it might be a bit more tricky to
determine when to register/revoke an object from the ROT because
the lifetime of a DLL server is dependent on the client host.
|
Assuming that we want a global
Foo object to be accessible from the ROT, here's how we implement it:
In
Delphi
In your DPR file: begin
Application.Initialize;
RegisterGlobalFoo;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
var
GlobalFooHandle : longint = 0;
procedure RegisterGlobalFoo;
var
GlobalFoo : IFoo;
begin
//create Foo instance
GlobalFoo := CoFoo.Create;
//register into ROT
OleCheck (RegisterActiveObject (
GlobalFoo, //Foo instance
Class_Foo, //Foo's CLSID
ACTIVEOBJECT_STRONG, //strong registration flag
GlobalFooHandle //registration handle result
));
end; Then we add a Quit
method to Foo (IFoo) and revoke the GlobalFoo instance from there: procedure TFoo.Quit;
begin
RevokeGlobalFoo;
end;
procedure RevokeGlobalFoo;
begin
if (GlobalFooHandle <> 0) then
begin
//revoke
OleCheck (RevokeActiveObject (
GlobalFooHandle, //registration handle
NIL //reserved, use NIL
));
//make sure we mark as revoked
GlobalFooHandle := 0;
end;
end; Here's how a Delphi
client locates our GlobalFoo from the ROT using the GetActiveObject
API: var
FooUnk : IUnknown;
Foo : IFoo;
begin
//check if Foo is active
//can also use Delphi's GetActiveOleObject function here if Foo
is an automation object
if (Succeeded (GetActiveObject (
Class_Foo, //Foo's CLSID
NIL, //reserved, use NIL
FooUnk //returned Foo from ROT
)))
then begin
//QI for IFoo
Foo := FooUnk as IFoo;
//...do something with Foo here...
//terminate global Foo, will revoke from the ROT
Foo.Quit;
end;
end;
Delphi
also has a GetActiveOleObject function that accepts a PROGID
instead of the object's CLSID. GetActiveOleObject internally
calls GetActiveObject, and only works for automation (IDispatch-based)
objects. |
In C++
Builder In your
server's main CPP file: WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
RegisterGlobalFoo ();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
static DWORD GlobalFooHandle = 0;
void RegisterGlobalFoo ()
{
//create Foo instance
TCOMIFoo GlobalFoo = CoFoo::Create ();
//register into ROT
HRESULT hr = RegisterActiveObject (
GlobalFoo, //Foo instance
CLSID_Foo, //Foo's CLSID
ACTIVEOBJECT_STRONG, //strong registration flag
&GlobalFooHandle //registration handle result
);
ErrorCheck (hr);
} Then we add a Quit
method to Foo (IFoo) and revoke the GlobalFoo instance from there: STDMETHODIMP TFooImpl::Quit()
{
RevokeGlobalFoo ();
return S_OK;
}
void RevokeGlobalFoo ()
{
if (GlobalFooHandle != 0)
{
//revoke
HRESULT hr = RevokeActiveObject (
GlobalFooHandle, //registration handle
NULL //reserved, use NULL
);
ErrorCheck (hr);
//make sure we mark as revoked
GlobalFooHandle = 0;
}
} Here's how a CBuilder
client locates our GlobalFoo from the ROT using the GetActiveObject
API: {
IUnknown *FooUnk = NULL;
IFoo *Foo = NULL;
//check if Foo is active
HRESULT hr = GetActiveObject (
CLSID_Foo, //Foo's CLSID
NULL, //reserved, use NULL
&FooUnk //returned Foo from ROT
);
if (SUCCEEDED (hr))
{
//QI for IFoo
FooUnk->QueryInterface (IID_IFoo, (void**)&Foo);
FooUnk->Release ();
//...do something with Foo here...
//terminate global Foo, will revoke from the ROT
Foo->Quit ();
Foo->Release ();
}
}
|
20. Know
how to implement an object property that supports the automation
default-property syntax
|
Assuming you created an
automation interface like this: ICollection
= interface (IDispatch)
property Item [Index : variant] : variant;
end; For a client, given
an ICollection pointer, you can get to any item using this syntax: Collection.Item
[Index] You might have
come across other automation-based collections where they let you be
lazy and do something like this instead: Collection
[Index] Allowing clients
(particularly VB clients) this syntax can be very convenient specially
if you have deep levels of hierarchies of collections. To understand
what I mean, compare this: Collection.Item
[Index].SubCollection.Item [Index].SubsubCollection.Item [Index] to this simpler syntax: Collection
[Index].SubCollection [Index].SubsubCollection [Index] Fortunately
for us, automation allows us to easily do this kind of thing. In the
type library, simply mark your Item [] property with a dispid value of 0
(DISPID_VALUE). This means that COM will automatically "hint
at" the Item
[] property as the default property of your collection.
Since
this default property facility is based on dispids, this only
works for automation (IDispatch-based) interfaces. For pure
vtable interfaces, there is no such thing as default properties
- so you can forget about this :). |
|
|