|
Building COM Components
by Binh Ly
A COM component is a self-contained
class definition and implementation of a specific functionality that is
accessible through COM. Some texts refer to this as COM objects or COM classes.
For our discussion purposes, we'll call them COM components. We've previously
studied how to build COM servers and the different types of COM components.
Designing and building COM components is probably the most difficult skill to
master in COM. Fortunately, Delphi is one of the best IDEs around
that can help us make COM development as painless as possible.
The most important skills necessary to
build COM components are:
- Understanding IDL
- Implementing interfaces
- Understanding COM error handling,
HRESULTs, and safecall
- Understanding type libraries
IDL
It is important to understand the
concept of interfaces
before doing any kind of COM development. COM interfaces are defined using a
Microsoft standard: Microsoft Interface Definition Language or IDL in short. IDL
specifies the syntax for defining COM interfaces, data types, structures, etc.
The Delphi Type Library Editor
(TLE)
helps a lot when designing COM interfaces. The
TLE allows us to work with interfaces in both IDL and native Pascal. The
desired choice is available by tweaking the Tools | Environment Options | Type
Library | Language setting. COM beginners usually like the native Pascal
mappings instead of IDL. However, I strongly recommend that IDL be used
when doing any serious COM development. The reason for this is twofold:
-
IDL is COM's native language.
A good COM developer must know IDL because this knowledge is helpful
in other areas of COM.
-
Knowledge of IDL helps
tremendously in conveying COM designs that is understood by other COM
developers, specially those that do COM development in other
environments such as MS Visual C++.
|
Data Types
The following summarizes some of the
most common COM/Automation data types available to us:
| IDL
Data Type |
Delphi
native type |
Description |
| BSTR |
WideString |
string |
| CURRENCY |
Currency |
8 byte fixed point |
| DATE |
TDateTime |
8 byte float |
| DECIMAL |
TDecimal |
12 byte float structure with sign and scale |
| double |
Double |
8 byte float |
| IDispatch* |
IDispatch |
IDispatch pointer |
| IUnknown* |
IUnknown |
IUnknown pointer |
| long |
Integer |
4 byte number |
| SAFEARRAY |
PSafeArray |
Automation/COM array |
| short |
SmallInt |
2 byte number |
| single |
Single |
4 byte float |
| VARIANT |
OleVariant |
Automation/COM
variant |
| VARIANT_BOOL |
WordBool |
Automation/COM
boolean |
| COM interfaces built using Delphi are
limited to the automation data types. This is because the Delphi compiler does
not natively support marshaling other than the default COM type library
marshaling implementation when using the IDL [oleautomation] tag.
|
In addition to the above native types,
the TLE also allows us to construct custom types such as enums (enumerated
type), structs
(records), unions (variant records), and interfaces.
Implementing Interfaces
HRESULTs
It is standard COM practice
to have all interface methods return HRESULTs and use the stdcall calling convention.
An HRESULT
is a 4 byte data type used to return status information (success or failure
codes) as a result of method execution. If you come from a non-COM background,
this COM convention may take getting used to. For example, a Delphi procedure (a
subroutine that does not return a value) is represented in IDL as follows:
interface
IEcho: IDispatch
{
//procedure Echo;
HRESULT _stdcall Echo( void );
};
A Delphi function (a subroutine that
returns a value) is represented in IDL as follows:
interface
IEcho: IDispatch
{
//function Echo: WideString;
HRESULT _stdcall Echo( [out, retval] BSTR* Result );
};
Parameters
IDL allows interfaces to contain
methods that contain practically any number of parameters. The TLE restricts
parameter types to the automation data types. IDL provides flags to
specify how parameters are marshaled by the COM runtime.
| Marshaling is
the low-level process of transforming information contained in COM calls
into flat packets of information that are transported between COM clients
and servers. The COM runtime provides excellent marshaling facilities. In
addition, the COM runtime also provides the infrastructure to build customized
marshaling facilities into COM components. If you want to learn more about
custom marshaling, read up on the IMarshal interface.
|
The basic parameter direction/marshaling flags
are:
| Parameter
direction flag |
Delphi
native |
Description |
| [in] |
in |
Parameter is
marshaled from the client into the server |
| [out] |
out |
Parameter is
marshaled from the server to the client |
| [in, out] |
var |
Parameter is
marshaled from the client to the server and vice-versa |
| [out, retval] |
(method result) |
Prompts caller that
this parameter can be treated as the method result/return value. There can only be
one parameter marked with this flag - it is usually the last parameter. |
It is important to always pick the
correct parameter marshaling direction flag. For instance, if you expect the
server to only return a parameter value to the client, it is more efficient to
use [out] instead of [in, out]:
interface
IEcho: IDispatch
{
//server returns a BSTR value to the client
HRESULT _stdcall Echo( [out] BSTR* Param );
};
If the server expects a value from the
client and expects to modify it, use the [in, out] flags.
interface
IEcho: IDispatch
{
//server accepts and then returns a BSTR value to the client
HRESULT _stdcall Echo( [in, out] BSTR* Param );
};
If the server expects a value from the
client and does not modify it, use the [in] flag.
interface
IEcho: IDispatch
{
//server accepts a BSTR value from the client
HRESULT _stdcall Echo( [in] BSTR Param );
};
|
Note that a parameter marked with the [out] flag must be defined as a
pointer type (ala C pointers). In IDL, this is indicated by the asterisk (*)
symbol between the data type and the parameter name.
|
The Safecall Mapping
By default, Delphi maps all [dual]
interfaces as using the safecall calling convention. This normally applies to
interfaces built from creating new Automation Objects using the wizards. Under
the safecall mapping, our implementation does not directly see/access the HRESULTs
returned from interface methods. So for instance, this IDL definition:
interface
IEcho: IDispatch
{
//server accepts a BSTR value from the client
HRESULT _stdcall Echo( [in] BSTR Param );
};
Translates to this native Delphi
implementation:
procedure
TEcho.Echo(const Param: WideString); safecall;
begin
end;
Note that Echo is a safecall
procedure instead of a stdcall function that returns an HRESULT. As an
implementer, the safecall mapping eliminates the cumbersome task of assigning
HRESULT values (specially S_OK to indicate method success) at the end of every
method implementation.
Another convenience that the safecall
mapping provides is automatic conversion of the [out, retval] parameter as a
method return value. For instance:
interface
IEcho: IDispatch
{
//returns a BSTR value to the client as method return
value
HRESULT _stdcall Echo( [out, retval] BSTR* Param );
};
function TEcho.Echo: WideString; safecall;
begin
Result := 'I say Hello World!';
end;
The following simple examples
demonstrate implementing [out] and [in, out] parameters, under safecall:
interface
IEcho: IDispatch
{
//server accepts and then returns a BSTR value to the client
HRESULT _stdcall Echo( [in, out] BSTR* Param );
};
procedure TEcho.Echo(var Param: WideString);
begin
ShowMessage ('You said: ' + Param);
//param comes out as new value
Param := 'I say Shut Up!';
end;
interface
IEcho: IDispatch
{
//server returns a BSTR value to the client
HRESULT _stdcall Echo( [out] BSTR* Param );
};
procedure TEcho.Echo(out Param: WideString);
begin
//param comes out as new value
Param := 'I say Hello World!';
end;
Defining Custom Types
IDL also allows us to define custom types.
The most common custom types are enums, structs, and interfaces.
An enum is an enumerated data
type. IDL implements enums similar to C/C++ enums. The enum type is
simply a data storage of size 4 bytes and an enum value is a numeric constant.
The following illustrates a simple IDL enum:
typedef enum
tagEchoType
{
EchoTypeHelloWorld = 0,
EchoTypeGoodbyeWorld = 1
} EchoType;
Here, EchoType is the type name of our
enum and EchoTypeHelloWorld and EchoTypeGoodbyeWorld are its possible values.
The following illustrates usage of our
custom enum:
interface
IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] EchoType Param );
};
procedure TEcho.Echo(Param: EchoType);
begin
if Param = EchoTypeHelloWorld then
ShowMessage ('You said Hello World')
else
if Param = EchoTypeGoodbyeWorld then
ShowMessage ('You said Goodbye World');
end;
Another common custom type is the
struct. A struct is a record structure that consists of multiple parts/fields.
The following illustrates a simple IDL struct:
typedef struct tagEchoStruct
{
BSTR Message;
} EchoStruct;
Here, EchoStruct is the type name of our
struct and Message is its only field.
The following illustrates usage of our
custom struct:
interface IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] EchoStruct Param );
};
procedure TEcho.Echo(Param: EchoStruct);
begin
ShowMessage ('You said: ' + Param.Message);
end;
| Type library
struct marshaling is supported by the COM runtime only under the NT4 SP4 (and
above) equivalent COM version installation. If you have an earlier version
of COM, you must upgrade to the latest SP that supports struct marshaling
for your COM applications to work when using type library marshaled
structs. For Win 9x, I believe this is DCOM 1.2 or above.
|
Still another common custom type is an
interface pointer. The following illustrates usage of a custom interface pointer
data type:
interface IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] BSTR Param );
HRESULT _stdcall RepeatEcho( [in] IEcho* Echo, [in] BSTR Param, [in] long Count );
};
procedure TEcho.RepeatEcho(const Echo: IEcho; const Param: WideString;
Count: Integer);
var
i: integer;
begin
//call IEcho.Echo Count times
for i := 1 to Count do
Echo.Echo (Param);
end;
Note that interface pointers are
pointers to vtables. Therefore, they are represented in IDL with at least 1
level of indirection using the asterisk (*) symbol. When defining interface
pointers as [out] params, we'll also need another extra level of indirection.
Thus:
interface IEcho: IDispatch
{
HRESULT _stdcall YouGotMe( [out] IEcho** Param );
};
procedure TEcho.YouGotMe(out Param: IEcho);
begin
//return IEcho pointer to self
Param := Self;
end;
CoClasses
A coclass is a creatable COM class. It
is similar in concept to a native class in a particular language. Coclasses are uniquely identified using
CLSIDs.
CLSIDs are used by COM clients to instantiate a coclass. In COM, a
coclass implements at least 1 interface.
The following illustrates an IDL definition of a coclass, Echo, that implements
an interface, IEcho:
interface IEcho: IDispatch
{
HRESULT _stdcall Echo( [in] BSTR Param );
};
[
//CLSID of Echo
uuid(1050AE61-C88E-48FE-9C76-E655B23ADECA)
]
coclass Echo
{
[default] interface IEcho;
};
This simply states that a client can
create a COM component named Echo and ask for its IEcho interface. The [default]
flag indicates that IEcho is Echo's "primary" implemented interface. When imported by the
Delphi Type Library Import facility, the above definition translates to this:
IEcho =
interface(IDispatch)
procedure Echo(const Param: WideString); safecall;
end;
CoEcho = class
class function Create: IEcho;
class function CreateRemote(const MachineName: string): IEcho;
end;
Thus, if you recall our lesson
on how to build COM clients, the following illustrates a Delphi client that
creates the Echo coclass and uses the resultant IEcho interface:
uses
EchoServer_TLB;
procedure TForm1.EchoTestClick(Sender: TObject);
var
Echo: IEcho;
begin
//create Echo coclass and get back default IEcho interface
Echo := CoEcho.Create;
//use IEcho interface
Echo.Echo ('Hello World');
end;
Delphi implements each coclass as a
separate module. The basic implementation has the following skeletal structure:
//coclass
module
unit Echo;
interface
uses
ComObj, ActiveX;
type
//coclass implementation class
TEcho = class(TAutoObject, IEcho) //implements default
interface, IEcho
protected
//IEcho methods
end;
implementation
uses
ComServ;
//TEcho implementation
initialization
//coclass factory class initialization
TAutoObjectFactory.Create(ComServer, TEcho, Class_Echo,
ciMultiInstance, tmApartment);
end.
From the above, TEcho implements the
Echo coclass. TEcho derives from TAutoObjectFactory which means that Echo is an
Automation Object. TEcho implements IEcho which is Echo's default interface.
Towards the bottom of the module, a TAutoObjectFactory instance is initialized
that serves as Echo's class factory object.
Type Libraries
A type library stores and exposes type information
contained in a COM server. It is COM's native binary
format for defining interfaces, enums, structs, coclasses, etc. Type library
files have the following extensions: .TLB, .OLB.
A type library is normally
created by compiling IDL source code using the MIDL utility to produce a .TLB
file. This is similar in concept to
how Delphi compiles its source code into binary EXEs or DLLs. MIDL is part of
Microsoft's development tools specifically, VC++.
A Delphi COM project does not store physical
IDL files. Instead, the TLE compiles IDL in memory and creates the corresponding type
library file on the fly (this is actually done using COM's type information
builder interfaces: ICreateTypeLib, ICreateTypeInfo, ITypeInfo). Because of this, the IDL that we type into the TLE must
be syntax free before the TLE can save it to file. Don't worry about this
requirement, the TLE will always notify us of any syntax errors at save-time.
Building a COM server project binds/embeds the
type library file, as a resource, directly into the resultant EXE or DLL. This is made possible
by the {$R *.TLB} directive found in a server's DPR file. Since the type library is embedded in the server binary file, it is
possible to obtain type information about the server directly from the server
binary. In fact, when a Delphi COM server is registered (or unregistered), its
embedded type library is registered (unregistered) along with it. Because of
this, it is normally not necessary to distribute a separate .TLB file along with
a Delphi COM server.
Miscellany
Inside the Safecall Mapping
The safecall mapping alleviates some of
the complexities of standard behaviors expected of a COM interface
implementation. Consider the following implementation without safecall:
interface
IEcho: IDispatch
{
//server accepts a BSTR value from the client
HRESULT _stdcall Echo( [in] BSTR Param );
};
function TEcho.Echo(const Param: WideString); HRESULT; stdcall;
begin
ShowMessage ('You said: ' + Param);
//S_OK is an HRESULT that means success
Result := S_OK;
end;
Without safecall, we have to manually return
HRESULT information from each method implementation. In addition, we have
to ensure that native Delphi exceptions do not "leak out" of methods
without the proper HRESULT return codes. For instance, it is not valid to do the
following because an undefined HRESULT return code is returned and the
effect of leaking out a Delphi exception to COM is undefined:
function
TEcho.Echo(const Param: WideString); HRESULT; stdcall;
begin
//this exception will leak out into COM
raise Exception.Create ('You said: ' + Param);
//following code never executes
Result := S_OK;
end;
Instead, a correct way to implement this is:
function
TEcho.Echo(const Param: WideString); HRESULT; stdcall;
begin
try
raise Exception.Create ('You said: ' +
Param);
except
//no exceptions will leak out of this
handler
//E_FAIL is a generic HRESULT failure
code
Result := EFAIL;
end;
end;
Let's take a closer look again at
the safecall version:
procedure
TEcho.Echo(const Param: WideString); safecall;
begin
ShowMessage ('You said: ' + Param);
end;
The first thing you'll notice is the
absence of the HRESULT return code. Where did it go?
The Delphi compiler handled it for us.
The compiler emits a hidden Result := S_OK statement at the end of every
safecall method. If a native Delphi exception is raised, the compiler also emits
code that guarantees that the exception never leaks out of a safecall method.
All exceptions originating from methods of TComObject-derived classes are routed
through TComObject.SafecallException, which calls HandleSafecallException (ComObj),
which in turn translates Delphi exceptions into standard COM error information
using the IErrorInfo
construct.
How then do we return
an HRESULT code from safecall methods?
There are 3 ways to do this:
- Raise a native exception and have
the HandleSafecallException infrastructure handle it for us as described
above. When raising native exceptions, it is important to understand that
HandleSafecallException will only translate valid custom error codes into HRESULTs
if our exception is of type EOleSysError. This is discussed in detail in
this error
handling tip.
- Override
TComObject.SafecallException and return our own HRESULT codes directly from
there. Note that TComObject.SafecallException only gets called when a native
exception is raised from within a safecall method. Therefore, it is still
necessary to raise a native exception in a safecall method to trigger our
customized SafecallException implementation.
- Install a custom exception handler
callback to our TComObject-derived class. TComObject provides a
ServerExceptionHandler property of type IServerExceptionHandler that
contains 1 method, OnException. This allows us to provide specific exception
handlers per TComObject-derived instance or a global exception handler for
all TComObject-derived instances. Custom exception handlers are called from
TComObject.SafecallException, so it is still necessary to raise a native
exception in a safecall method to trigger our customized exception handler.
The following illustrates the 3 methods
mentioned above:
//Method
#1
uses
Windows;
procedure
TEcho.Echo(const Param: WideString); safecall;
begin
raise EOleSysError.Create ('You said: ' + Param,
ErrorCodeToHRESULT (1), //1 is
an arbitary custom error number
0);
end;
//converts a custom application error into a valid HRESULT error code
function ErrorCodeToHRESULT (ErrorNumber: integer): HRESULT;
begin
Result := MakeResult (SEVERITY_ERROR, FACILITY_ITF,
ErrorNumber);
end;
//Method #2
procedure
TEcho.Echo(const Param: WideString); safecall;
begin
raise Exception.Create ('You said: ' + Param);
end;
function TEcho.SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult;
begin
//return E_FAIL as actual HRESULT
Result := E_FAIL;
end;
//Method #3
procedure
TEcho.Echo(const Param: WideString); safecall;
begin
raise Exception.Create ('You said: ' + Param);
end;
procedure TEcho.Initialize; override;
begin
inherited;
//attach custom exception handler
ServerExceptionHandler := TEchoExceptionHandler.Create;
end;
//our custom exception handler
type
TEchoExceptionHandler = class (TInterfacedObject, IServerExceptionHandler)
private
procedure OnException(
const ServerClass, ExceptionClass, ErrorMessage: WideString;
ExceptAddr: Integer; const ErrorIID, ProgID: WideString;
var Handled: Integer; var Result: HResult);
end;
procedure TEchoExceptionHandler.OnException(const ServerClass,
ExceptionClass, ErrorMessage: WideString; ExceptAddr: Integer;
const ErrorIID, ProgID: WideString; var Handled: Integer;
var Result: HResult);
begin
//mark that we handled this exception
Handled := 1;
//fabricate out-of-memory HRESULT error
Result := E_OUTOFMEMORY;
end;
Coclasses and Interfaces
As mentioned earlier, a coclass can
implement as many interfaces as it wants. Continuing with our example, let's say
we introduce a new interface into the type library, IEcho2:
IEcho2 =
interface(IDispatch)
procedure SuperEcho(const Param: WideString); safecall;
end;
Implementing IEcho2 into the Echo
coclass simply requires adding IEcho2 to TEcho's class implementation:
TEcho = class(TAutoObject, IEcho,
IEcho2)
//IEcho methods
procedure Echo(const Param: WideString); safecall;
//IEcho2 methods
procedure SuperEcho(const Param: WideString); safecall;
end;
| Note that the
TLE allows inserting of additional interfaces into a coclass' Implements
tab. This results in a public description of a coclass' implemented
interfaces in the resultant type library. In addition, the TLE also
enables 2-way code synchronization using this technique.
However, it is not necessary to
add secondary interfaces implemented by a coclass to its Implements
section. The manual code modifications mentioned above is sufficient for a
COM client to ask for Echo's IEcho2 interface and use it. In fact, I don't
recommend using the TLE coclass' Implements mechanism as it sometimes has
the tendency to produce unexpected synchronization behaviors and errors
from within the TLE.
|
Conclusion
Building COM components involves
careful study and mastery of the following techniques:
- Learning IDL
- Implementing interfaces
- Understanding Delphi's native COM
implementation framework
|