techvanguards.com
Last updated on 9/27/2002

Building a COM Client Application
by Binh Ly

A COM client application uses the services of a COM server application. If you've previously studied the basics of COM clients and servers, we're now ready to see it in action in Delphi. The following basic steps are performed to build a COM client:

  1. Ensure that the desired COM server is installed and registered on your machine. If you are using remote COM (DCOM), ensure that the server is registered on the remote machine and, if necessary, the server's type library is installed and registered on the client machine.

  2. Import the server's type library into native Delphi interface definitions. This process generates a COM import module that we include into our COM client project to simplify programmatic access to the COM server.

  3. Use the import definitions to access the COM server's functionality

Registering a COM Server

Every COM server needs to be registered before it can be used by a client. The registration process gives the COM runtime information about the server that is later used to activate and use the server. 

There are 2 general types of COM servers, EXEs and DLLs. EXEs are easily registered by running the server with the "/regserver" command line parameter. For example, if a server is named Server.exe, the following command registers it:

Server /regserver

A DLL server is normally registered using a secondary utility. An example of such utility is Microsoft's regsvr32.exe or Borland's tregsvr.exe. Assuming that a server is named Server.dll, the following command registers it:

regsvr32 Server.dll or tregsvr Server.dll

Borland's tregsvr.exe utility can be found under the Bin directory of your Delphi installation. Microsoft's regsvr32.exe utility is usually installed under $WINDOWS\System32 as part of installing a Microsoft development tool such as Visual Studio.

For development convenience, I've created QuickReg, a Windows shell extension, that is used to easily register (and unregister) COM servers. QuickReg eliminates the cumbersome command-line registration steps mentioned earlier.

DLLs may also be registered into the MTS/COM+ catalog. MTS/COM+ is a runtime environment that is used to host transactional COM components. In this case, registration is slightly different. We'll need to first create a package (MTS Package or COM+ Application) using the MTS/COM+ admin utility (Microsoft Management Console under MTS or Component Services Applet under COM+) and we add our DLL server into the package.

We'll discuss building COM components for  MTS/COM+ in a future lesson.

It is important to understand how to verify if a COM server is properly registered. Fortunately, a very simple yet effective utility exists for this purpose: OleView. Download and install the x86 binaries for OleView. Upon successful installation, run the OleView.exe application and browse through the Type Libraries section in its main application window. For instance, under Type Libraries, you'll see "Borland standard VCL type library", which is the type library for the standard VCL COM server that comes with Delphi. If a properly registered COM server has a type library, which most do, it should be listed under the Type Libraries section.

Importing a COM Server

Importing is the process of extracting a COM server's interface into native Delphi constructs. A COM server's interface normally resides in a type library that is bound to the server binary file. The contents of a type library is also called type information. Thus, importing is the process of extracting type information from a type library and translating this information into native Delphi constructs.

Importing can be done either in the Delphi IDE or through the tlibimp.exe command-line utility.

In the IDE, use Project | Import Type Library to import a COM type library. Delphi will scan the registry for registered type libraries and give us a list to pick the desired type library from. Once a type library is selected, use the "Create Unit" option in the "Import Type Library" dialog. 

