Understanding and correcting interface reference leaks in Delphi’s Vcl.OleCtrls.pas

Update 21 Sep 2015: This bug has been fixed in Delphi 10 Seattle.

I have spent quite some time recently tracing a memory leak in a Delphi application.  It is quite a large application and makes a lot of use of embedded MSHTML (TWebBrowser and TEmbeddedWB) controls for presentation.  Somehow, somewhere, we were leaking memory: users were reporting that after a few hours of use, the application slowed down, and checking Task Manager certainly reflected excessive memory usage.

The normal procedure to reproduce the memory leak was followed, including using tools such as AQtime and other debug logging tools.  However, no leaks were detected using these tools, although we could see the memory usage increasing in Task Manager on our test machines.  This suggested the memory we were leaking was not allocated by Delphi code: i.e. it was Windows or a 3rd party DLL.  This doesn’t mean, of course, that it wasn’t our fault — just that it wasn’t allocated directly from Delphi source!

At this point, I was asked to trace this issue further.  I ran Performance Monitor with a couple of key counters: Handle Count and Working Set.  Running the test in question (involving opening and closing a window with an embedded web browser control) showed a gradual increase in both handle count and working set size.  However, Performance Monitor unfortunately does not include a user handle (i.e. window handles) counter.  It was when I noticed in Process Explorer that user handles were also increasing that I got my first break in tracing the cause.

It turned out that the embedded web browser window was not always being destroyed when its parent window was.  This window had the class name “Internet Explorer_Server”.

With a bit more tracing, I found that the trigger was the Document or Application properties.  If the Document property of the TWebBrowser control was ever referenced, the window was never destroyed (note, there are also some other properties that trigger the same behaviour — look for properties returning type IDispatch).

This set me to researching the Document property.  It looks like this:

property Document: IDispatch index 203 read GetIDispatchProp;

Looking at GetIDispatchProp in Vcl.OleCtrls.pas, we see the following code:

function TOleControl.GetIDispatchProp(Index: Integer): IDispatch;
var
  Temp: TVarData;
begin
  GetProperty(Index, Temp);
  Result := IDispatch(Temp.VDispatch);
end;

And here some alarm bells go off.  Delphi, rather nicely, manages all the reference counting on interfaces.  This works pretty smoothly, until you trick the compiler by casting other types to interfaces.  Here the code in question is triggering an invisible call to IntfCopy in the line:

Result := IDispatch(Temp.VDispatch);

The IntfCopy function internally calls  _AddRef on the object, but because of the cast to IDispatch from a Pointer, this new reference is never released.  The fix is to change the function (and the similar GetIUnknownProp function) to:

function TOleControl.GetIDispatchProp(Index: Integer): IDispatch;
var
  Temp: TVarData;
begin
  GetProperty(Index, Temp);
  Pointer(Result) := Temp.VDispatch;
end;

function TOleControl.GetIUnknownProp(Index: Integer): IUnknown;
var
  Temp: TVarData;
begin
  GetProperty(Index, Temp);
  Pointer(Result) := Temp.VUnknown;
end;

By casting this way, we avoid the call to IntfCopy and hence the call to _AddRef.  Alternatively, you could have called _Release on Result, but this would require another test to ensure that Result wasn’t nil (and also, of course, redundant _AddRef and _Release calls).

It turns out that this problem was identified way back in 1999 and the workaround has been commonly referenced since then.  So I am not taking credit for the fix here!  And yet it is still unresolved in Delphi XE2 — and so still causing trouble for Delphi programmers today!  There are no clear references to the problem in QualityCentral that I could find (that’s about to change!)

But don’t stop reading yet!

“Now my app crashes”

There are frequent complaints online that this fix results in crashes.  This is because other developers have engineered fixes to this reference leak in places where these GetIDispatchProp and/or GetIUnknownProp calls are made, rather than where the problem actually occurs.  I have found this in TEmbeddedWB.  TEmbeddedWB is a web browser hosting component that takes up where TWebBrowser leaves off, and it does fix a lot of the limitations of TWebBrowser.

But here are the places in the TEmbeddedWB source that you’ll need to “unfix” once you fix the root problem in Vcl.OleCtrls.pas:

EmbeddedWB.pas [2603]: (procedure TEmbeddedWB.SetUserAgentInt): Delete _Release call
EwbCore.pas [1367]: (procedure TCustomEmbeddedWB.SetDesignMode): Delete _Release call
EwbCore.pas [1404]: (procedure TCustomEmbeddedWB.SetDownloadOptions): Delete _Release call
EwbCore.pas [1468]: (procedure TCustomEmbeddedWB.SetUserInterfaceOptions): Delete _Release call
EwbTools.pas [1015]: (function GetBmpFromBrowser): Delete _Release call
EwbTools.pas [3164]: (function InvokeCMD): Delete _Release call

Note, you’ll also see an experimental fix buried — and disabled — in the TEmbeddedWB code (but without fixing the lines above), and without a whole lot of documentation as to why!

I have created a Quality Central report, along with a test case and example fix.  I also checked the Delphi VCL source, and about 30 other components that we use, and found no more calls to _Release relating to this issue.

14 thoughts on “Understanding and correcting interface reference leaks in Delphi’s Vcl.OleCtrls.pas

  1. Hello!

    Can you upload a modified version of EmbeddedWB ? We have the same problem as you did. Five instances of TEmbeddedWB use nearly 1500 Handles in the TaskManager. And 600~ after Free call.

    1. Hi David,

      Our version of EmbeddedWB has other fixes and changes specific to our use. The six lines that need changing are all itemised above — it’s just a matter of commenting out the _Release call in each case. Those line numbers are approximate, depending on the version of EmbeddedWB that you have.

  2. Hi,

    I’ve been struggling with this bug for about 8 years and NEVER found any solution that really fixed. My workaround is to close and reopen my app after sometime.

    I tried your implementation, but i could not see any improvements in memory usage (after some time my app can easily use more than 2GB of RAM) and also, now when i close the app, i get an Access Violation on MSHTML.DLL…

    Anyway, thanks for your post 🙂

    1. Pedro, thanks for reading. Because you are getting a crash on exit, it sounds like your version of EmbeddedWB (if that’s what you are using) is still doing its own workaround for the bug — so you are calling _Release twice on an interface, thus causing an access violation in MSHTML when it goes to release its own interface references. If you are using EmbeddedWB, it has changed a bit since I wrote this post — you may need to look for additional _Release calls and comment them out.

        1. I’m not sure about the cause of the leak ; my hope was it would be fixed after implementing the fix you provided. But even after doing this, the app keeps using massive amount of memory after some time. I will check the link you provided, many thanks !

        1. Hi Pedro,

          Where did you fix the code in Embedded files and what did you change to prevent programs from crashing.
          I did change all the source code according to the suggestions made above. In Vcl.OleCtrls.pas and the other mentioned files. But unfortunately it was not sufficient

          Regards, Daan

          1. Have you searched for all the _Release calls in the EmbeddedWB source? Note that recent versions of Delphi have corrected this bug in Vcl.OleCtrls.pas.

  3. Thanks, will try FastMM. I’ve been struggling with this bug for such long time, that i’ve learned to live with it : what i do in my app is to close and re-open it periodically. Not a very clever workaround, but it’s the one i could get 😉

    1. We already tried to user FastMM. It does not report any memory leak becouse this memory is not allocated by the Delphi, but by the Internet Explorer engine.

      1. Yes; FastMM won’t report on this specific leak, but it does help to trace other leaks that may be occurring. Again, remember that this leak has been fixed in recent versions of Delphi.

Leave a Reply

Your email address will not be published. Required fields are marked *