Introduction
I will remind that in this series of articles I research cases, when COM-server application doesn't unload. In the previous part of the article I introduced the concept of smart pointers, which might be useful to keep track of application unloading cases. In this part of the article I will produce the complete example of application, which uses the concept of smart pointers to keep track of application unloading cases.
Index
The article consists of several parts. Here is the full list of the article parts:
Basic idea
Let me describe the basic idea first. I intend to replace all fields, which contain entry-point interface references with smart pointers to entry-point interface. These smart pointers would automatically register themselves in a global registry when created. And of course, they will clear themselves from this global registry on cleanup. Aside from this I will make each shown form to register itself in the global form registry. The form will clear itself from the global registry when it is hidden. And after eveey form destruction a check will be performed. This check will check if there are no visible application pointers and there are left smart application pointers in the global registry. In such situation checker will write all left smart application pointers creation stack to a log file. Using this log file application developer is likely to find out which entry-point references were not properly released.
Smart pointers registry
I have simplified smart pointer implementation and get rid of generics, which made things a bit harder. The result implementation looks like this:
type
ISmartApplication = reference to function: ITestUnloadApplication;
function CreateSmartApplication(
const AApplication: ITestUnloadApplication): ISmartApplication;
implementation
...
type
TSmartApplication = class(TInterfacedObject, ISmartApplication)
private
FApplication: ITestUnloadApplication;
function Invoke: ITestUnloadApplication; inline;
public
constructor Create(
const AApplication: ITestUnloadApplication);
destructor Destroy; override;
end;
function CreateSmartApplication(const AApplication: ITestUnloadApplication): ISmartApplication;
begin
if Assigned(AApplication) then
Result := TSmartApplication.Create(AApplication)
else
Result := nil;
end;
constructor TSmartApplication.Create(
const AApplication: ITestUnloadApplication);
begin
inherited Create;
FApplication := AApplication;
TSmartApplicationRegistry.Instance.RegisterApplication(Self);
end;
destructor TSmartApplication.Destroy;
begin
FApplication := nil;
TSmartApplicationRegistry.Instance.UnregisterApplication(Self);
inherited;
end;
function TSmartApplication.Invoke: ITestUnloadApplication;
begin
Result := FApplication;
end;
The CreateSmartApplication
function is added to make sure that old smart pointer to application interface equals nil
when pointed interface equals nil
. The key part of this implementation is that every one application smart pointer register itself in the global application pointer registry.
Smart application pointers registry is simple singleton object, which keeps track of smart application pointer creating and destroying. The singleton implementation is trivial so I will skip the most uninteresting parts of it. Every time registering smart pointer happens smart application registry class remembers call stack, which lead to smart application pointer creation (I use JCL to catch the call stack). The most interesting parts of smart application registry implementation follows:
type
TSmartApplicationRegistry = class
private class var
FInstance: TSmartApplicationRegistry;
private
FApplications: TDictionary<Pointer,String>;
function GetCount: Integer;
public
constructor Create;
destructor Destroy; override;
class function Instance: TSmartApplicationRegistry;
class procedure DestroyInstance;
procedure RegisterApplication(
const APointer: Pointer);
procedure UnregisterApplication(
const APointer: Pointer);
procedure WriteToLog;
property Count: Integer read GetCount;
end;
implementation
...
function GetCallStack: String;
var
JclStackInfoList: TJclStackInfoList;
StringList: TStringList;
begin
JclStackInfoList := JclCreateStackList(True, 3, Caller(0, False));
try
StringList := TStringList.Create;
try
JclStackInfoList.AddToStrings(StringList, True, False, True, True);
Result := StringList.Text;
finally
StringList.Free;
end;
finally
JclStackInfoList.Free;
end;
end;
...
procedure TSmartApplicationRegistry.WriteToLog;
const
LOG_FILE_NAME = 'logfile.log';
APPLICATION_NOT_UNLOADED_ERROR_MESSAGE = 'Application is not unloaded.';
APPLICATION_SMART_POINTER_MESSAGE = 'Application pointer (0x%s). Call stack: ' + sLineBreak + '%s';
var
LogFile: Text;
LogFileName: String;
SmartPointer: Pointer;
begin
LogFileName := LOG_FILE_NAME;
Assign(LogFile, LogFileName);
try
if FileExists(LogFileName) then
Append(LogFile)
else
Rewrite(LogFile);
if FApplications.Count > 0 then
begin
WriteLn(LogFile, APPLICATION_NOT_UNLOADED_ERROR_MESSAGE);
for SmartPointer in FApplications.Keys do
WriteLn(LogFile, Format(APPLICATION_SMART_POINTER_MESSAGE,
[IntToHex(Integer(SmartPointer), 8), FApplications.Items[SmartPointer]]));
end;
finally
CloseFile(LogFile);
end;
end;
Forms registry
Every form shown by the application is registered in global form registry. When form is hidden it clears itself from the global form registry:
procedure TTestForm.FormHide(Sender: TObject);
begin
TFormRegistry.Instance.UnregisterForm(Self);
end;
procedure TTestForm.FormShow(Sender: TObject);
begin
TFormRegistry.Instance.RegisterForm(Self);
end;
The implementation of form registry is trivial so I would show only interface part of it:
type
TFormRegistry = class
private class var
FInstance: TFormRegistry;
function GetCount: Integer;
private
FForms: TList<TForm>;
public
constructor Create;
destructor Destroy; override;
class function Instance: TFormRegistry;
class procedure DestroyInstance;
procedure RegisterForm(
const AForm: TForm);
procedure UnregisterForm(
const AForm: TForm);
property Count: Integer read GetCount;
end;
...
Unloading checker
I implemented a separate class TUnloadChecker
, which implements application non-unloading checks. It creates invisible auxiliary window. This hack is used because we initiate performing check in the form destructor, when not all resources are already cleaned up. So we need to execute some code after cleanup code is successfully finished. That's why I use PostMessage
to send user message to the auxiliary window and perform actual check inside this auxiliary window window procedure:
type
TUnloadChecker = class
private class var
FInstance: TUnloadChecker;
private
FWindowHandle: THandle;
procedure WindowProcedure(
var AMessage: TMessage);
public
constructor Create;
destructor Destroy; override;
class function Instance: TUnloadChecker;
class procedure DestroyInstance;
procedure PostCheck;
end;
implementation
...
procedure TUnloadChecker.PostCheck;
begin
PostMessage(FWindowHandle, WM_CHECK_APPLICATION_UNLOAD, 0, 0);
end;
procedure TUnloadChecker.WindowProcedure(var AMessage: TMessage);
begin
with AMessage do
begin
if Msg = WM_CHECK_APPLICATION_UNLOAD then
begin
if ComServer.StartMode <> smAutomation then
begin
if (TFormRegistry.Instance.Count = 0) and
(TSmartApplicationRegistry.Instance.Count > 0) then
TSmartApplicationRegistry.Instance.WriteToLog;
end;
Result := 0;
end
else
Result := DefWindowProc(FWindowHandle, AMessage.Msg, AMessage.WParam, AMessage.LParam);
end;
end;
As I said earlier the check procedure is initiated inside form destructor:
destructor TTestForm.Destroy;
begin
inherited;
TUnloadChecker.Instance.PostCheck;
end;
Last code changes
What left for us to do is just replace all fields, which reference entry-point interface, with smart pointers and replace assignment to these fields with function, which will create smart pointer from entry-point interface. The rest parts of code may be left unchanged.
The changes I made to TTestForm
class are the following:
type
TTestForm = class(TForm)
...
private
FApplication: ISmartApplication;
...
end;
implementation
...
procedure TTestForm.FormCreate(Sender: TObject);
begin
FApplication := CreateSmartApplication(TTestUnloadApplication.CreateFromFactory(
ComClassManager.GetFactoryFromClass(TTestUnloadApplication), nil));
end;
The changes I made to TTestLeak
class are the following:
type
TTestLeak = class
private
FApplication: ISmartApplication;
...
end;
implementation
...
constructor TTestLeak.Create(const AApplication: ITestUnloadApplication);
begin
inherited Create;
FApplication := CreateSmartApplication(AApplication);
end;
Profit
Now I can start TestUnloadApp.exe
, click DoLeak
button and after the application form is closed the following record is made in the log-file (only the part of log is shown):
Application is not unloaded.
Application pointer (0x02AA2EA0). Call stack:
... SmartApplicationRegistry.GetCallStack$qqrv (Line 57...)
... SmartApplicationRegistry.TSmartApplicationRegistry.RegisterApplication$qqrpxv (Line 108...)
... SmartApplication.TSmartApplication... (Line 55...)
... SmartApplication.CreateSmartApplication... (Line 43...)
... TestLeak.TTestLeak... (Line 31...)
... TestUnloadApplication.TTestUnloadApplication.DoLeak... (Line 57...)
... TestFm.TTestForm.DoLeakButtonClick (Line 47...)
...
As you can see we can setup our application to create such a log-file. In the log file we will see every case when application is not unloaded when we expected it to unload. And in every such case we will see the list of entry-point interface references which were not released at the moment when application's last visible window was closed.