The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 4,262 other subscribers

Links to debugging COM stuff for Office Automation in Delphi

Posted by jpluimers on 2019/11/12

From a long time ago, but recently I needed them again.

Note the .NET side is much much harder Revisited from the .NET side: Why doesn’t WINWORD.EXE quit after Closing the document from Delphi? (via: Stack Overflow).

[WayBack] Why doesn’t WINWORD.EXE quit after Closing the document from Delphi? – Stack Overflow

Q

I managed to distill one of the underlying issues rooted in my question How to trace _AddRef / _Release calls for OLE Automation objects in the unit below.

I’ll answer this answer too, just in case anyone else bumps into this.

The question: with the below code, why doesn’t WINWORD.EXE always quit (sometimes it does quit).

The unit can probably be trimmed down even more.

unit Unit2;

interface

uses
  Winapi.Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls,
  WordXP;

type
  TForm2 = class(TForm)
    WordXPFailsToQuitButton: TButton;
    procedure WordXPFailsToQuitButtonClick(Sender: TObject);
  private
    FWordApplication: TWordApplication;
  strict protected
    function GetWordApplication: TWordApplication; virtual;
    function GetWordApplication_Documents: Documents; virtual;
    procedure WordApplication_DocumentBeforeClose(ASender: TObject; const Doc: _Document; var Cancel: WordBool); virtual;
    procedure WordApplication_Quit(Sender: TObject); virtual;
    property WordApplication: TWordApplication read GetWordApplication;
    property WordApplication_Documents: Documents read GetWordApplication_Documents;
  end;

var
  Form2: TForm2;

implementation

uses
  Vcl.OleServer;

{$R *.dfm}

function TForm2.GetWordApplication: TWordApplication;
begin
  if not Assigned(FWordApplication) then
  begin
    FWordApplication := TWordApplication.Create(nil);

    FWordApplication.AutoConnect := False;
    FWordApplication.AutoQuit := False;
    FWordApplication.ConnectKind := ckNewInstance;
    FWordApplication.OnDocumentBeforeClose := WordApplication_DocumentBeforeClose;
    FWordApplication.OnQuit := WordApplication_Quit;
    FWordApplication.Connect;
  end;
  Result := FWordApplication;
end;

function TForm2.GetWordApplication_Documents: Documents;
begin
  Result := WordApplication.Documents;
  if not Assigned(Result) then
    raise EAccessViolation.Create('WordApplication.Documents');
end;

procedure TForm2.WordXPFailsToQuitButtonClick(Sender: TObject);
begin
  try
    WordApplication_Documents.Add(EmptyParam, EmptyParam, EmptyParam, EmptyParam);
    WordApplication.Visible := True;
    WordApplication.ActiveDocument.Close(False, EmptyParam, EmptyParam);
  finally
    WordApplication.OnQuit := nil;
    WordApplication.OnDocumentBeforeClose := nil;
    WordApplication.AutoQuit := True;
    WordApplication.Disconnect;
    WordApplication.Free;
    FWordApplication := nil;
  end;
end;

procedure TForm2.WordApplication_DocumentBeforeClose(ASender: TObject; const Doc: _Document; var Cancel: WordBool);
begin
  FWordApplication.Disconnect;
end;

procedure TForm2.WordApplication_Quit(Sender: TObject);
begin
  FWordApplication.Disconnect;
end;

end.

Answer part 1:

Comment out the disconnect in the below event:

procedure TForm2.WordApplication_DocumentBeforeClose(ASender: TObject; const Doc: _Document; var Cancel: WordBool);
begin
//  FWordApplication.Disconnect;
end;

The event will be called during the DocumentClose(…) method, then disconnect and delete the OLE interface from the FWordApplication instance.

I have not yet figured out which reference is dangling, but this effectively keeps WINWORD.EXE alive most of the times.

Answer part 2:

Sometimes WINWORD.EXE does quit because toe WordApplication_DocumentBeforeClose event is not called. The reason is that the code runs so fast that Word is not fully initialized yet to perform the event.

[WayBack] delphi – How to trace _AddRef / _Release calls for OLE Automation objects – Stack Overflow

Q

(Delphi XE2 update 4)

I’m trying to get a big Microsoft Word OLE automation unit I inherited (based on the early bindingTWordApplication and interfaces from the WordXP/Word2010 units) to close WINWORD.EXE when all references have been released.

So far, it looks like I did catch a couple of reference leaks: most references are properties or local variables.

Some usage scenario’s however still keep WINWORD.EXE open.

A few of the fixes indicate I should favour local variables in stead of chains from

procedure TOffice_10_XP_WordInterface.AddDocument;
var
  WordApplicationDocuments: Documents;
begin
  WordApplication_Documents.Add(EmptyParam, EmptyParam, EmptyParam, EmptyParam);
end;

to

procedure TOffice_10_XP_WordInterface.AddDocument;
var
  WordApplicationDocuments: Documents;
begin
  WordApplicationDocuments := WordApplication_Documents;
  WordApplicationDocuments.Add(EmptyParam, EmptyParam, EmptyParam, EmptyParam);
end;

based on a WordApplication_Documents property that calls this function:

function TOffice_10_XP_WordInterface.GetWordApplication_Documents: Documents;
begin
  Result := WordApplicationReference.Documents;
  if not Assigned(Result) then
    raise EAccessViolation.Create('Documents');
end;

The properties are there to make the EAccessViolation messages more readable than the $C0000005 errors you get in the debugger.

I’m wondering about generic (since I’ll probably need this for other automation projects as well) ways to monitor _AddRef and _Release calls.

I did take a look at these links:

A

A tedious way that gets you going is this:

Put breakpoints of all _AddRef and _Release calls in the Delphi System unit that are not inside TInterfacedObject specific methods.

Now eliminate (using conditional expressions) all the interfaces that are not part of Delphi (EAXcontains the vTable pointer for each interface).

  1. Start debugging your application with a simple run/exit without doing much actual functionality:
  2. Before tracing in each call, make a note of the EAX value.
  3. If you end up in any of these methods: NopReleaseNopAddrefTInterfacedObject._AddRefTInterfacedObject._ReleaseMemReleaseMemAddRef then add the EAX value to a conditional breakpoint instruction for all of the breakpoints like below.

Example conditional breakpoint expression for my application:

(EAX <> $401E58) and (EAX <> $54AD14) and (EAX <> $4A7C88) ...

This method has a lot of drawbacks, but it gets you going.

Drawbacks:

  • There are limitations on the length of the conditional breakpoint expression. Which means that if you continue to add and (EXA <> $xxxx) portions, the debugger will ignore those without indicating a warning.
  • You loose the setting if you exit Delphi without saving your desktop
  • It takes a lot of time to setup

–jeroen

2 Responses to “Links to debugging COM stuff for Office Automation in Delphi”

  1. P. Schmitz said

    Didn’t look into your solution in depth, but a lot of similiar issues we used to have with Word automation a couple years back suddenly got resolved when I implemented a IMessageFilter. In a nutshell: https://stackoverflow.com/questions/46913922/how-to-extend-existing-interface-imessagefilter-with-tinterfacedobject

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.