techvanguards.com
Last updated on 1/25/2016

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:

  1. Understanding IDL

  2. Implementing interfaces

  3. Understanding COM error handling, HRESULTs, and safecall

  4. 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:

  1. IDL is COM's native language. A good COM developer must know IDL because this knowledge is helpful in other areas of COM.

  2. 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:

  1. 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

  2. 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.

  3. 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:

  1. Learning IDL
  2. Implementing interfaces
  3. Understanding Delphi's native COM implementation framework
Copyright (c) 1999-2011 Binh Ly. All Rights Reserved.