Class factories
When an external client creates an automation object, the creation process
itself is handled by the class factory. An automation class factory takes care
of the registration of the class, so an external client can find the factory.
When a request is made to create an object the class factory can perform a well
defined check whether the client has a license to create objects of the class.
In this chapter I will take a closer look at the way these class factories
are implemented in Delphi and I will show how you can adapt a class factory to
the needs of your objects.
Creation of the factory
When the Delphi wizard generates code for the implementation of an automation
class you will always find this piece of code in the initialization
section of the unit
initialization
TAutoObjectFactory.Create(ComServer, TFileZapper, Class_FileZapper,
ciMultiInstance, tmApartment);
end.
As the server is loaded an object of type tAutoObjectFactory is
created. This VCL class is found in ComObj.pas and inherits from TComObjectFactory.
The most important part of its declaration reads as follows
TComObjectFactory = class(TObject, IUnknown, IClassFactory, IClassFactory2)
private
....
protected
function GetProgID: string; virtual;
function GetLicenseString: WideString; virtual;
function HasMachineLicense: Boolean; virtual;
function ValidateUserLicense(const LicStr: WideString): Boolean; virtual;
{ IUnknown }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
{ IClassFactory }
function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID; out Obj): HResult; stdcall;
function LockServer(fLock: BOOL): HResult; stdcall;
{ IClassFactory2 }
function GetLicInfo(var licInfo: TLicInfo): HResult; stdcall;
function RequestLicKey(dwResrved: Longint; out bstrKey: WideString): HResult; stdcall;
function CreateInstanceLic(const unkOuter: IUnknown; const unkReserved: IUnknown; const iid: TIID; const bstrKey: WideString; out vObject): HResult; stdcall;
public
constructor Create(ComS ........................
The class implements three interfaces defined in the automation specs: the
well known Iunknown, IClassFactory and IClassFactory2. IclassFactory.CreateInstance
is the workhorse of the factory, this is the place where the actual automation
objects themselves are created.
ICLassFactory2 adds three methods to the factory, all of these have to do
with licensing.
The VCL is rich in a variety of classfactories. Every type of COM object,
from a pure COM object to an ActiveForm has its own factory. All of these
inherit from TCOMobjectFactory, each adds some functionality and has an
overloaded constructor to pass the necessary info to create the factory.
Lets take a look at the parameters of the constructor of an automation object
:
- ComServer A global
object which holds a lot of useful information on the server.
- TFileZapper The Delphi class which implements the
automation class.
- Class_FileZapper The GUID identifying the
automation class. It can be found in the typelibrary.
- ciMultiInstance The instancing mode of the
server. Default this is ciMultiInstance which means that
multiple instances of a client can share one and the same instance of
the server. The alternative is ciSingleInstance which means for every
new client a new instance of the server is started.
- tmApartment The threading model supported by the
server. Using anything else then tmApartment is a science on itself.
An automation server having an user interface, such as this demo fileserver,
can only support apartment threading.
Some of these parameters were options in the automation object wizard. You can change
those values here.
The comserver object is found in the VCL unit ComServ.Pas. It is
created in the initialization section of the unit:
initialization
begin
OleAutHandle := SafeLoadLibrary('OLEAUT32.DLL');
ComServer := TComServer.Create;
if not ModuleIsLib then
begin
SaveInitProc := InitProc;
InitProc := @InitComServer;
AddTerminateProc(@AutomationTerminateProc);
end;
end;
First OLEAUT32.dll is loaded, this Windows DLL houses the automation API
functions. If the project is a standalone exe (not ModuleIsLib) then the
code starts tampering with the initialization code in such a way that the
automation system is initialized (by a call to the API function CoInitializeEx)
and that the server will try to register itself.
When the server is running the ComServer object has some very usable
properties. They provide interesting info on the server but can also be used to
influence its behavior.
- IsInprocServer, Serverkey A boolean flag
indicating a server DLL. Serverkey is the textual representation,
being InprocServer32 or LocalServer32.
- ServerFileName, ServerName The name of the
exe or dll. ServerFilename is the full name including the path
- StartMode This is an indication how the server
was started. It can be smStandalone when no automation is involved or
smAutomation when a client has started the server by requesting an
object. The smRegServer and smUnregServer indicate that the
server was started for registration
purposes, either with the command line parameter (UN)REGSERVER, the
Delphi IDE or any other registration tool.
- UIinteractive This flag indicates whether the
server has any user interface. You can set this flag to false to silence the
error messages which will pop up if the server is stopped while there are
still clients using it. Something useful when the code is running on an
unattended (web-) server. The property does not silence all UI, you will
have to take additional measures to catch other undesired dialogs.
This ComServer object is passed to all objectfactories present in the
server. So when the server is loaded there is one ComServer object and a
classfactory for every automation class the server implements.
Creating an own classfactory
Now that we have a overview how classfactories work in Delphi it is time to
do something with them. First I will do some work in the registration process of
the server. After which I will dive deeper in the actual creation of the
automation objects and implement licensing.
I will declare an own class factory which inherits from the VCL classfactory
type
TFileZapperFactory = class(TAutoObjectFactory)
end;
To use my factory, I will have to change the initialization section of the
server
initialization
TFileZapperFactory .Create(ComServer, TFileZapper, Class_FileZapper,
ciMultiInstance, tmApartment);
end.
Now the filezapper server will use my classfactory and I can introduce all
desired behavior by adapting the TFileZapperFactory.
Adding info in the registration of your class
In the registration process the Windows registry is updated. Here I would
like to see the date on which this registration has taken place. tAutoObjectfactory
has an UpdateRegistry method, I will override this and do my work there.
type
TFileZapperFactory = class(TAutoObjectFactory)
public
procedure UpdateRegistry(Register: Boolean); override;
...
procedure TFileZapperFactory.UpdateRegistry(Register: Boolean);
const ValueName = 'DateRegistered';
begin
inherited;
if register then
CreateRegKey(ProgID, ValueName, DateToStr(Date()));
end;
First I will call inherited. The classes tTypedComObjectFactory
and tComObjectFactory, two ancestors of tAutoObjectFactory,
will do some work in the registry after which there will be a registry key HKEY_CLASSES_ROOT\FileManager.FileZapper.
When the register parameter has a value of true I want to add the
date under this key. ProgID is a handy property of the factory class, it
returns the progID FileManager.FileZapper of the class. CreateRegKey is a
handy API function which will add the value under the key.
After registration the date of registration can be read with the registry
editor:

When unregistering the updateregistry method receives false as
parameter value. In my case I don't have to do anything as the inherited
code will delete the entire key, including my date value. If you want to do something
more precise CreateRegKey has a counterpart named DeleteRegKey.
Registration of the ProgId and registration of the CoClass
The ProgId is the user readable identification of the automation
server. It is used by late binding clients to
create instances of the class. The value of ProgId itself is obtained by
a virtual method TComObjectFactory.GetProgId. It's implementation
in the VCL is very straightforward:
function TComObjectFactory.GetProgID: string;
begin
if FClassName <> '' then
Result := FComServer.ServerName + '.' + FClassName else
Result := '';
end;
In case you are not satisfied with this string you are free to override this
function. Which will, amongst other things, result in a different registry
location.
The updateregistry method uses this ProgId to find the
HKEY_CLASSES_ROOT\ProgId key in the registry. An early binding client does not
use the ProgId but directly the ClassId.
This ClassId is also found in the registry under HKEY_CLASSES_ROOT\CLSID\ClassId.
When registering the automation server this entry has to be created as well. It
is done in TComObjectFactory.RegisterClassObject. This methods makes a
call to CoRegisterClassObject, an automation API function. RegisterClassObject
is a static method, so it is not possible to add or change any behavior of this
part of the registration process.
Registration problems
A standalone server (.exe) will try to register itself every time it is run. In
newer Windows versions it is possible to deny a Windows user the right to write
in the registry. As we have seen registering an automation server means writing
in the registry. All registry updates get called from tComServer.Initialize.
A write failure is handled in a very elegant way :
procedure TComServer.Initialize;
begin
try
UpdateRegistry(FStartMode <> smUnregServer);
except
on E: EOleRegistrationError do
// User may not have write access to the registry.
// Squelch the exception unless we were explicitly told to register.
if FStartMode = smRegServer then raise;
end;
if FStartMode in [smRegServer, smUnregServer] then Halt;
ComClassManager.ForEachFactory(Self, FactoryRegisterClassObject);
end;
A failure to write to the registry leads to an exception. And this exception is
only propagated when there was an explicit wish (or need) to register. Which can be seen
by inspecting the startmode property. So if it is not possible to write
to the registry this will be skipped in silence.
Things get a little muddy when the server had not been registered yet. Now
the registration just has to take place. After a vague general AV message a more
informative dialog pops up :

The registration fails and the program terminates.
The program has to be installed by an user with write access to the registry,
after which it can be used by an user with restricted rights. This elegance is not found in
Delphi versions prior to 5.01. There the exception in Initialize is not
caught and the program always crashes when the user does not have enough rights.
Licensing is an explicit part of the automation specification. The interface
definition of IClassFactory2 adds methods
and properties to the classfactory to provide for a standard way of handling
illegal use of automation software components. The base of this licensing
schema makes the following assumptions :
- Licensing is based on a license key, which is a string.
- The key is stored in a file with the same name as the server, with the
extension .lic.
In the Delphi VCL licensing has been implemented for ActiveX controls. To
make an automation object licensed will take some work, I will use a simplified
version of the ActiveX implementation as a guideline.
The tAutoObjectFactory class has two properties concerning licensing
- LicString The value of the key needed to create
objects.
- SupportsLicensing A flag indicating if the class
checks licensing.
To see how licensing works in the VCL I will take a deeper look at
implementation of tAutoObjectFactory. The creation of objects is taken
care of by the CreatInstance method.
function TComObjectFactory.CreateInstance(const UnkOuter: IUnknown; const IID: TGUID; out Obj): HResult;
begin
Result := CreateInstanceLic(UnkOuter, nil, IID, '', Obj);
end;
All this method does is pass the call to the IClassFactory2 method CreateInstanceLic
with an empty key. This method does the work :
function TComObjectFactory.CreateInstanceLic(const unkOuter: IUnknown; const unkReserved: IUnknown; const iid: TIID; const bstrKey: WideString;out vObject): HResult; stdcall; begin
.....
// Check for licensing.
if FSupportsLicensing and
((bstrKey <> '') and (not ValidateUserLicense(bstrKey))) or
((bstrKey = '') and (not HasMachineLicense)) then
begin
Result := CLASS_E_NOTLICENSED;
Exit;
end;
...
The methods first checks if the class does support licensing. If so it will
check the key passed. This key is always empty, as passed by CreateInstance.
The second check is HasMachineLicense, which is a virtual protected
method.
Now I can implement the licensing of automation objects in two steps
- Pass the value of the required key in the constructor of the
factory.
- Implement a license check in HasMachineLicense.
I will create a new constructor CreateLicensed, the location of the
key parameter is inspired by the VCL objectfactory for ActiveX controls.
type
TFileZapperFactory = class(TAutoObjectFactory)
protected
function HasMachineLicense: boolean; override;
constructor TFileZapperFactory.CreateLicensed(ComServer: TComServerObject;
AutoClass: TAutoClass;
const ClassID: TGUID;
Instancing: TClassInstancing;
const LicStr: string;
ThreadingModel: TThreadingModel);
begin
inherited Create(ComServer, AutoClass, ClassId, Instancing, ThreadingModel);
LicString := LicStr;
SupportsLicensing := LicStr <> '';
end;
This constructor will call the default constructor and set the two licensing
properties of the factory object.
Next is the actual implementation of the license check in HasMachineLicense
function TFileZapperFactory.HasMachineLicense: boolean;
var i: Integer;
FLicFileStrings : tSTringList;
begin
if not SupportsLicensing then
Result := True
else
begin
result:= False;
fLicFileStrings:= tSTringList.Create;
try
FLicFileStrings.LoadFromFile(ChangeFileExt(ComServer.ServerFileName, '.lic'));
i := 0;
while (i < FLicFileStrings.Count) and (not Result) do
begin
Result := ValidateUserLicense(FLicFileStrings[i]);
inc(i);
end;
except
// Licence file not found
end;
fLicFileStrings.Free;
end;
end;
The first check is if the factory does support licensing, when not the
function only needs to return true. A stringlist is created to hold the
textlines of the license file. To find the file the name of the serverfile can
be read from the global ComServer object, which will be changed to the
correct filename by ChangeFileExt. The file is loaded into the stringlist
which then can be scanned for the license key.
To bring it all together I have to change the initialization section
so my new constructor will be used
initialization
TFileZapperFactory.CreateLicensed(ComServer, TFileZapper, Class_FileZapper,
ciMultiInstance, 'Gekko Software', tmApartment);
Whenever a client will try to create a filemanger object a check on a
license file containing a line reading Gekko Software will be made.

Upon failure a dialog will pop up and the client will not have its automation
object.
Making a license harder to crack
What really happens inside the license check is up to you. The format of the
contents of the license file is up to you. Inside HasMachineLicense you
could decrypt any data read any way from the license file and even perform
hardware checks on "dongles" and the like. There is only one rule you
have to obey, the acceptance of the license has to be done by a call to ValidateUserLicence.
As an automation client could make a direct call to CreateInstanceLic passing
the license key itself. In which case your HasMachineLicense method
will not be called at all. You can override ValidateUserLicence, but you
cannot override CreateInstanceLic.
Where are we ?
We have seen that the creation of automation objects is governed by a
classfactory. This classfactory is the place where an automation class
communicates with the outside world. It takes care of the registration of the
class and it takes care of the actual creation of the automation objects. The
object oriented way in which classfactories are implemented in the Delphi
VCL makes it not too difficult to change the behaviour of these factories.
What's next ?
|