Smart pointers and COM-server unloading. Part 4





5.00/5 (1 vote)
In the article I describe an approach to handle COM-server unloading issues using smart pointers.
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:
- Smart pointers and COM-server unloading. Part 1
- Smart pointers and COM-server unloading. Part 2
- Smart pointers and COM-server unloading. Part 3
- Smart pointers and COM-server unloading. Part 4
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
// Smart application pointer.
ISmartApplication = reference to function: ITestUnloadApplication;
// Create smart application pointer.
function CreateSmartApplication(
// Application.
const AApplication: ITestUnloadApplication): ISmartApplication;
implementation
...
type
// Smart application pointer implementation.
TSmartApplication = class(TInterfacedObject, ISmartApplication)
private
// Application interface reference.
FApplication: ITestUnloadApplication;
{ ISmartApplication }
function Invoke: ITestUnloadApplication; inline;
public
// Constructor.
constructor Create(
// Application interface reference.
const AApplication: ITestUnloadApplication);
{ TObject }
destructor Destroy; override;
end;
{ TSmartApplication }
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
// Registry of smart applications.
TSmartApplicationRegistry = class
private class var
// Registry instance.
FInstance: TSmartApplicationRegistry;
private
// Dictionary of applications.
FApplications: TDictionary<Pointer,String>;
// Get smart application pointer count.
function GetCount: Integer;
public
// Constructor.
constructor Create;
{ TObject }
destructor Destroy; override;
// Create instance.
class function Instance: TSmartApplicationRegistry;
// Destroy instance.
class procedure DestroyInstance;
// Register smart application.
procedure RegisterApplication(
// Pointer to smart application.
const APointer: Pointer);
// Unregister smart application.
procedure UnregisterApplication(
// Pointer to smart application.
const APointer: Pointer);
// Write to log alive smart applications.
procedure WriteToLog;
// Smart application pointer count.
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;
{ TSmartApplicationRegistry }
...
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
// Form registry class.
TFormRegistry = class
private class var
// Instance.
FInstance: TFormRegistry;
// Get form count.
function GetCount: Integer;
private
// Registered forms.
FForms: TList<TForm>;
public
// Constructor.
constructor Create;
{ TObject }
destructor Destroy; override;
// Create instance.
class function Instance: TFormRegistry;
// Destroy instance.
class procedure DestroyInstance;
// Register form.
procedure RegisterForm(
// Form.
const AForm: TForm);
// Unregister form.
procedure UnregisterForm(
// Form.
const AForm: TForm);
// Form count.
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
// Class which performs non-unloading checks.
TUnloadChecker = class
private class var
// Instance.
FInstance: TUnloadChecker;
private
// Invisible auxiliary window handle.
FWindowHandle: THandle;
// Auxiliary window procedure.
procedure WindowProcedure(
// Window message.
var AMessage: TMessage);
public
// Constructor.
constructor Create;
{ TObject }
destructor Destroy; override;
// Create instance.
class function Instance: TUnloadChecker;
// Destroy instance.
class procedure DestroyInstance;
// Post non-unloading check.
procedure PostCheck;
end;
implementation
...
{ TUnloadChecker }
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
// Test application form.
TTestForm = class(TForm)
...
private
// It is now smart pointer!
FApplication: ISmartApplication;
...
end;
implementation
...
{ TTestForm }
procedure TTestForm.FormCreate(Sender: TObject);
begin
// We now need to call this function to create smart pointer!
FApplication := CreateSmartApplication(TTestUnloadApplication.CreateFromFactory(
ComClassManager.GetFactoryFromClass(TTestUnloadApplication), nil));
end;
The changes I made to TTestLeak
class are the following:
type
// Test leak class.
TTestLeak = class
private
// It is now smart pointer!
FApplication: ISmartApplication;
...
end;
implementation
...
{ TTestLeak }
constructor TTestLeak.Create(const AApplication: ITestUnloadApplication);
begin
inherited Create;
// We now need to call this function to create smart pointer!
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.