Quantcast
Channel: Windows Server 2012 – The Wiert Corner – irregular stream of stuff
Viewing all articles
Browse latest Browse all 53

A year ago on Telegram: “Do I need to use GarbageCollectAtoms in Delphi? I used it in delphi 7, but I dont know what is benefit. 😐”

$
0
0

Last week I found out that I had some Windows ATOM issues before, but this beats them easily was still a draft in stead if in the blog queue.

I got reminded to it by someone asking on Telegram about

“Do I need to use GarbageCollectAtoms in Delphi? I used it in delphi 7, but I dont know what is benefit. 😐”.

The short answer is: yes, if your Delphi application does terminate in a way that the Controls unit cannot cleanly unload (and cannot free the Windows atoms) or leaks Windows atoms in a different way. I have been in that situation and that’s why I wrote the above blog post that got published in 2016.

The longer answer is likely no, both the Windows atom and registered Windows message table share a heap and that registered VCL Windows message leaking bug got fixed some 10 years ago in Delphi XE2, see:

Note that there is still a ton of applications built in older versions without Andreas Hausladen’s fix for those older versions (the fix used to be at Wayback: andy.jgknet.de, but is now at Archive.is Downloads – Andy’s Blog and Tools as [Wayback] ControlsAtomFix1.7z).

Let’s explain the long answer

Back in 2016, I already hinted I would like to give more information in Some notes and links: when a filled ATOM table is not caused by your Delphi app.

For non-Delphi applications, it was excessive registration of Windows messages through RegisterWindowMessage or leaking atoms by not deleting them.

Both make use of the atom tables (atoms directly, registered Windows messages indirectly, just like clipboard formats also use them indirectly) as per [Wayback/Archive.is] About Atom Tables – Win32 apps | Microsoft Docs; look for “Atom table size“.

For Delphi applications, the most common problem was an excessive registration in the global Windows message table by the InitControls method of the Controls (for older Delphi versions), or Vcl.Controls (for newer) unit: each Delphi application instance would register a new message there.

Before Delphi XE2, the relevant portion of the code used to be this:

var
  WindowAtom: TAtom;
  ControlAtom: TAtom;
  WindowAtomString: string;
  ControlAtomString: string;
  RM_GetObjectInstance: DWORD;  // registered window message
...
procedure InitControls;
...
  WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
  WindowAtom := GlobalAddAtom(PChar(WindowAtomString));
  ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
  ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
  RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
...

As of Delphi XE2, it is this:

var
  WindowAtom: TAtom;
  ControlAtom: TAtom;
  WindowAtomString: string;
  ControlAtomString: string;
  RM_GetObjectInstance: DWORD;  // registered window message
...
procedure InitControls;
...
  WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
  WindowAtom := GlobalAddAtom(PChar(WindowAtomString));
  ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
  ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
  RM_GetObjectInstance := RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance'));  // Do not localize
...

The bold italic bits are different, and this is why:

  • The old code assumed that the Windows message ID for calling RM_GetObjectInstance had to be unique for each process. It doesn’t: the Windows message ID can be shared across Delphi application instances as each of them will handled it individually because the only place it is used is the implementation of this method:
    function ObjectFromHWnd(Handle: HWnd): TWinControl;

    The result of that function is only valid within the current process, so there is no need for the RM_GetObjectInstance to be different for each Delphi process.

What does the fix in ControlsAtomFix1.7z do?

That 7z file contains unit ControlsAtomFix which for Delphi versions before XE2 needs to be in the uses list before the Controls unit and supports Delphi versions 6 through XE.

It hooks the RegisterWindowMessage Windows API method (actually [Wayback/Archive.is] RegisterWindowMessageA function (winuser.h) – Win32 apps | Microsoft Docs for non-unicode Delphi before Delphi 2009 and [Wayback/Archive.is] RegisterWindowMessageW function (winuser.h) – Win32 apps | Microsoft Docs for Delphi 2009 and up).

