techvanguards.com
Last updated on 9/27/2002

Building a COM Server Application
by Binh Ly

A COM server application provides services to a COM client 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 server:

  1. Determine what kind of COM server to create. This can be an EXE server or a DLL server.

  2. Create the COM server framework/housing.

  3. Create the COM components

  4. Deploy the COM server

DLL vs. EXE 

A DLL server normally executes in the address space of its client. As such, it is very efficient in terms of execution speed. Examples of DLL servers are windows shell extensions, plugins, ActiveX controls, and utility servers.

An EXE server executes outside the address space of its client. Since an EXE server is isolated from the client application, it can be configured to run under a separate security context, implement its own threading mechanisms, and has less impact on the client in case of runtime failure. Examples of EXE servers are standalone applications that also support automation such as the Microsoft Office applications, or a COM server implemented as a Windows Service application.

Although DLLs are normally executed in the address space of a client, it is possible to host a DLL server into an EXE application and thus achieve the same behavior and advantages of an EXE server. An example of such host is the MTS/COM+ runtime environment. One of main reasons to do this is to have the host provide add-on infrastructure not normally/easily available for DLL servers such as security management, thread management, process isolation, etc.

In general, it is safe to assume the we want to build DLL servers unless we are building COM into an existing standalone EXE application or a Windows Service application.

Note that only DLLs have the ability to be hosted into the MTS/COM+ runtime. If we build an EXE server, it cannot be hosted into the MTS/COM+ runtime down the road unless, of course, we convert it to a DLL server.

Creating the COM Server Framework/Housing

In Delphi, we create a DLL server using File | New | ActiveX | ActiveX Library. This creates a skeleton project that provides the 4 standard exports of a valid DLL COM server:

uses 
    ComServ;

exports
    DllGetClassObject,    //called by COM to obtain a class factory object
    DllCanUnloadNow,      //called by COM at runtime to determine if this DLL server is safe to unload
    DllRegisterServer,    //called by COM to register this DLL server
    DllUnregisterServer;  //called by COM to unregister this DLL server

The implementations of these exports can be found in ComServ. The Delphi COM library automatically handles management of the class factories and server outstanding reference count used to implement the above exports. 

For the EXE server, we simply create a standard EXE application: File | New Application. Delphi automatically adds a default form to our server that acts as the server's interactive user interface. If we don't want an EXE server to show this form, we simply add the following lines to our project's DPR file:

begin
    Application.Initialize;
    Application.ShowMainForm := False;
    Application.CreateForm(...);
    Application.Run;
end.

Note that it is important to have the Application.Initialize call as the first statement in the DPR file. This enables the ComObj module to initialize the COM runtime by calling CoInitialize/Ex at startup.

If you are migrating an EXE application to a COM server and forgot to call Application.Initialize, you'll get a "CoInitialize has not been called" error at the first statement that makes a COM call that requires exporting/importing a COM interface. This is a common error when migrating standalone applications into COM servers or upgrading a COM server from an older version of Delphi to Delphi 5.

Creating COM Components

In Delphi, we can create several types of COM components:

  1. COM Object - a COM component that does not support IDispatch/Automation. This is used to build lightweight COM components such as Windows shell extensions and non-scriptable servers such as plugins, etc.

  2. Automation Object - a COM component that supports IDispatch/Automation. This is used to build COM components that are scriptable and support late-binding, in addition to being able to implement a standard vtable interface. In general, this is the most common type of COM component that we are going to create.

  3. MTS Object - an Automation Object that has access to the intrinsic MTS/COM+ runtime context object. This is used to build COM components that will be hosted in the MTS/COM+ runtime and will need the convenience of accessing the MTS/COM+ context object and other basic MTS/COM+ facilities.

  4. Active Server Object - an Automation Object that has access to the intrinsic ASP server objects such as Request, Response, etc. ASP is a scripting environment used to build web applications on the Microsoft IIS platform. This is used to build COM components that will be called from ASP and will need the convenience of accessing the ASP server objects.

  5. ActiveX Control - a COM component that conforms to the COM ActiveX control standard. An ActiveX control is a UI control that is interoperable across and can be hosted into ActiveX container environments.

