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.
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
Use the
Project | Import Type Library menu and import FooServer from the list of
registered servers.
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
var Foo,
AnotherFoo : IFoo;
Foo :=
CoFoo.Create;
AnotherFoo := Foo;
The
assignment to AnotherFoo implicitly calls AddRef on the Foo instance,
compliments of the
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
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
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
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
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:
DCOM 95
Frequently Asked Questions
COM
Security Frequently Asked Questions
COM
Internet Services
Using
Distributed COM with Firewalls
Dan Miser's
DCOM FAQ
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.
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
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
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
Is there an
equivalent to For Each in
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
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
The QI
forwarding for IFoo is easily implemented using
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,
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
In
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
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
var
FooUnk : IUnknown;
Foo : IFoo;
begin
//check if Foo is active
//can also use
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;
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 :).