Inside the hooked method, it checks if the calling parameters are consistent with RegisterWindowMessage(PChar(ControlAtomString)). If so, it returns the result for RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance')), otherwise it calls the original method.

That way, Delphi versions 6 through XE will behave like Delphi XE2 and up.

What does GarbageCollectAtoms do?

Back to “GarbageCollectAtoms in Delphi? I used it in delphi 7, but I dont know what is benefit. 😐”. It is part of the answers to question [Wayback/Archive.is] delphi – System Error. Code: 8. Not enough storage is available to process this command – Stack Overflow of which each has a distinctive edge:

  1. [Wayback/Archive.is] about the GlobalDeleteAtom method where [Wayback/Archive.is] Christian explains the workaround of manually modifying the Controls unit with
    RM_GetObjectInstance := RegisterWindowMessage('RM_GetObjectInstance');

    ..and explaining the working of his GlobalDeleteAtom method.

  2. [Wayback/Archive.is] https://stackoverflow.com/users/62123/steve-black where user [Wayback/Archive.is] Steve Black explains how to change the second number in the SharedSection parameter of csrss.exe (the Windows Client/Server Runtime Subsystem) inside the Windows value data of the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems registry key. It is further documented in these Microsoft articles:
    • [Wayback/Archive.is] Microsoft KB Archive/126962 – BetaArchive Wiki covering Windows NT 3.1, NT 3.5/3.51, NT 4.0, 2000, XP and 2003 Server:

      The first SharedSection value (1024) defines the heap size common to all desktops. This includes the global handle table (Window handles are unique machine wide) and shared system settings (such as SystemMetrics). It is unlikely you would ever need to change this value.

      The second SharedSection value (3072) controls the size of the desktop heap (used for Windows objects). This static value is used to prevent ill- behaved applications from consuming too many resources. Because the desktop heap is mapped into each process’ address space, this value should not be set to an arbitrarily high value (as it would decrease performance), but should only be increased sufficiently to allow all the desired applications to run.

    • [Wayback/Archive.is] User32.dll or Kernel32.dll does not initialize – Application Developer | Microsoft Docs (formerly KB184802) covering Windows Vista through Windows Server 2012 R2:

      The first SharedSection value (1024) is the shared heap size common to all desktops. This includes the global handle table. This table holds handles to windows, menus, icons, cursors, and so on, and shared system settings. It is unlikely that you would ever have to change this value.

      The second SharedSection value is the size of the desktop heap for each desktop that is associated with the interactive window station WinSta0. User objects such as hooks, menus, strings, and windows consume memory in this desktop heap. It is unlikely that you would ever have to change this value.

    • [Wayback/Archive.is] Desktop heap limitation causes out of memory error – Windows Server | Microsoft Docs (formerly KB947246) covering Windows 7 and Windows Server 2012 R2:
      • The second value of the SharedSection registry entry is the size of the desktop heap for each desktop that is associated with an interactive window station. The heap is required for each desktop that is created in the interactive window station (WinSta0). The value is in kilobytes (KB).
      • The third SharedSection value is the size of the desktop heap for each desktop that is associated with a non-interactive window station. The value is in kilobytes (KB).
      • We don’t recommend that you set a value that is over 20480 KB for the second SharedSection value.
    • [Wayback/Archive.is] CreateDesktopExA function (winuser.h) – Win32 apps | Microsoft Docs
      • The first SharedSection value is the size of the shared heap common to all desktops, in kilobytes.
      • The second SharedSection value is the size of the desktop heap needed for each desktop that is created in the interactive window station, WinSta0, in kilobytes.
      • The third SharedSection value is the size of the desktop heap needed for each desktop that is created in a noninteractive window station, in kilobytes.

    The second SharedSection value has grown over time (with one step back in Windows 3.5):

    • 512 (Windows NT 3.5)
    • 3072 (Windows NT 3.1, NT 4.0, 2000, XP and Vista)
    • 12288 (x86 editions of Windows Vista SP1, 7, 8, 8.1, and newer x86 Windows versions)
    • 20480 (x64 editions of Windows Vista, 7, 8, 8.1, Server 2008, Server 2008 R2, Server 2012, and Server 2012 R2, and newer x64 Windows versions)
  3. [Wayback/Archive.is] delphi – System Error. Code: 8. Not enough storage is available to process this command – Stack Overflow where [Wayback/Archive.is] Jordi Corbilla explains the bugs filed in QC (which have now been fixed), the structure of the atom and message names registered by the Controls unit, and his [Wayback/Archive.is] Global Atom Monitor GUI application on GitHub that allows you to visually watch the global atom table. The GlobalDeleteAtom method is based on this code. I amended that post with the fix for Delphi 6-XE by Andreas Hausladen.

