2009. augusztus 6., csütörtök

COM Activation


Problem/Question/Abstract:

Did you ever wonder how COM obejcts are created? Did you know you are not the one that creates them? Read more about COM activation right here

Answer:

Introduction

COM has always been presented as something complex to digest and tedious to administer. Nothing can be further from the truth. What you need is, as with everything else, to understand a few principles. After that everything will start to make sense and you will be able to find your way in the land of COM. You may ask why COM and SOAP? Well, the answer is simple: because we can.

Windows comes equipped with an incredible set of tools and services that make distributed development a breeze. This is especially true if you are a Delphi developer because the way it wraps COM is so elegant that there's virtually no difference in writing a regular Delphi class and a COM object. Obviously the devil is in the details and that is what scares most of the people.

In this article I will explain a basic principle: what happens when you try to instantiate a COM object first. Then I will explain why this mechanism (pattern) is so useful. If you get lost check the "To recap" section below. It might help.

Registration and class factories

After you build a COM object like I explained in my Introduction to COM article you need to register it in order to make it available for your client applications. There are different ways to register a COM object:

You can use RegSvr32.exe which is under Windows\System32

You can install your COM object in a COM+ application (which was called Microsoft Transaction Server (MTS) Package under NT4)

You can click on the button Register in the Delphi type library editor from the IDE

Option 2 is the best way for a variety of reasons I will explain later. But why do you need to register a COM object? Why cannot you just use it?

Well, in order to use an object you need to create it. That is the problem. You are not the one that creates the COM object. Windows does it for you and it does that using a class factory that you provide with your COM object.

Take a look at the DSOAPNTier sample application and open the unit uOrderManager_Impl.pas. Go to the end to the initialization section. This is what you will find:

initialization
  TAutoObjectFactory.Create(ComServer, TOrderManager, Class_OrderManager,
    ciMultiInstance, tmApartment);
end.

This code runs as soon as the COM DLL is loaded and creates a class factory. Windows uses it to create the COM object.

Structure of a COM DLL

Class factory... Ok, the initialization section creates it. Windows then uses it... But wait a moment. Who gives Windows the reference to the class factory? Looking at the code, it only looks like a class that is created and potentially never destroyed. Is that a memory leak?

Obviously not. The answer is in the project file (.dpr). If you open it this is what you will see:

library DSOAPNTierLib;

uses
  ComServ, [..];
exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

{$R *.TLB}

{$R *.RES}

begin
end.

Those 4 exported functions are the key to our class factory problem. In order for a regular DLL to be a COM DLL, it has to export those functions. If you open the unit ComServ.pas you will be able to see their formal declaration and the implementation that Delphi automatically provides for you.

Those functions are declared as:

function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult; stdcall;
function DllCanUnloadNow: HResult; stdcall;
function DllRegisterServer: HResult; stdcall;
function DllUnregisterServer: HResult; stdcall;

I hope everything begins to make sense.

DllGetClassObject: it is the function that Windows calls to get the pointer to the class factory object (the one that gets automatically created in the initialization section). The first parameter indicate whose object's class factory it wants returned by using the class factory GUID. The second is a parameter that indicates what interface Windows wants to use to communicate with the class factory. This is almost always IID_IClassFactory or IID_IClassFactory2 which are system interfaces. The third is the pointer to the returned class factory.

After Windows calls this function it will have a pointer to the class factory. That object implements IClassFactory

IClassFactory = interface(IUnknown)
  ['{00000001-0000-0000-C000-000000000046}']
  function CreateInstance(const unkOuter: IUnknown; const iid: TIID; out obj): HResult;
    stdcall;
  function LockServer(fLock: BOOL): HResult; stdcall;
end;

There we go! Trought the method CreateInstance finally, after all this work, Windows is able to create the object!

DllCanUnloadNow: is called by Windows to indicate whether the server can be unloaded from memory because it is no longer in use.

DllRegisterServer, DLLUnregisterServer: they register and unregister the DLL by storing a bunch of informations in the Windows registry as we will see soon.

