|
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:
- 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.
- 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.
- 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:
- Registering the COM server
application/type library
- Importing the COM server type library
- Using the COM server import module
|