Under Delphi 5, we can also use  the "Install" option. The "Install" option simply extends the import coclass definitions into a TComponent-derived wrapper that can be dropped onto a Delphi form. This wrapper provides no extra benefits (and in fact, adds some extra overhead when accessing a COM server's functionality) other than it makes it simpler to use COM servers for newbies. The wrapper does, however, simplify handling of events from COM components that expose them. If you do any kind of serious COM development, I recommend you stay away from these wrappers.

After performing the import, Delphi will create a module named <TypeLib>_TLB.pas. <TypeLib> is the programmatic shorthand name for the type library as defined by the vendor who built the COM server. This module can now be included and used in our COM client application.

Using the COM Server Import Module

The COM server import module contains all definitions that enable programmatic access to the COM server. Among others, it contains LIBIDs, CLSIDs, IIDs, interface definitions, enums, records/structs , unions, aliases, and coclass wrappers.

In COM parlance, a coclass defines a creatable COM class.

Take a minute to browse through and familiarize yourself with a COM server import module. Most COM servers will produce interface definitions and coclass wrappers. Interface definitions look like this:

//an interface definition
<InterfaceName> = interface (<BaseInterface>)
    <Interface methods>
end;

Coclass wrappers look like these:

//a coclass wrapper
Co<CoclassName> = class
    class function Create;
    class function CreateRemote;
end;

For example if we import Microsoft Word 2000's type library (Microsoft Word 9.0 Object Library), we get the following interface and coclass definitions, among others:

//_Application interface definition
_Application = interface(IDispatch)
    //_Application IID
    ['{00020970-0000-0000-C000-000000000046}']
    //_Application methods
    function Get_Application: WordApplication; safecall;
    ...
end;

//WordApplication coclass definition
CoWordApplication = class
    class function Create: _Application;
    class function CreateRemote(const MachineName: string): _Application;
end;

This definition means that the Microsoft Word provides a creatable Application component (CoWordApplication) that implements  the _Application interface. The _Application interface supports automation because it derives from IDispatch.

How exactly do we create the Word Application component? 

Simple:

uses
    Word_TLB;  //MS Word import module

procedure TForm1.CreateWordApplicationClick(Sender: TObject);
begin
    //create Word Application component
    //FWordApplication is defined elsewhere as FWordApplication: _Application;
    FWordApplication := CoWordApplication.Create;
end;

What is happening here and how does this relate to what we've learned in the past?

To answer this question, let's dig into CoWordApplication.Create:

class function CoWordApplication.Create: _Application;
begin
    //create Word Application component using its CLSID: CLASS_WordApplication
    //and ask for its _Application interface
    Result := CreateComObject(CLASS_WordApplication) as _Application;
end;

//defined in ComObj
function CreateComObject(const ClassID: TGUID): IUnknown;
begin
    OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
        CLSCTX_LOCAL_SERVER, IUnknown, Result));
end;

CoWordApplication.Create calls CreateComObject and CreateComObject calls CoCreateInstance. CoCreateInstance is a COM API used to create a COM component given the component's CLSID. CoCreateInstance internally triggers launching of the COM server, and reaches into the desired server class factory to obtain an instance of a COM component. 

Therefore, the end result of calling CoWordApplication.Create is that COM will create a new instance of the Word Application component (specified by the CLASS_WordApplication CLSID) and return its _Application interface (specified by the "as _Application" cast) to the caller. 

After receiving the _Application interface pointer, the client can now execute functionality provided by the _Application interface. For instance:

procedure TForm1.CreateWordApplicationClick(Sender: TObject);
begin
    //create Word Application component
    FWordApplication := CoWordApplication.Create;

    //make Word visible
    FWordApplication.Visible := True;

    //create a new/empty document. 
    //EmptyParams mean omit/ignore Document.Add params
    FWordApplication.Documents.Add (EmptyParam, EmptyParam, 
        EmptyParam, EmptyParam);

    //insert some words into the new document
    FWordApplication.Selection.TypeText ('Hello World!!!');
end;

procedure TForm1.QuitWordApplicationClick(Sender: TObject);
var
    SaveChanges: OleVariant;
begin
    //execute quit command. EmptyParams mean omit/ignore Quit params
    SaveChanges := False;
    FWordApplication.Quit (SaveChanges, EmptyParam, EmptyParam);

    //release Word interface pointer
    FWordApplication := nil;
end;

All the above properties/methods are defined in the Word_TLB import module. If you want to learn more about the intricacies of how to make Word do more interesting things, consult the Word VBA reference that comes with your MS Office installation.

Miscellany

COM Registration

When doing COM development, it is sometimes necessary to unregister or reregister a COM server several times. The following illustrates how to unregister a COM server.

For EXEs, execute this command:

Server /unregserver

For DLLs, execute this command

regsvr32 /u Server.dll or tregsvr -u Server.dll

Again, the QuickReg shell extension simplifies these procedures.

Dispinterfaces

Some coclasses do not support vtable interfaces and may possibly support only dispinterfaces. 

A dispinterface is simply a specification for making IDispatch.Invoke calls.

