Introduction
COM technology is a great thing, which allows transparent application interaction on Windows platform. But it has its drawbacks, one of which is increasing application development complexity (by the way Delphi makes many things much simpler).
One basic thing about COM-server application is the fact that COM-server must be present in memory while it has active COM-clients, which keep its entry-point interface references. When last entry-point interface reference is released COM-server can unload. But it happens that sometimes entry-point interface references are misuseed inside the COM-server application itself. So COM-server thinks that entry-point interfaces are still referenced when all references to them are actually from the COM-server itself. It makes COM-server stay in memory forever until somebody force it to terminate.
The COM-server which can be used as usual application with user interface is a worse case, because internally it likely to use entry-point interfaces, even when the application is launched not as a COM-server. And the problem may happen after a long user work session.
In the first place you need to keep track of such unloading cases. Sometimes users complain about them and sometimes not. The bad news is that you don't always have stable step sequence which allow you to reproduce cases in development environment. In the article I would like to tell about some techique which can make the process of investigation of COM-server unload cases much easier.
Index
The article consists of several parts. Here is the full list of the article parts:
Background
In the article I would not cover the details of COM technology. So some basic knowledge is assumed. The topic is likely to be of interest by those who are experienced with the COM technology. For the beginners there are quite good books and materials out there.
Non-unloading server
To illustrate the problem I have implemented a simple COM-server application.
It has entry-point interface ITestUnloadApplication
, which looks like this:
ITestUnloadApplication = interface(IDispatch)
['{59DC4CB0-33EE-4B68-8209-C1CEC8E341A5}']
procedure DoSomething; safecall;
procedure DoLeak; safecall;
...
end;
Entry-point interface implementation TTestUnloadApplication
looks like this:
type
TTestUnloadApplication = class(TAutoObject, ITestUnloadApplication)
private
procedure DoSomething; safecall;
procedure DoLeak; safecall;
...
public
constructor Create;
constructor CreateFromFactory(Factory: TComObjectFactory;
const Controller: IUnknown);
destructor Destroy; override;
end;
implementation
...
constructor TTestUnloadApplication.Create;
begin
inherited;
MessageBox(0,
'TTestUnloadApplication.Create',
'Test Unload Application',
MB_OK or MB_SYSTEMMODAL);
end;
constructor TTestUnloadApplication.CreateFromFactory(Factory: TComObjectFactory;
const Controller: IInterface);
begin
MessageBox(0,
'TTestUnloadApplication.CreateFromFactory',
'Test Unload Application',
MB_OK or MB_SYSTEMMODAL);
inherited;
end;
destructor TTestUnloadApplication.Destroy;
begin
MessageBox(0,
'TTestUnloadApplication.Destroy',
'Test Unload Application',
MB_OK or MB_SYSTEMMODAL);
inherited;
end;
procedure TTestUnloadApplication.DoLeak;
var
TestLeak: TTestLeak;
begin
TestLeak := TTestLeak.Create(Self as ITestUnloadApplication);
end;
procedure TTestUnloadApplication.DoSomething;
begin
MessageBox(0,
'TTestUnloadApplication.DoSomething',
'Test Unload Application',
MB_OK or MB_SYSTEMMODAL);
end;
initialization
TTestUnloadApplicationFactory.Create(ComServer, TTestUnloadApplication, Class_TestUnloadApplication,
ciMultiInstance, tmApartment);
As you see there are two methods available to COM-clients:
DoSomething
method just shows a message box
DoLeak
method creates TTestLeak
object
DoLeak
method creates a memory leak, because object destructor is not called. Aside from memory leak this method increases entry-point interface reference count, because entry-point interface reference is supplied as TTestLeak
constructor argument and it is later stored in TTestLeak
object field as you will see later.
I have implemented custom COM-object factory TTestUnloadApplicationFactory
to make sure my overriden CreateFromFactory
constructor is called when COM-client creates entry-point instance. Also I implemented OnLastRelease
handler to ensure application shutdown after last object release even in cases when application is not run as COM-server (I would discuss this approach later in the article). So the factory is implemented like this:
type
TTestUnloadApplicationFactory = class(TAutoObjectFactory)
private
procedure HandleLastRelease(
var Shutdown: Boolean);
function CreateComObject(const Controller: IUnknown): TComObject; override;
public
constructor Create(ComServer: TComServerObject; AutoClass: TAutoClass;
const ClassID: TGUID; Instancing: TClassInstancing;
ThreadingModel: TThreadingModel = tmSingle);
destructor Destroy; override;
end;
implementation
...
constructor TTestUnloadApplicationFactory.Create(ComServer: TComServerObject;
AutoClass: TAutoClass; const ClassID: TGUID; Instancing: TClassInstancing;
ThreadingModel: TThreadingModel);
begin
inherited Create(ComServer, AutoClass, ClassID, Instancing, ThreadingModel);
if ComServer is TComServer then
(ComServer as TComServer).OnLastRelease := HandleLastRelease;
end;
destructor TTestUnloadApplicationFactory.Destroy;
begin
if ComServer is TComServer then
(ComServer as TComServer).OnLastRelease := nil;
inherited;
end;
function TTestUnloadApplicationFactory.CreateComObject(
const Controller: IInterface): TComObject;
begin
Result := TTestUnloadApplication.CreateFromFactory(Self, Controller);
end;
procedure TTestUnloadApplicationFactory.HandleLastRelease(
var Shutdown: Boolean);
begin
Shutdown := True;
end;
The TTestLeak
object just keeps entry-point interface reference like this:
type
TTestLeak = class
private
FApplication: ITestUnloadApplication;
public
constructor Create(
const AApplication: ITestUnloadApplication);
destructor Destroy; override;
end;
implementation
...
constructor TTestLeak.Create(const AApplication: ITestUnloadApplication);
begin
inherited Create;
FApplication := AApplication;
end;
destructor TTestLeak.Destroy;
begin
FApplication := nil;
inherited;
end;
Application contains main form, which contains no logic. The form is just created on application startup to ensure we can enter application message loop. Main form is invisible in both COM-server and GUI start modes. As you will see later we need such an invisible main form to ensure that application will not shutdown when we close its forms (especially when the application is run as COM-server and COM-clients still need to use it).
Application contains a test form TTestForm
also. This form is displayed when application is launched not as COM-server. Form creates entry-point interface reference when it is created and releases this reference when it is destroyed. Form conatins two buttons:
DoSomething
just calls ITestUnloadApplication.DoSomething
method
DoLeak
button just calls ITestUnloadApplication.DoLeak
method
On startup application shows information message box, creates invisible main application form and if not run as COM-server creates TTestForm
application form. Startup code looks like this:
begin
MessageBox(0, 'Application launched', 'Test Unload Application', MB_OK or MB_SYSTEMMODAL);
Application.Initialize;
Application.ShowMainForm := False;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainForm, MainForm);
if ComServer.StartMode <> smAutomation then
begin
Application.CreateForm(TTestForm, TestForm);
TestForm.Show;
end;
Application.Run;
end.
To register a COM-server we just need to execute it with /regserver command line switch:
TestUnloadApp.exe /regserver
Now we can reproduce the leak with the following VBS-script:
Dim TestUnloadApp
Set TestUnloadApp = WScript.CreateObject("TestUnloadApp.TestUnloadApplication")
Call TestUnloadApp.DoSomething
Call TestUnloadApp.DoLeak
When you execute the script you the following things happen:
- the message box appears, when application is launched
- the message box appears, when entry-point interface instance created
- the message box appears, when
DoSomething
method is called
- no message boxes are shown after that
And when script host application is terminated, TestUnloadApp.exe
will still be in memory. As you can see TTestUnloadApplication
class instance is created through COM mechanisms but is never destroyed thereafter.
There is another approach, which leads to application not unloading. You can start TestUnloadApp.exe
manually and press DoLeak
button and try to close the one visible application window. The following things happen when you do that:
- the message box appears when applicatoin is launched
- the message box appears, when entry-point interface instance created
TTestForm
application form is shown
- no message boxes appear when you click
DoLeak
form button
- no message boxes appear when you click
Close
form button
- no message boxes are show after that
So we have reproduced unloading case which is the point of interest here. In our situation it is quite easy to find the reason of unloading and fix the application. But in real world code bases the task might be more difficult.
What's next
In this part of the article I explained the problem, which I want to solve. So you now see that it is quite easy to run into application non-unloading cases when implementing COM-server application.
The next step is to understand the depth of application unloading behavior.
Our final goal is to produce some instrument which will make solving application unloading issues much easier than it is from the box.