Registry

Ok, now we know how to get a pointer to the object that creates the object. We know how to tell it to create the object (IClassFactory) but there's another thing that is not clear. Who told Windows to load that DLL instead of another one? The answer is the registry.

When you call RegSrv32.exe (see above, how to register a COM DLL) you specify a file name (the DLLs). Regsrv32 first checks that those 4 functions exist in the DLL. If it is so then calls either DllRegisterServer or DllUnRegisterServer (depending on what you told it to do). That is all RegSvr32.exe does. It really doesn't care of what those functions do until they return 0.

When you create a COM DLL using Delphi, the VCL provides a default implementation for those 4 functions. Those simply store the GUIDs of the class factory, the GUIDs of the COM objects and associate the DLL name to them. This is really all that happens.

Take a look at the following screenshot:



Well, there's another small detail. The ProgID. That is just another redirection. A ProgID is the friendly name (a string) for your COM object such as MyLibrary.MyBusinessObject

You can create a COM object by using it's a GUID or a ProgID. When you use the ProgID, Windows will see to which GUID it is associated and the use that to retrieve all the rest of the information (DLL name). There's another section in the registry that starting from the ProgID will let you find the node I've just shown.

To recap

I hope that I succeeded in making it clear. There are many other details that should be mentioned, but this is meant to be an introduction, not a book on the subject. Here's again what happens in a step by step mode:

Your client application wants to create a COM object so does something like MyObject := CoMyObject.Create

The CoMyObject class (CoClass) tells Windows to return it a pointer to the class factory for the object

Windows scans into the registry and finds the name of the right DLL/EXE

Windows loads it and calls DllGetClassObject

The class factory returned by that call is finally used to create the object

Why is this so useful?

Well, for a variety of reasons.

By not being the one that directly creates the object, COM allows you to cross the boundaries of your local PC. A COM object doesn't necessarily live on the same pc of your client application but could be hosted by a remote server. This is what DCOM (Distributed COM) allows: remote creation and method invokation.

The other useful thing that comes out from this is COM+/MTS and object pooling/just in time activation

COM+ and just in time activation

When you use COM+ or MTS to register your COM object (which means, you create a COM+ application and you drop your DLL in it), you get some benefits in terms of scalability and performance that aren't available with regular in-process creation.

Take a look at the following screen-shot:



I created the "DSOAP Samples" COM+ application and I installed the Login and OrderManager objects into it.

So, what do I need to do in my client to create them now? Nothing. You would create them exactly the way you did before. Same code, no need to recompile or change anything. A few things will change anyways: scalability, creation time and execution speed (depending on what you do) will improve significantly.

The reasons for this lie in the just in time activation on one side, database connection pooling on the other. Actually let's also also add object pooling.

Just in time activation is that process trough which an object is put "asleep" until it is actually used. You create your object and let's say you keep it active without calling any method for 2 minutes. Wouldn't it be nice if something would just free it and call it only after 2 minutes? Well, this is what COM+ does for you transparently. This improves scalability of the server a lot. Instead of keeping memory utilized for nothing, COM+ is able to free the objects and whenever you will call them again it will recreate them for you and restore their state. All that is automatic.

The OrderManager class uses ADO inside to query the database. Before plugging the object in COM+, if we would have created, used and destroyed the object a hundred times, it would have taken let's say 100 seconds. This is because each time it would have had to reconnect to the database. By plugging OrdersManager in COM+ we now benefit from ADO connection pooling. COM+ will keep a list of active connections to the database for you and whenever a call is made, unless necessary, it will resuse one of the existing ones.

Finally (as you can see from the "Pooled" column on the right), COM+ objects can be pooled. There are a few things you need to do to make this happen but I just wanted to give you the idea that that is possible as well. The client wouldn't know the difference.

Conclusion

This is by no means an article that explains everything. There's far more that should be said but I think that I provided you with enough understanding on the subject to start digging into the subject by yourself.

Happy coding!

Nincsenek megjegyzések:

Megjegyzés küldése