|
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:
- Determine what kind of COM server to
create. This can be an EXE server or a DLL
server.
- Create the COM server
framework/housing.
- Create the COM components
- 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:
- 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.
- 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.
- 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.
- 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.
- 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:
- 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.
- 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:
- 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:
- 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.
- 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:
- An Active Server Object can hide a
lot of complex business logic that executes at maximum performance.
- 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:
- Implementing
a MultiUser DCOM Application tutorial
- 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:
- Determine the desired server type:
DLL or EXE
- Building the server
framework/housing
- Building the COM components
- Deploying the server
|