In this case, Delphi will properly import dispinterface definitions into the server's import module. When using dispinterfaces in Delphi, the Delphi compiler will emit the proper code that makes the IDispatch.Invoke call.

Here's an example of how to create the Word Application component and then using its _ApplicationDisp dispinterface:

uses
    Word_TLB;  //MS Word import module

procedure TForm1.CreateWordApplicationClick(Sender: TObject);
begin
    //create Word Application component
    //FWordApplication is defined elsewhere as FWordApplication: _ApplicationDisp;
    FWordApplication := _ApplicationDisp(CoWordApplication.Create as IDispatch);
end;

Late Binding

If a coclass implements an IDispatch-based interface, it usually supports late-binding. Delphi also allows us to perform late-bound calls to COM components. This is made possible through the Delphi Variant/OleVariant data type and some compiler magic.

Here's an example of how to create the Word Application component using late-binding:

uses
    Word_TLB,  //MS Word import module
    ComObj;

procedure TForm1.CreateWordApplicationClick(Sender: TObject);
begin
    //create Word Application component
    //FWordApplication is defined elsewhere as FWordApplication: OleVariant;
    //Using Word Application's ProgID "Word.Application"
    FWordApplication := CreateOleObject ('Word.Application');

    //this is also legal for late-binding
    //FWordApplication is defined elsewhere as FWordApplication: OleVariant;
    FWordApplication := CoWordApplication.Create;
end;

function CreateOleObject(const ClassName: string): IDispatch;
var
    ClassID: TCLSID;
begin
    //convert PROGID to CLSID
    ClassID := ProgIDToClassID(ClassName);
    //instantiate COM component asking for IDispatch
    OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
        CLSCTX_LOCAL_SERVER, IDispatch, Result));
end;

CreateOleObject (ComObj) simply calls CoCreateInstance asking for a component's IDispatch interface. From there on, the IDispatch interface pointer is stored into the FWordApplication variable. Any method calls made through the FWordApplication variable is handled by the Delphi compiler (which internally emits the proper IDispatch.GetIDsOfNames and IDispatch.Invoke calls). 

The Delphi compiler actually emits a call to VarDispInvoke (ComObj) to handle all late-bound calls. VarDispInvoke is where all the IDispatch fun is happening. If you don't like VarDispInvoke's IDispatch implementation, simply replace the global VarDispProc (System) handler.

When using late-binding, we usually have to know what ProgID to use. The ProgID is commonly obtained from documentation that accompanies the COM server. If not, spend a little time spelunking HKCR in the registry to find out this information. Usually a ProgID is found directly under HKCR, which in turn has a CLSID subkey that points to HKCR\CLSID\<Server CLSID>, which has reference to the COM server physical file.

When using late binding, the OleVariant/Variant variable holds an IDispatch pointer to the target COM component. It is possible to extract a vtable interface pointer back from this variable. The following illustrates this mechanism: 

uses
    Word_TLB,  //MS Word import module
    ComObj;

procedure TForm1.CreateWordApplicationClick(Sender: TObject);
var
    WordApplicationVar: OleVariant;
    WordApplication: _Application;
begin
    //create Word Application component
    WordApplicationVar := CreateOleObject ('Word.Application');

    //extract _Application interface from WordApplicationVar OleVariant
    WordApplication := IUnknown (WordApplicationVar) as _Application;
end;

The Safecall Mapping

When Delphi creates the COM server import modules, it normally maps all IDispatch/dual interfaces to something called the safecall calling convention. 

Safecall is a Borland-compiler specific calling convention used simplify COM error handling.

It is COM convention (and good COM programming practice) to have every interface method return an HRESULT code. For example, if we design a COM interface named IEcho that has a method (procedure) named Echo:

IEcho = interface
    procedure Echo;
end;

It is COM convention to redefine the above interface as:

IEcho = interface
    function Echo: HRESULT;
end;

So what is HRESULT for? 

HRESULT provides a way to return status information regarding execution of COM functionality. HRESULTs can be used (by COM) to return error information that have nothing to do with a COM server application, per se, such as network failures, security failures, etc. In addition, an interface method can return customized HRESULT values that is used to classify software errors originating from the server. For more details on this, consult this COM error handling tip.

