|
Inside the COM Client
by Binh Ly
COM expects quite a few standard
behaviors from its client applications. Delphi simplifies a lot of these
behaviors that we often take for granted. Once in a while, we come across a
problem where it can be necessary to understand what Delphi does and how this
helps us solve the problem. In this lesson, we'll take a look at how Delphi
encapsulates the following COM behaviors:
- COM runtime initialization
- Interface pointer handling
- Error handling
- Ad hoc interface implementation
- Late binding
Initializing the COM Runtime
A client application is
required to initialize the COM runtime before it can interact with COM. This is
done by calling the CoInitialize/Ex API before making any COM calls. The syntax
for CoInitialize/Ex is as follows:
//CoInitialize
CoInitialize (nil);
//CoInitializeEx
//ThreadingModel can be either COINIT_APARTMENTTHREADED or COINIT_MULTITHREADED
CoInitializeEx (nil, ThreadingModel);
CoInitialize is a legacy API and has
been superceded by CoInitializeEx. CoInitializeEx initializes the COM runtime
for the current thread that's making the CoInitializeEx call. Each thread
that wants to interact with COM must call CoInitializeEx, no ifs no buts. The
Flags parameter to CoInitializeEx specifies the threading
model of the client thread, apartment
threading for COINIT_APARTMENTTHREADED and free
threading for COINIT_MULTITHREADED.
| Older versions
of the COM runtime did not support the CoInitializeEx API. This is true of
installations prior to the NT4 or equivalent version of COM. In this case,
calling CoInitialize (or its cousin, OleInitialize) is the only way to
initialize the COM runtime into a thread. For versions of the COM runtime
where CoInitializeEx is supported, calling CoInitialize has the same
effect as calling CoInitializeEx (COINIT_APARTMENTTHREADED).
|
Every successful call to CoInitialize/Ex
must be paired with a call to CoUninitialize, in the same thread, to cleanup the
COM runtime for the current thread. Thus, a pattern for using COM in a standard
EXE application is:
uses
ComObj;
begin
//OleCheck checks for failure HRESULT
OleCheck (CoInitializeEx (nil, ThreadingModel));
//do stuff with COM
//done with COM
CoUninitialize;
end.
In a standard Delphi EXE COM
application, CoInitialize/Ex and CoUninitialize is automatically called from
within the ComObj module. The CoInitialize/Ex process is chained through the
InitProc initialization sequence that gets called from TApplication.Initialize.
Thus, it is important to call Application.Initialize (usually in the DPR file)
as the first statement in an EXE application.
| The effect of
forgetting to call Application.Initialize is usually the nasty "CoInitialize
has not been called" error at the first statement that tries to make
a COM call, or more specifically, the first statement that exports/imports
a COM interface pointer.
On a different note, ComObj calls
CoInitialize/Ex only for EXEs, not for DLLs. A DLL's lifetime and
threading requirements is a subset of its host application. Therefore, it
is the responsibility of the host application to initialize the COM
runtime before calling into a DLL application. Explicitly calling
CoInitialize/Ex in a DLL can result in unpredictable behavior and nasty
runtime failures.
|
The threading model used by
ComObj.CoInitializeEx is specified by the global CoInitFlags variable. Thus, the
following illustrates how initialize the COM free threaded environment:
//DPR
file
uses
ComObj;
begin
//set threading model
CoInitFlags := COINIT_MULTITHREADED;
//let ComObj do the work
Application.Initialize;
...
end.
| By default,
CoInitFlags has an internal value of -1. ComObj translates this to a
CoInitialize (nil) call at startup.
|
For UI-less console applications, we do
not usually make use of the Application variable from the Forms module. Thus, we
cannot call Application.Initialize to jumpstart COM initialization. However, we
can still directly start the InitProc sequence that eventually calls into ComObj:
//DPR
file for console application
uses
ComObj;
var
Echo: IEcho;
begin
//activate InitProc sequence
TProcedure (InitProc);
Echo := CoEcho.Create;
Echo.Echo ('Hello World');
Echo := nil;
end.
| This technique
works only if you link ComObj into your console project. If you don't want
ComObj, manually call CoInitialize/Ex and CoUninitialize directly within
the DPR file.
|
The automatic COM runtime
initialization performed in ComObj is only good for the primary application
thread. If we want to create secondary threads that interact with COM, we apply
this pattern to explicitly initialize the COM runtime:
//thread
handler routine
procedure TFooThread.Execute;
begin
OleCheck (CoInitialize (nil)); //or CoInitializEx
UseCOM;
ReleaseCOMInterfacePointers;
CoUninitialize;
end.
It is important to release all local
interface pointers obtained from within a thread before calling CoUnintialize.
Failure to do may cause unexpected behavior from the COM runtime. For example,
the following code is a violation of this rule:
//thread
handler routine
procedure TFooThread.Execute;
var
Echo: IEcho;
begin
OleCheck (CoInitialize (nil)); //or CoInitializEx
Echo := CoEcho.Create;
Echo.Echo ('Hello World');
//must release Echo pointer at this point!!!
//uncomment next line for correctness
//Echo := nil; <- calls IEcho.Release
CoUninitialize;
end.
It is also illegal to obtain an
interface pointer from a thread and use it from another thread, unless
marshaling is performed. For instance, the following code is a violation of this
rule:
//thread
handler routine
procedure TFooThread.Execute;
begin
OleCheck (CoInitialize (nil)); //or CoInitializEx
//FEcho is a global variable declared elsewhere
FEcho := CoEcho.Create;
CoUninitialize;
end.
//in another thread
procedure SayHello;
begin
//FEcho acquired from TFooThread and incorrectly used from
this thread
FEcho.Echo ('Hello World');
end;
| Marshaling is a
non-trivial topic and will be discussed in a future lesson.
|
Working with Interface Pointers
Consider the canonical Echo coclass
from our previous lesson:
interface
IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] BSTR Message );
};
coclass Echo
{
[default] interface IEcho;
};
Again, here's that client code that
creates Echo and uses its IEcho interface:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
Echo.Echo ('Hello World');
end;
From our knowledge of the concepts
behind reference counting and IUnknown,
we should expect to see AddRef and Release calls all over the place. The above
code clearly indicates not a single shred of IUnknown-ness when using the IEcho
interface. So what's the story here?
If we unmask the code generated by the
Delphi compiler, the above code actually looks more like this:
//code in
bold is generated by the compiler at the asm level
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
//standard init
Echo := nil;
Echo := CoEcho.Create;
Echo.Echo ('Hello World');
//standard cleanup
IntfClear (Echo);
end;
procedure IntfClear (var p: Pointer);
begin
if p <> nil then
begin
IUnknown (p).Release;
p := nil;
end;
end;
IntfClear is called to clear interface
pointers using IUnknown.Release. The following example illustrates the rest of
the IUnknown behavior:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo, AnotherEcho: IEcho;
Echo2: IEcho2;
begin
Echo := CoEcho.Create;
AnotherEcho := Echo;
Echo2 := Echo as IEcho2;
Echo := nil;
end;
And here's the equivalent
compiler-generated pseudocode:
//code in
bold is generated by the compiler at the asm level
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo, AnotherEcho: IEcho;
begin
//standard init
Echo := nil;
AnotherEcho := nil
Echo := CoEcho.Create;
//AnotherEcho := Echo;
IntfCopy (AnotherEcho, Echo)
//Echo2 := Echo as IEcho2;
IntfCast (Echo2, Echo, IID_IEcho2);
//Echo := nil;
IntfClear (Echo);
//standard cleanup
IntfClear (Echo);
IntfClear (AnotherEcho);
end;
procedure IntfCopy (var Dest: Pointer; const Source: Pointer);
var
OldDest: Pointer;
begin
OldDest := Dest;
Dest := Source;
if Dest <> nil then IUnknown (Dest).AddRef;
//Release old Dest
if OldDest <> nil then IUnknown (OldDest).Release;
end;
procedure IntfCast (var Dest: Pointer; const Source: Pointer; const IID:
TGUID);
var
hr: HRESULT;
begin
if Dest <> nil then IUnknown (Dest).Release;
if Source <> nil then
begin
hr := Source.QueryInterface (IID,
Dest);
if hr <> S_OK then
raise
EIntfCastError.Create ('Interface not supported');
end;
end;
IntfCopy duplicates interface
pointers (while doing the necessary AddRefs) and IntfCast queries for
interface pointers using IUnknown.QueryInterface.
If you're quick to notice:
- Assigning an interface pointer
variable to NIL calls IntfClear, which eventually calls IUnknown.Release.
- Assigning an interface pointer to
another calls IntfCopy, which eventually calls IUnknown.AddRef to bump up
the reference count.
- Using the "as" operator
calls IntfCast, which eventually calls IUnknown.QueryInterface. In addition,
IntfCast will raise an EIntfCastError exception if IUnknown.QueryInterface
fails.
- The compiler always generates code
to initialize (NIL out) interface pointers at the start of scope and release
interface pointers (calling IntfClear) and the end of scope in every
function.
| IntfCast has a
small problem. Notice that the Dest pointer is Release'd before
QueryInterface is called. Thus, if Dest is the same as Source, the QI call can potentially fail, specifically if the Source's
refcount was 1coming into IntfCast. The following code illustrates this
bug:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
Echo := Echo as IEcho; //whoops, access
violation!!!
end;
This bug is reproducible in
Delphi 5.
|
Of course, we can still explicitly use
(and abuse) IUnknown anytime we want. For instance:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
PEcho: Pointer;
begin
Echo := CoEcho.Create;
//explicitly call IUnknown methods
Echo._AddRef;
Echo._Release;
//copy raw Echo pointer value without AddRef
PEcho := Pointer (Echo);
//call AddRef on pointer copy
IUnknown (PEcho)._AddRef;
//call Release on pointer copy
IUnknown (PEcho)._Release;
end;
Note that Delphi has redefined
IUnknown.AddRef as IUnknown._AddRef and IUnknown.Release as IUnknown._Release.
These are simply name redefinitions and have no impact on the semantics of
IUnknown.
Another interesting Delphi nicety that
we take for granted is the automatic compiler aliasing of interface names to
IIDs. For example, IUnknown.QueryInterface requires that we pass in an IID (GUID)
value but Delphi allows us to specify interface names for simplicity:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
Echo2: IEcho2;
begin
Echo := CoEcho.Create;
//using "as" operator
Echo2 := Echo as IEcho2; //IEcho2's IID is IID_IEcho2
//using IUnknown.QueryInterface
Echo.QueryInterface (IEcho2, Echo2); //IEcho2's IID is
IID_IEcho2
//this is also OK and is equivalent to the above
Echo.QueryInterface (IID_IEcho2, Echo2); //IEcho2's IID
is IID_IEcho2
end;
Despite the fact that Delphi gives us
these IUnknown niceties, it is still possible to bypass the compiler code and
incorrectly use IUnknown. For instance:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
Echo._Release; //wrong! where's the corresponding
AddRef?
end;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
Echo._AddRef; //wrong! where's the corresponding
Release?
end;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
Echo2: IEcho2;
begin
Echo := CoEcho.Create;
Echo2 := IEcho2 (Echo); //wrong! we need QueryInterface
called!
end;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
Pointer (Echo) := nil; //wrong! bypasses
IUnknown.Release!
end;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
PEcho: Pointer;
begin
Echo := CoEcho.Create;
PEcho := Pointer (Echo); //wrong! bypasses
IUnknown.AddRef!
end;
It is important to note that the
"as" operator will always raise an exception on failure. Sometimes, we
may need to test if a given COM component supports an interface without wanting
to be bothered with an exception. To do this, simply call
IUnknown.QueryInterface explicitly:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
Echo2: IEcho2;
begin
Echo := CoEcho.Create;
if (Echo.QueryInterface (IEcho2, Echo2) = S_OK) then
ShowMessage ('Echo2 rules!!!')
else
ShowMessage ('Ooops, Echo2 is
AWOL!');
end;
When designing native functions that accept
interface pointers, it is good programming practice to always use the Delphi
const attribute. This allows to compiler to optimize out unnecessary calls to
IUnknown.AddRef and Release. For instance, the following is good programming
practice:
procedure UseEcho (const Echo: IEcho);
begin
Echo.Echo ('Hello World');
end;
In addition to native interface types,
the Delphi compiler also automatically handles IUnknown management for interface
pointers contained in records and arrays. For example:
type
TEcho = Record
Echo: IEcho;
end;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: TEcho;
EchoArray: array [1..1] of IEcho;
begin
Echo.Echo := CoEcho.Create;
EchoArray [1] := CoEcho.Create;
end;
Translates to this compiled pseudocode:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: TEcho;
EchoArray: array [1..1] of IEcho;
begin
InitializeRecord (Echo);
InitializeArray (EchoArray);
Echo.Echo := CoEcho.Create;
EchoArray [1] := CoEcho.Create;
FinalizeRecord (Echo);
FinalizeArray (EchoArray);
end;
procedure InitializeRecord (const Rec)
begin
RecursiveInitializeRecordFields;
end;
procedure FinalizeRecord (const Rec)
begin
RecursiveFinalizeRecordFields;
end;
procedure InitializeArray (const Arr)
begin
RecursiveInitializeArrayElements;
end;
procedure FinalizeArray (const Arr)
begin
RecursiveFinalizeArrayElements;
end;
InitializeRecord, FinalizeRecord,
InitializeArray, and FinalizeArray recursively inspect all fields/elements
searching for interface pointers and apply the same basic IUnknown rules
discussed earlier.
Error Handling
If you've studied the previous lessons,
you should already be familiar with HRESULTs and the safecall calling
convention. Again, safecall is a Borland-specific calling convention that
simplifies COM error handling. Consider our IEcho interface without safecall:
interface
IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] BSTR Message );
};
//stdcall mapping
IEcho = interface (IDispatch)
function Echo (const Message: WideString): HRESULT; stdcall;
end;
Using the above IEcho interface
requires checking for the HRESULT return value when invoking any of its methods:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
hr: HRESULT;
begin
Echo := CoEcho.Create;
hr := Echo.Echo ('Hello World');
if Failed (hr) then
raise Exception.Create ('Cannot
Echo!!!');
end;
Since checking HRESULT codes is
cumbersome, Delphi provides a convenience function, OleCheck:
uses
ComObj;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
OleCheck (Echo.Echo ('Hello World'));
end;
//defined in ComObj
procedure OleCheck (hr: HRESULT);
begin
if Failed (hr) then raise EOleSysError.Create ('', hr, 0);
end;
| It is good COM
programming practice to test HRESULT codes immediately after making calls
that return HRESULTs. This applies to most COM APIs as well as interfaces
not mapped using safecall. OleCheck is a convenient generic function that
tests HRESULT codes, extracts the corresponding COM error description, and
raises a native Delphi EOleSysError exception.
|
COM defines an infrastructure for
components to pass on detailed error information in addition to HRESULT codes.
This is done through the IErrorInfo interface. The gist of using IErrorInfo is
that a COM component fills out an IErrorInfo with detailed error information
which COM will then pass on to the client. The client then calls the
GetErrorInfo API to extract the detailed error information from IErrorInfo. The
mechanics of this is discussed in detail in this error
handling tip.
OleCheck does not extract detailed
error information contained in IErrorInfo. OleCheck is simply a function used to
test standard COM HRESULTs, not custom errors. Because of this, it may be
useful, in addition to the simple test done by OleCheck, to manually extract the
IErrorInfo information by explicitly calling GetErrorInfo. Thus, we can define a
better version of OleCheck as follows:
procedure
OleCheck2 (hr: HRESULT);
var
ErrorInfo: IErrorInfo;
Source, Description, HelpFile: WideString;
HelpContext: Longint;
begin
if Failed (hr) then
begin
HelpContext := 0;
//is IErrorInfo available?
if GetErrorInfo (0, ErrorInfo) = S_OK then
begin
//dig into
IErrorInfo
ErrorInfo.GetSource (Source);
ErrorInfo.GetDescription (Description);
ErrorInfo.GetHelpFile (HelpFile);
ErrorInfo.GetHelpContext (HelpContext);
end;
//raise error to caller
raise EOleException.Create
(Description, hr, Source,
HelpFile, HelpContext);
end;
end;
Using OleCheck2 is as simple as
substituting it where OleCheck is called:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
OleCheck2 (Echo.Echo ('Hello World'));
end;
Since OleCheck (and OleCheck2) raises a
native Delphi exception, standard exception handling logic can be used to trap
COM errors:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
try
OleCheck2 ( Echo.Echo ('Hello
World'));
except
on E: EOleException do
ShowMessage
('Cannot Echo. Techie error is ' + E.Message + #13 +
'HRESULT is ' + IntToStr (E.ErrorCode));
end;
end;
Realizing the convenience of
OleCheck2's concept, Delphi goes a step further and incorporates this concept
into the safecall calling convention. Under safecall, importing IEcho yields the
following:
//safecall
mapping
IEcho = interface (IDispatch)
procedure Echo (const Message: WideString): safecall;
end;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
Echo.Echo ('Hello World');
end;
Notice that HRESULT and IErrorInfo
testing are completely gone. Well, not quite. Here's the actual compiler-generated
pseudocode:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
hr := Echo.Echo ('Hello World');
CheckAutoResult (hr);
end;
procedure CheckAutoResult (hr: HRESULT);
begin
if Failed (hr) then SafeCallError (hr);
end;
procedure SafeCallError (hr: HRESULT);
begin
//same logic as OleCheck2 above
end;
As we can see, safecall is simply our
good old OleCheck2 concept in disguise, after all.
| SafeCallError is
actually a replaceable function. If you want your own SafeCallError
version, simply plug it in to the global SafeCallErrorProc (System)
variable.
|
Not surprisingly, trapping for errors
under safecall is similar to trapping errors under OleCheck/OleCheck2:
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
Echo := CoEcho.Create;
try
Echo.Echo ('Hello World');
except
on E: EOleException do
ShowMessage
('Cannot Echo. Techie error is ' + E.Message + #13 +
'HRESULT is ' + IntToStr (E.ErrorCode));
end;
end;
Thus, if we desire to obtain the
HRESULT code returned from a COM call, we can either use the stdcall calling
convention or we can use the safecall calling convention and error trap for
EOleException and extract its ErrorCode property.
Implementing Ad hoc Interfaces
Once in a while, it may be necessary to
implement a COM interface from within a client application. The most common
reason for doing this is to hook up a callback interface that gets passed from
the client to the server so that the server can make calls to the interface
methods, thus communicating with the client. COM events make extensive use of
this concept.
Consider the following vtable
interface:
[
uuid(50CD06F0-F3A2-4583-94D5-383D9AA38614)
]
interface IEchoCallback: IUnknown
{
HRESULT _stdcall BeforeEcho( void );
HRESULT _stdcall AfterEcho( void );
};
//safecall mapped
IEchoCallback = interface(IUnknown)
['{50CD06F0-F3A2-4583-94D5-383D9AA38614}']
procedure BeforeEcho; safecall;
procedure AfterEcho; safecall;
end;
Implementing IEchoCallback in a Delphi
client application simply requires implementing it into an appropriate class.
For instance, the following is a trivial implementation of IEchoCallback:
TEchoCallback
= class (TObject, IUnknown, IEchoCallback)
private
FRefCount: integer;
//IUnknown methods
function QueryInterface(const IID: TGUID; out Obj): HResult;
stdcall;
begin
//GetInterface is a TObject method
used to locate interface
//vtable offsets
if GetInterface(IID, Obj) then
Result :=
S_OK
else
Result := E_NOINTERFACE;
end;
function _AddRef: Integer; stdcall;
begin
Result :=
InterlockedIncrement(FRefCount);
end;
function _Release: Integer; stdcall;
begin
Result := InterlockedDecrement(FRefCount);
if Result = 0 then Free;
end;
//IEchoCallback methods
procedure BeforeEcho; safecall;
procedure AfterEcho; safecall;
end;
Note that we also have to implement
IUnknown to ensure COM correctness of TEchoCallback's identity. Assuming that
our IEcho interface has an extra method that enables us to pass in an
IEchoCallback pointer:
interface IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] BSTR Message );
HRESULT _stdcall TalkToMe( [in] IEchoCallback* Callback );
};
Here's how we pass the above
IEchoCallback implementation down to IEcho:
procedure TForm1.PassCallbackClick(Sender:
TObject);
var
Echo: IEcho:
Callback: IEchoCallback;
begin
//create IEchoCallback implementation
Callback := TEchoCallback.Create;
Echo := CoEcho.Create;
Echo.TalkToMe (Callback);
end;
It is important that the Callback
variable be of type IEchoCallback instead of TEchoCallback. This is because
TEchoCallback has a proper implementation of IUnknown's reference counting
semantics. Consider what would happen if Callback was declared as TEchoCallback:
procedure TForm1.PassCallbackClick(Sender:
TObject);
var
Echo: IEcho:
Callback: TEchoCallback;
begin
Callback := TEchoCallback.Create;
//at this point, Callback's refcount = 0
Echo := CoEcho.Create;
Echo.TalkToMe (Callback);
//at this point, Callback's refcount = 1
//because Echo will hold a reference to it
Echo := nil;
//at this point, Callback's refcount = 0
//because Echo will have called ICallback.Release
//in addition, Callback is already freed as a result of it's
//IUnknown.Release implementation
//attempt to use Callback
Callback.Free; //Ooops!!! not good!
end;
Since TEchoCallback's
IUnknown implementation is useful for interface implementers, Delphi
encapsulates this logic into the TInterfacedObject class. Here is TEchoCallback
again, this time using TInterfacedObject to hide the IUnknown details:
//TInterfacedObject
handles IUnknown
TEchoCallback = class
(TInterfacedObject, IEchoCallback)
private
procedure BeforeEcho; safecall;
procedure AfterEcho; safecall;
end;
Sometimes, it may be inconvenient
to manually create a TInterfacedObject-derived class just to implement a COM
interface. For instance, we may want to directly implement an interface from a
UI component such as a form (TForm descendant). Fortunately for us, Delphi
already provides a default no-operation implementation of IUnknown for
TComponent - the root of most UI components. Thus, a custom form may trivially
implement an interface:
//TComponent
is TForm's ancestor, thus we get IUnknown for free
TMyForm = class
(TForm, IUnknown, IEchoCallback)
private
procedure BeforeEcho; safecall;
procedure AfterEcho; safecall;
end;
Note that we
still need to explicitly specify IUnknown in our TComponent-derived class
because TComponent does not explicitly implement IUnknown - it merely
implements IUnknown's 3 methods. This is important in case the receiver of
IEchoCallback wants to perform an explicit QI for IUnknown.
|
TComponent's implementation of IUnknown
is simply a no-operation. This is because we explicitly control the lifetime of
UI components (by eventually calling Free) and we don't really want a true
implementation of IUnknown's reference counting semantics. The following
pseudocode illustrates TComponent's IUnknown:
TComponent = class
(TPersistent)
private
//IUnknown
function TComponent.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterface (IID, Obj) then
Result := S_OK
else
Result := E_NOINTERFACE
end;
function TComponent._AddRef: Integer;
begin
Result := -1 // nop/dummy refcount
end;
function TComponent._Release: Integer;
begin
Result := -1 // nop/dummy refcount
end;
end;
Another type of interface that may be
implemented by a Delphi client is a dual
interface. A dual interface consists of 2 parts: a vtable part and an
IDispatch part. We've just learned how to implement a vtable interface from the
above. As for the IDispatch part, we simply implement the 4 methods of the
IDispatch interface:
IDispatch = interface(IUnknown)
['{00020400-0000-0000-C000-000000000046}']
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer;
out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult; stdcall;
end;
Although implementing IDispatch is
non-trivial (consult the Automation Programmer's Reference for more details),
dual interfaces are likely to be defined in a type library. The COM runtime has
the ability to generically compose an IDispatch implementation based on type
information about a dual interface. For instance, ITypeInfo defines an Invoke
method that can be used to implement IDispatch.Invoke:
//ITypeInfo
is used to browse COM type information
ITypeInfo = interface(IUnknown)
['{00020401-0000-0000-C000-000000000046}']
...
function Invoke(pvInstance: Pointer; memid: TMemberID; flags: Word;
var dispParams: TDispParams; varResult: PVariant;
excepInfo: PExcepInfo; argErr: PInteger): HResult;
stdcall;
...
end;
If all this sounds like mumbo jumbo to
you, Delphi provides a class, TAutoIntfObject, that encapsulates this COM
functionality. Thus, assuming that we have a dual IEchoCallback interface
defined as follows:
[
uuid(50CD06F0-F3A2-4583-94D5-383D9AA38614),
dual,
oleautomation
]
interface IEchoCallback: IDispatch
{
HRESULT _stdcall BeforeEcho( void );
HRESULT _stdcall AfterEcho( void );
};
//safecall mapped
IEchoCallback = interface(IDispatch)
['{50CD06F0-F3A2-4583-94D5-383D9AA38614}']
procedure BeforeEcho; safecall;
procedure AfterEcho; safecall;
end;
Here's a TAutoIntfObject-based
implementation of IEchoCallback:
uses
ActiveX;
TEchoCallback = class (TAutoIntfObject, IEchoCallback)
private
procedure BeforeEcho; safecall;
procedure AfterEcho; safecall;
public
constructor Create;
var
TypeLib: ITypeLib;
begin
//load type type library that
contains IEchoCallback
OleCheck (LoadTypeLib (PWideChar (WideString('TypeLibFile.tlb')),
TypeLib));
//tell TAutoIntfObject to use
IEchoCallback's type information
//for IDispatch
inherited Create (TypeLib, IEchoCallback);
end;
end;
Since TAutoIntfObject's IDispatch
implementation is based on type information, it is necessary to first point
TAutoIntfObject to the desired type information as evidenced in the above
constructor.
And when it comes time to hand out our
IEchoCallback implementation, we simply do the usual:
procedure TForm1.PassCallbackClick(Sender:
TObject);
var
Echo: IEcho:
Callback: IEchoCallback;
begin
//create IEchoCallback implementation
Callback := TEchoCallback.Create;
Echo := CoEcho.Create;
Echo.TalkToMe (Callback);
end;
Again, note that Callback is of type
IEchoCallback, not TEchoCallback. TAutoIntfObject derives from TInterfacedObject
and we've previously studied its underlying reference counting rules.
The last type of interface that we may
also need to implement in a client application is a dispinterface.
Implementing a dispinterface is nothing but implementing IDispatch.Invoke. This
is notoriously common with COM connection
point-based event interfaces.
Here's IEchoCallback again, in
dispinterface form:
[
uuid(BC41869F-0179-45B7-82E4-52C9C7C51109)
]
dispinterface IEchoCallback
{
methods:
[id(0x00000001)]
HRESULT BeforeEcho( void );
[id(0x00000002)]
HRESULT AfterEcho( void );
};
As we can see, we have BeforeEcho with
a dispid = 1, and AfterEcho with a dispid = 2. Here's our IEchoCallback
IDispatch.Invoke implementation based on TInterfacedObject:
TEchoCallback = class (TInterfacedObject, IUnknown, IDispatch)
private
//IUnknown
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
begin
//if asking for IEchoCallback, give them our IDispatch
if IsEqualGUID (IID, IEchoCallback) then
Result := inherited QueryInterface (IDispatch, Obj)
else
Result := inherited QueryInterface
(IID, Obj);
end;
//IDispatch
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult; stdcall;
begin
case DispID of
1: ShowMessage ('Before Echo');
2: ShowMessage ('After Echo');
end;
Result := S_OK;
end;
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
begin
Result := E_NOTIMPL;
end;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
begin
Result := E_NOTIMPL;
end;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
begin
Result := E_NOTIMPL;
end;
end;
Since IEchoCallback is an
IDispatch.Invoke specification, we reimplement QI to simply hand out our
IDispatch when asked for IEchoCallback. As for Invoke, we simply case out the
dispid and execute the appropriate logic. As for the rest of the IDispatch
methods, we simply return E_NOTIMPL to indicate that they aren't implemented;
remember, a dispinterface is simply an IDispatch.Invoke specification.
Of course not all dispinterfaces are as
simple as IEchoCallback. Some will have methods with lots of parameters and
you'll have fun decoding each parameter from the Params (TDispParams structure)
parameter in IDispatch.Invoke. If you, like me, have no time to do such things,
download my EventSinkImp utility and
let it build IDispatch.Invoke for you.
The mechanics
of implementing IDispatch is fully discussed in the Automation
Programmer's Reference. If you do any kind of serious development with
IDispatch, type information, and automation in general, I strongly suggest
getting a copy of this book.
|
One final interesting note: Delphi
provides the ability to realias an interface method name at design time. For
instance, consider the following 2 interfaces:
interface IEchoCallback: IUnknown
{
HRESULT _stdcall BeforeEcho( void );
};
interface IEchoCallbackCousin: IUnknown
{
HRESULT _stdcall BeforeEcho( [in] BSTR Message );
};
If we want to implement IEchoCallback
and IEchoCallbackCousin into 1 class, we'd have a name clash on the 2 distinct
BeforeEcho methods:
TEchoCallback
= class (TInterfacedObject, IEchoCallback, IEchoCallbackCousin)
private
//IEchoCallback
procedure BeforeEcho; safecall;
//IEchoCallbackCousin
procedure BeforeEcho (const Message: WideString);
safecall;
end;
Obviously, the above can't work. Thanks
to realiasing, we can do the following:
TEchoCallback
= class (TInterfacedObject, IEchoCallback, IEchoCallbackCousin)
private
//aliases
procedure IEchoCallback.BeforeEcho = IEchoCallbackBeforeEcho;
procedure IEchoCallbackCousin.BeforeEcho =
IEchoCallbackCousinBeforeEcho;
//implementations
procedure IEchoCallbackBeforeEcho; safecall;
procedure IEchoCallbackCousinBeforeEcho (const Message:
WideString); safecall;
end;
Realiasing is a compile-time feature. It
has nothing to do with the runtime semantics of interface implementation.
Late binding
The Delphi compiler implements
impressive late binding techniques. To fully understand how Delphi does late
binding, lets look at our beloved IEcho interface again:
interface
IEcho: IDispatch
{
[id(0x00000001)]
HRESULT _stdcall Echo( [in] BSTR Message );
};
coclass Echo
{
[default] interface IEcho;
};
IEcho derives from IDispatch which
means that it supports automation,
otherwise called late
binding. We've previously looked at
late binding and the Delphi OleVariant/Variant data type. Here's some sample
code again that illustrates late bound usage of IEcho:
uses
ComObj;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: OleVariant;
begin
//assume Echo's ProgID is "EchoServer.Echo"
Echo := CreateOleObject ('EchoServer.Echo');
Echo.Echo ('Hello World');
end;
OleVariant is
Delphi's native type for COM's VARIANT data type. Variant is Delphi's
native polymorphic data type. When doing COM programming and late-binding,
I suggest using OleVariant instead of Variant. Although the internal
storage structure of OleVariant and Variant are the same, the compiler
manipulates the two differently.
|
And here's the compiler generated
pseudocode:
//code in
bold is generated by the compiler at the asm level
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: OleVariant;
Dispatch: IDispatch;
Params: Pointer;
begin
//standard init
VarInit (Echo)
//Echo := CreateOleObject ('EchoServer.Echo');
Dispatch := CreateOleObject ('EchoServer.Echo');
VarFromDisp (Echo, Dispatch);
//Echo.Echo ('Hello World');
Params := BuildInvokeParams ({method} 'Echo', {params}
'Hello World');
VarDispInvoke (Echo, Params);
//standard cleanup
IntfClear (Dispatch);
VarClr (Echo);
end;
//reset a VARIANT
procedure VarInit (var V: OleVariant);
begin
//zero out OleVariant memory
FillChar (V, sizeof (V), 0);
end;
//copy an IDispatch into a VARIANT
procedure VarFromDisp (var V: OleVariant; const Dispatch: Pointer);
begin
VarClr (V);
//setup VARIANT's IDispatch pointer
tagVariant (V).vt := VT_DISPATCH;
tagVariant (V).dispVal := Dispatch;
//bump up refcount
if tagVariant (V).dispVal <> nil then
IUnknown (tagVariant (V).dispVal).AddRef;
end;
function BuildInvokeParams: Pointer;
begin
Result := Allocate memory for Invoke params
Copy method names, param names, and param values into Result
end;
//execute a late bound call
procedure VarDispInvoke (const V: OleVariant; Params: Pointer);
var
Dispatch: IDispatch;
DispID: integer;
begin
if not HoldsIDispatch (V) then
raise EOleError.Create ('Variant is
not an Automation object');
Dispatch := IDispatch (tagVariant (V).dispVal);
OleCheck (Dispatch.GetIDsOfNames (GetMethodName (Params),
DispID));
OleCheck (Dispatch.Invoke (DispID, BuildParams (Params));
end;
//free and clear a VARIANT
procedure VarClr (var V: OleVariant);
begin
//VariantClear is a COM API used to free VARIANT's
VariantClear (V);
end;
Whew! Isn't that something?! Of course,
I oversimplified a lot of the compiler code. For instance, VarInit is not really
a function - the compiler will zero out OleVariant storage in-place using the
appropriate asm instructions. BuildInvokeParams does not really exist - the
compiler will push the invoke parameters in-place onto the stack where the late bound call is
made. Finally, VarDispInvoke is actually more complex in real life.
Anyway, the bottom line here is that
the Echo variable holds an IDispatch pointer obtained from CreateOleObject. Whenever a
method is called on it, the call is actually routed through VarDispInvoke (ComObj),
which performs the IDispatch.GetIDsOfNames and IDispatch.Invoke business.
The compiler
does not directly call VarDispInvoke. VarDispInvoke is merely the default
late binding dispatch handler. If you write your own handler, substitute
it into the VarDispProc (System) global variable.
|
Note that as long as an OleVariant
contains a valid IDispatch pointer, we can make late bound method calls through
it. In addition, the compiler also intrinsically supports a named-argument
parameter syntax for late binding. For instance:
uses
ComObj;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
EchoVar: OleVariant;
begin
Echo := CoEcho.Create;
//assign IDispatch to EchoVar
EchoVar := Echo as IDispatch;
EchoVar.Echo ('Hello World');
//do named-arguments
EchoVar.Echo (Message := 'Hello World');
end;
In addition to the above late binding
mechanics, the Delphi compiler also natively supports dispinterface binding. For
instance, consider the following dispinterface:
dispinterface IEchoDisp
{
methods:
[id(0x00000001)]
HRESULT Echo( [in] BSTR Message );
};
IEchoDisp = dispinterface
procedure Echo(const Message: WideString); dispid 1;
end;
And the following client code:
uses
ComObj;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEchoDisp;
begin
//assume Echo's ProgID is "EchoServer.Echo"
Echo := IEchoDisp (CreateOleObject ('EchoServer.Echo') as
IDispatch);
Echo.Echo ('Hello World');
end;
Note that the
IEchoDisp hard-cast above is legal. A dispinterface is not really an
interface - it is merely an IDispatch.Invoke specification, thus the QI
for IDispatch.
|
And here's the compiler-generated
pseudocode:
//code in
bold is generated by the compiler at the asm level
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEchoDisp;
Params: Pointer;
begin
//standard init
IntfClear (Echo)
Echo := IEchoDisp (CreateOleObject ('EchoServer.Echo') as
IDispatch);
// Echo.Echo ('Hello World');
Params := BuildInvokeParams ({dispid} 1, {params} 'Hello
World');
DispCallByID (IDispatch (Echo), Params);
//standard cleanup
IntfClear (Echo);
end;
function BuildInvokeParams: Pointer;
begin
Result := Allocate memory for Invoke params
Copy dispids and param values into Result
end;
procedure DispCallByID (const Dispatch: IDispatch; Params: Pointer);
var
DispID: integer;
begin
OleCheck (Dispatch.Invoke (GetDispID (Params), BuildParams (Params));
end;
As we can see, all dispinterface calls
eventually end up calling IDispatch.Invoke in DispCallByID. Again, I've
oversimplified DispCallByID so if you want the real deal, browse into the ComObj
source code.
The compiler
does not directly call DispCallByID. DispCallByID is merely the default
dispinterface dispatch handler. If you write your own handler, substitute
it into the DispCallByIDProc (System) global variable.
|
Conclusion
This lesson has shown us a lot of the
most important innards of Delphi COM client applications. This knowledge can be
used when debugging problems, implementing advanced solutions, and more
importantly, getting a better grasp on how Delphi does COM.
|