techvanguards.com
Last updated on 9/27/2002

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:

  1. COM runtime initialization
  2. Interface pointer handling
  3. Error handling
  4. Ad hoc interface implementation
  5. 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:

  1. Assigning an interface pointer variable to NIL calls IntfClear, which eventually calls IUnknown.Release.
  2. Assigning an interface pointer to another calls IntfCopy, which eventually calls IUnknown.AddRef to bump up the reference count.
  3. Using the "as" operator calls IntfCast, which eventually calls IUnknown.QueryInterface. In addition, IntfCast will raise an EIntfCastError exception if IUnknown.QueryInterface fails.
  4. 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.

Copyright (c) 1999-2011 Binh Ly. All Rights Reserved.