When creating a COM component, the wizard always asks for the desired threading model. As we've studied before, the threading model specifies how our COM components behave when used in a multithreaded environment. The wizard also asks for an Instancing option. Instancing defines how COM asks a class factory to create our server components.

The COM Object

A COM Object is created using File | New | ActiveX | COM Object. In the COM Object Wizard dialog, we're presented with 2 extra options:

  1. Include Type Library - if checked, specifies that our COM component will implement a new IUnknown-derived interface that should be defined in the type library. If this option is unchecked, no new interface is created and the resultant COM component does not implement any custom interfaces by default.

  2. Mark Interface OleAutomation - if checked, specifies that if a new interface is implemented (Include Type Library option is checked), this interface should have the [oleautomation] flag set. IMO, this option has no use because you'll most likely need to check it all the time. Unchecking this option makes no sense because Delphi does not support creation of custom proxy-stub marshalers and so the resultant interface is not usable by client applications (since it is not marked as [oleautomation] and there is no other marshaling option).

When creating a COM Object, the resultant class derives from either TComObject (Include Type Library option unchecked) or TTypedComObject (Include Type Library option checked). TComObject does not support any kind of type information exposure at all. TTypedComObject supports the most basic type information exposure through the IProvideClassInfo interface.

When adding methods to this COM component using the TLE (Type Library Editor), Delphi does not use the safecall calling convention by default. Thus, we'll see every method implemented to return the standard COM HRESULT and use the stdcall calling convention. To enable the safecall mapping, tweak the Tools | Environment Options | Type Library | Safecall function mapping option in the IDE. If set to "All vtable interfaces", this enables the safecall mapping for our interface.

The Automation Object

An Automation Object is created using File | New | ActiveX | Automation Object. In the Automation Object wizard dialog, we're presented with 1 extra option:

  1. Generate Event Support Code - if checked, this generates a dispinterface event interface definition in the type library and some simple code that allows management of the event. This event is based off of the COM connection points architecture. We'll get into the details of this in a later lesson.

When creating an Automation Object, the resultant class derives from TAutoObject. TAutoObject supports type information exposure (IProvideClassInfo) as well as a standard type-information based implementation of IDispatch. A new interface is created in the type library that derives from IDispatch and is marked [dual, oleautomation].

Methods added to an Automation Object are automatically mapped as safecall by default. Again this behavior can be changed by tweaking the Tools | Environment Options | Type Library | Safecall option in the IDE.

The MTS Object

An MTS Object is created using  File | New | Multitier | MTS Object. In the MTS Object wizard dialog, we're presented with a Transaction Model option. This option specifies how the MTS/COM+ runtime manages transactions while hosting our component. 

The principles behind MTS transactions are non-trivial so I highly recommend that you read up on the theories behind MTS/COM+ before doing any serious MTS/COM+ development. Some excellent references on the subject are: Understanding COM+ by David Platt or Understanding Windows 2000 Distributed Services by David Chappell.

When creating an MTS Object, the resultant class derives from TMtsAutoObject. TMtsAutoObject provides a default implementation of IObjectControl and encapsulates some of the facilities provided by the MTS context object. For example, the following illustrates usage of the IObjectContext SetComplete and SetAbort methods as wrapped by TMtsAutoObject:

type
    TAccount = class(TMtsAutoObject, IAccount);

function TAccount.Update(var ID: OleVariant; const LoginID,
    Name: WideString): WordBool;
begin
    try
        ...Execute Update logic here...
        //MTS SetComplete
        SetComplete;
    except
        //MTS SetAbort
        SetAbort;
        raise;
    end;
end;

The Active Server Object