Because of this convention, a client must test the HRESULT value returned from every COM call to determine if the call was successful or not. For example:

procedure CallFoo;
var
    Foo: IFoo;
    hr: HRESULT;
begin
    Foo := CoFoo.Create;
    hr := Foo.Foo;

    //Failed is a standard function used to test for 
    //failure codes in an HRESULT value 
    if Failed (hr) then 
        raise Exception.Create ('Foo.Foo failed with HRESULT: ' + IntToStr (hr));
    //... do more stuff here ...
end;

As we can see, it can quickly become cumbersome and error prone to implement COM clients with all this HRESULT error checking code around every COM call. 

Delphi provides a convenient function that tests for failure codes in an HRESULT and raises a native Delphi EOleSysError exception. If you need to manually test HRESULTs a lot, use the OleCheck function (ComObj).

In addition, the following are standard functions used for testing HRESULT codes:

  • Succeeded (hr) - tests for a success code in an HRESULT and returns a boolean
  • Failed (hr) - tests for a failure code in an HRESULT and returns a boolean

Enter Safecall.

Using the safecall calling convention, Delphi will import interfaces while abstracting away the HRESULTs and any [out, retval] parameters. For example, an import of this interface:

IFoo = interface (IDispatch)
    //[out, retval] is discussed in a later lesson
    function Foo ([out, retval] Result: WideString): HRESULT; stdcall;
end;

Becomes this, under safecall:

IFoo = interface (IDispatch)
    function Foo: WideString; safecall;
end;

With safecall in place, whenever we make a call to IFoo.Foo (safecall version), the Delphi compiler emits code that calls IFoo.Foo (stdcall version) and in addition, adds some extra code that tests the HRESULT return value. The HRESULT test is done in CheckAutoResult (System) and, if a failed HRESULT is detected, is normally routed through SafeCallError (ComObj). SafeCallError then raises an EOleException that is trappable in our Delphi client code:

procedure CallFoo;
var
    Foo: IFoo;
begin
    try
        //using safecall version
        Foo := CoFoo.Create;
        Foo.Foo;
    except
        on E: EOleException do
            ShowMessage (E.Message);
    end;
end;

It is important to realize that safecall is simply a programmatic convenience and is optional. If you want to turn off the safecall mapping when importing a type library, tweak the Tools | Environment Options | Type Library | Safecall function mapping option.

Using Remote COM Servers/DCOM

If you've noticed carefully, each coclass wrapper code generated by the Delphi import process contains a CreateRemote method. Let's look at the CoWordApplication coclass wrapper again:

CoWordApplication = class
    class function Create: _Application;
    class function CreateRemote(const MachineName: string): _Application;
end;

class function CoWordApplication.CreateRemote(const MachineName: string): _Application;
begin
    Result := CreateRemoteComObject(MachineName, CLASS_WordApplication) as _Application;
end;

CreateRemote calls CreateRemoteComObject (ComObj), which in turn calls the CoCreateInstanceEx API to make a COM request to activate and create a COM component from a remote machine. The end result of this is that a COM client will receive an interface pointer that points to a COM component instance that runs on the remote machine.

Not all COM components are creatable from a remote location. Usually, EXE servers are (and DLLs hosted under MTS/COM+) but they'd have to be first configured for DCOM security access, normally using the DCOMCNFG utility. The details of configuring DCOM security is non-trivial and you can read more about this on MSDN.

What's important here is assuming that a COM server is available and properly configured for remote access, we can use the CreateRemote method to access it through DCOM:

uses
    Word_TLB;  //MS Word import module

procedure TForm1.CreateWordApplicationClick(Sender: TObject);
begin
    //create Word Application component
    //FWordApplication is defined elsewhere as FWordApplication: _Application;
    FWordApplication := CoWordApplication.CreateRemote ('ServerMachineNameOrIPAddress');
end;

Conclusion

To summarize, building COM client applications in Delphi involves 3 simple steps:

  1. Registering the COM server application/type library
  2. Importing the COM server type library
  3. Using the COM server import module
Copyright (c) 1999-2011 Binh Ly. All Rights Reserved.