I forked the Global Atom Monitor into [Wayback/Archive] jpluimers/atom-table-monitor: Automatically exported from code.google.com/p/atom-table-monitor back in 2015 because the original repository did not show any activity for a long time.

There I added a console application which displays all the User Atoms including their names, see the trick in [Wayback/Archive] atom-table-monitor/ATOMScannerConsoleApplicationUnit.pas at master · jpluimers/atom-table-monitor of [Wayback/Archive] atom-table-monitor/ATOMScannerConsole at master · jpluimers/atom-table-monitor commit [Wayback/Archive] First try at console version of ATOM scanner; Delphi 2007 version of … · jpluimers/atom-table-monitor@bd48b5b:

constructor TRegisteredWindowsMessageInformation.Create(const AIndex: Integer);
var
  LLength: Integer;
  LName: string;
  LWindowsMessageNameChars: array [Byte] of Char;
begin
..
  LLength := GetClipboardFormatName(AIndex, LWindowsMessageNameChars, High(Byte));
  if LLength = 0 then
    inherited Create(AIndex, '')
  else
  begin
    LName := StrPas(LWindowsMessageNameChars);
    SetLength(LName, LLength);
    inherited Create(AIndex, LName);
  end;
end;

In retrospect, I made a wordblindness error (WindowsMessage in stead of WindowMessage), so I will likely post a commit there some day: [Wayback/Archive] Fix `WindowsMessage` into `WindowMessage` in various identifiers. · Issue #2 · jpluimers/atom-table-monitor

This lists any User Atom nam be it registered with RegisterClass, RegisterWindowMessage, or RegisterClipboardFormat (as they share the same User Atom table)

The trick is explained in the Microsoft documentation linked further below.

More about Windows messages

Windows messages can be a mess, so two years ago, I put a lot of related links in Some odd Windows Messages for my research list (Windows 10 with a very basic Delphi application).

Edit 20221021: more on Windows Atoms

Seems not all of you are dinosaurs like me, so below are some links on Windows Atoms (most via [Wayback/Archive] microsoft windows atoms – Google Search).

Windows Atoms are basically dictionaries having an index of 16-bit SHORT integers and storing corresponding null terminated strings. This structure type helped preserve memory in the 16-bit Windows days. That way you can communicate using Windows messages (see below) where SendMessage and PostMessage had 16-bit WPARM and LPARAM typed parameters.

Each process could have their own atom tables (local atom tables), but there are also global atom tables shared by all running Windows processes, for instance the user atom table (API calls like RegisterClass, RegisterWindowMessage, or RegisterClipboardFormat  add entries here and the last two calls are persistent over your Windows Station session until you logoff or reboot) and the global atom table (often used for DDE – Dynamic Data Exchange – and OLE – Object Linking and Embedding).

Being 16-bit in index size, there can be only 65536 entries in an atom table, of which only 16383 can be used for string entries, so they can fill up. In the 16-bit Windows era, that was usually not much of a problem because processors were mainly single-core, memory was limited and therefore few processes ran at a time. Fast forward today, with ubiquitous memory and CPU resources, especially global atom tables can fill up faster than you might expect so keep an eye on that when adding entries to them.

Windows Messaging:

Related blog posts:

–jeroen


Viewing all articles
Browse latest Browse all 53

Trending Articles