An Active Server Object is created using File | New | ActiveX | Active Server Object. In the Active Server Object wizard dialog, the Active Server Type option selections are:

  1. Page-level methods - this adds the OnStartPage and OnEndPage methods to our COM component. These methods provide support for a legacy protocol that enables IIS to hook into our component when setting up the link to the ASP server objects. I don't recommend this option unless you are running IIS 3.0 or you are building an EXE server to be called from ASP/IIS.

  2. Object Context - this enables access to the ASP server objects through the newer MTS/COM+ runtime context object. When building DLL servers to run under the latest version of IIS, this option is recommended. 

When creating an Active Server Object, the resultant class derives from either TASPObject (Page-level methods option) or TASPMTSObject (Object Context option). Either way, access to the intrinsic ASP objects is the same. The following illustrates writing to the ASP response stream from within an Active Server Object:

type
    TBar = class(TASPMTSObject, IBar);

procedure TBar.HelloWorld;
begin
    Response.ContentType := 'text/html';
    Response.Write ('<html><body>');
    Response.Write ('Hello World');
    Response.Write ('</body></html>');
end;

//ASP code
<%
dim Bar
set Bar = Server.CreateObject ("BarServer.Bar")
Bar.HelloWorld
set Bar = nothing
%>

If you come from a Java/J2EE background, an Active Server Object is similar in concept to a Servlet component.

If you come from an ASP background, you'll probably wonder why you'd want to create an Active Server Object instead of performing business logic (page generation, etc.) directly in ASP. The main reasons are:

  1. An Active Server Object can hide a lot of complex business logic that executes at maximum performance. 
  2. ASP is script-based and makes late-bound calls. Therefore, it is less efficient that an Active Server Object that contains compiled code when accessing the intrinsic ASP objects.

Deploying COM Servers

We've previously studied how to register COM servers and how to properly check for a registered COM server. In general, we cannot use a COM server unless it is correctly installed and registered.

When developing COM clients, it is not uncommon to get the error "Invalid class string" or "Class not registered" once in a while or in a new install in a production environment. What this error means is that the COM server that our client is trying to create is somehow not properly registered on the target machine.

It is also important to understand that different versions of a COM server can produce this error. For example, if our COM client is expecting to automate MS Word 2000 and the target machine has MS Word 97 (but not 2000), it is very possible to receive the above error. The fix, obviously, is to ensure that the correct COM server version is installed and registered on the target machine.

COM servers that are intended to be accessed from a remote machine through DCOM almost always need to be properly configured before usage. This is usually done using the DCOMCNFG utility. Since the mechanics of configuring DCOM security is a complex topic, I refer you to some resources that might be of help:

  1. Implementing a MultiUser DCOM Application tutorial
  2. DCOM security resources on MSDN

Miscellany

Inside the COM Server Housing

Every COM server contains a global object, ComServer (ComServ module), that serves as the heartbeat of the server's housing. Some of the most important aspects of TComServer are:

TComServer = class(TComServerObject)
public
    procedure UpdateRegistry(Register: Boolean);
    property ObjectCount: Integer;
    property StartMode: TStartMode;
    property UIInteractive: Boolean; //D5 and above only
    property OnLastRelease: TLastReleaseEvent;
    property ServerFileName: string;
    property ServerKey: string;
    property TypeLib: ITypeLib;
end;

TComServer.UpdateRegistry

This method registers (and unregisters) our COM server. For DLLs, UpdateRegistry (True) is called by  DLLRegisterServer and UpdateRegistry (False) is called by DLLUnregisterServer. For EXEs, UpdateRegistry (True) is called when the server is not run with the "/unregserver" command-line parameter; otherwise UpdateRegistry (False) is called.

For EXEs, UpdateRegistry (True) is always called unless the "/unregserver" command-line parameter is specified. This means that our server will execute the registration process under circumstances that we may not expect. For instance, whenever the server is activated as a result of a request from a COM client, it registers itself, unnecessarily. This may have implications on a deployment scenario where registration could fail if the server's activator account does not have sufficient rights to the registry (registration writes entries into the registry). In fact, this was a problem with D4 and prior versions, but was corrected in D5.

