Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / Delphi

Smart pointers and COM-server unloading. Part 1

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
21 Nov 2015CPOL5 min read 6.9K   73   4  
In the article I describe an approach to handle COM-server unloading issues using smart pointers.

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:

Pascal
  ITestUnloadApplication = interface(IDispatch)
    ['{59DC4CB0-33EE-4B68-8209-C1CEC8E341A5}']
    procedure DoSomething; safecall;
    procedure DoLeak; safecall;
    ...
  end;

Entry-point interface implementation TTestUnloadApplication looks like this:

Pascal
type
  // Implementstion of COM-server entry object.
  TTestUnloadApplication = class(TAutoObject, ITestUnloadApplication)
  private
    { ITestUnloadApplication }
    procedure DoSomething; safecall;
    procedure DoLeak; safecall;
    ...
  public
    // Constructors.
    constructor Create;
    constructor CreateFromFactory(Factory: TComObjectFactory;
                                  const Controller: IUnknown);
    { TObject }
    destructor Destroy; override;
  end;

implementation
...

{ TTestUnloadApplication }

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:

Pascal
type
  // Application factory.
  TTestUnloadApplicationFactory = class(TAutoObjectFactory)
  private
    // Handle last object release.
    procedure HandleLastRelease(
      // Shutdown flag.
      var Shutdown: Boolean);
    { TComObjectFactory }
    function CreateComObject(const Controller: IUnknown): TComObject; override;
  public
    // Constructor.
    constructor Create(ComServer: TComServerObject; AutoClass: TAutoClass;
      const ClassID: TGUID; Instancing: TClassInstancing;
      ThreadingModel: TThreadingModel = tmSingle);
    { TObject }
    destructor Destroy; override;
  end;

implementation
...

{ TTestUnloadApplicationFactory }

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:

Pascal
type
  // Test leak class.
  TTestLeak = class
  private
    // Application.
    FApplication: ITestUnloadApplication;
  public
    // Constructor.
    constructor Create(
      // Application.
      const AApplication: ITestUnloadApplication);
    { TObject }
    destructor Destroy; override;
  end;

implementation
...

{ TTestLeak }

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:

Pascal
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:

BAT
TestUnloadApp.exe /regserver

Now we can reproduce the leak with the following VBS-script:

VBScript
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer DIRECTUM
Russian Federation Russian Federation
I am software developer currently working at DIRECTUM company. We are developing ECM system called DIRECTUM. My core work is connected with designing, programming and debugging Delphi applications, which are the platform for our system. For database layer we are using Microsoft SQL Server.

Comments and Discussions

 
-- There are no messages in this forum --