Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Smart pointers and COM-server unloading. Part 4

0.00/5 (No votes)
21 Nov 2015 2  
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:

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here