However, the fact still remains that EXE servers perform registration under circumstances that we may not expect.

TComServer.ObjectCount

ObjectCount returns the total number of outstanding COM objects in the server at any given time. This does not include the count of outstanding class factory instances - this count is kept in a private TComServer field, FFactoryCount. ObjectCount (and FFactoryCount) is used to implement DLLCanUnloadNow for DLLs or the shutdown process for EXEs.

TComServer.StartMode

StartMode specifies the activation mode for EXE servers. It can be any of the following values:

StartMode Description
smStandAlone Server is not activated through COM and is not being registered/unregistered
smAutomation Server is activated through COM
smRegServer Server is activated with the "/regserver" command-line parameter
smUnregServer Server is activate with the "/unregserver" command-line parameter

TComServer.UIInteractive

UIInteractive specifies if an EXE server should pop up a confirmation dialog when an attempt is made to terminate it while there are still outstanding COM objects running. It is convention for a COM server to refuse termination until the last outstanding COM object is released. If the server didn't do this, clients may get fatal errors as a result of unexpected termination of the server.

The default value for UIInteractive is True, meaning that a confirmation should always be asked. If we want to turn this behavior off, we simply add the following lines to our server's DPR file:

begin
    Application.Initialize;
    Application.UIInteractive := False;
    Application.CreateForm(...);
    Application.Run;
end.

TComServer.OnLastRelease

This is an event that gets called after the last outstanding server object is released in an EXE server. We can assign a custom handler to this event to determine the fate of our server's shutdown. For example, in our DPR file:

type
    TMyLastReleaseHandler = class
    public
        procedure OnLastRelease (var Shutdown: boolean);
    end;

procedure TMyLastReleaseHandler.OnLastRelease (var Shutdown: boolean);
begin
    //don't allow shutdown
    Shutdown := False;
    //reactivate class factories
    CoResumeClassObjects;
end;

var
    MyLastReleaseHandler: TMyLastReleaseHandler;
begin
    MyLastReleaseHandler := TMyLastReleaseHandler.Create;
    Application.Initialize;
    Application.OnLastRelease := MyLastReleaseHandler.OnLastRelease;
    Application.CreateForm(...);
    Application.Run;
    MyLastReleaseHandler.Free;
end;   

TComServer.ServerFileName

This returns the server's full file name.

TComServer.ServerKey

This returns "InProcServer32" for DLLs and "LocalServer32" for EXEs. This string is useful during the registration process.

TComServer.TypeLib

This returns an ITypeLib pointer to the server's primary type library.

Inside the Class Factories

The Delphi COM framework also provides implementations for COM class factories. The factories implement, among other things, the IClassFactory interface, COM component properties (such as the threading model, instancing, coclass CLSID), registration, licensing, etc. The following class factory implementations are available:

Factory Class Description
TComObjectFactory Base factory class. Used as factory for TComObjects.
TTypedComObjectFactory Used as factory for TTypedComObjects
TAutoObjectFactory Used as factory for TAutoObjects and descendants

The most important aspects of TComObjectFactory are:

Method/Property Description
CreateComObject Creates an instance of the associated COM component
RegisterClassObject Used by EXE servers to register a class factory into the COM runtime using the  CoRegisterClassObject API. Note that this is not the same as registering a COM component into the registry
UpdateRegistry Registers (and unregisters) a COM component
ClassID CLSID of the associated COM component
ClassName Short name of the associated COM component
ProgID PROGID of the associated COM component
Instancing Instancing option of the associated COM component
ThreadingModel Threading model option of the associated COM component

The most important aspects of TTypedComObjectFactory are:

Method/Property Description
ClassInfo Holds an ITypeInfo pointer of the associated COM component's type information

The most important aspects of TAutoObjectFactory are:

Method/Property Description
DispTypeInfo Holds an ITypeInfo pointer to the associated COM component's default dispatch interface
EventTypeInfo Holds an ITypeInfo pointer to the associated COM component's default [source] interface. This is usually the primary event dispinterface of the COM component.

Class factories are defined (and instantiated) in the initialization unit of every COM component module. An example of a class factory definition/instantiation for an Automation Object named Foo is:

initialization
    TAutoObjectFactory.Create(ComServer, TFoo, Class_Foo,
        ciMultiInstance, tmApartment);
end.

The Delphi COM framework keeps an internal list of all the class factories initialized in the server. Every class factory that's initialized like the above is automatically added to a running factory manager object, ComClassManager (ComObj). The factories are actually chained together in a linked-list fashion - each factory points to the next factory, and so on. ComClassManager handles simple tasks such as adding new factories, removing factories, locating factories given a CLSID, etc. 

During the server's registration (and unregistration) process, ComClassManager iterates the list of factories to perform registration/unregistration of each COM component. For instance, the following pseudocode illustrates the implementation for TComServer.UpdateRegistry:

procedure TComServer.UpdateRegistry(Register: Boolean);
begin
    //ComClassManager.ForEachFactory is an iterator method
    ComClassManager.ForEachFactory(Self, FactoryUpdateRegistry);
end;

//this is called for each factory contained in ComClassManager
procedure TComServer.FactoryUpdateRegistry(Factory: TComObjectFactory);
begin
    //call TComObjectFactory.UpdateRegistry
    Factory.UpdateRegistry(FRegister);
end;

From the above, we see that TComObjectFactory.UpdateRegistry is where the actual registration/unregistration process is implemented for each COM component in our server. 

If you ever have your own registration requirements, simply create your own customized factory class that derives from the appropriate Delphi factory class. In your factory class, override UpdateRegistry and implement the custom registration in there. Then replace the COM component's factory initialization line with your custom factory class.

ComClassManager also implements the DLLGetClassObject export for DLL serves. Here's the pseudocode for DLLGetClassObject:

function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HRESULT;
var
    Factory: TComObjectFactory;
begin
    //obtain factory object by CLSID
    Factory := ComClassManager.GetFactoryFromClassID(CLSID);
    if Factory <> nil then
        //if factory object found, QI it for IID
        Result := Factory.QueryInterface (IID, Obj)
    else
    begin
        //if factory object not found, return proper failure code
        Pointer(Obj) := nil;
        Result := CLASS_E_CLASSNOTAVAILABLE;
    end;
end;

The EXE Server Threading Model

Assuming that we properly call Application.Initialize as the first statement in our EXE server, the Delphi COM framework will automatically call CoInitialize/Ex for us upon startup. This allows our server to initialize its COM environment and interact with it. 

CoInitializeEx can be called to initialize an apartment-threaded environment (COINIT_APARTMENTTHREADED) or a free-threaded environment (COINIT_MULTITHREADED). The details of these threading models are non-trivial and can be studied elsewhere. By default, Delphi uses apartment threading for EXE servers. However, if at least one of the COM components in an EXE server is marked as having a Threading model of Free or Both, Delphi automatically uses free threading. Regardless of these default behaviors, the final decision as to which threading model is used lies in a global variable, CoInitFlags, defined in ComObj. Set it explicitly to COINIT_APARTMENTTHREADED or COINIT_MULTITHREADED, before calling Application.Initialize, to control the actual server threading model.

When using apartment threading for EXE servers, Delphi only provides a single thread for our entire server. There are no facilities for thread pooling under apartment threading in Delphi. However, if you're up to it, study my "Threading Options for Delphi COM Servers" article to learn how to implement thread pooling for EXE servers.

Conclusion

To summarize, building COM server applications in Delphi involves 4 simple steps:

  1. Determine the desired server type: DLL or EXE
  2. Building the server framework/housing
  3. Building the COM components
  4. Deploying the server
Copyright (c) 1999-2011 Binh Ly. All Rights Reserved.