Misuse of atoms and subtle bug in VCL
Bug search
I was tormented for a long time by a bug related to the inadequate behavior of the Delphic controls after a long uptime of the system and intensive debugging. Lists stopped updating, buttons were pressed, input fields began to lose focus. And everything was sad, and restarting the IDE did not help. Moreover, after restarting the IDE, it itself began to fail as well. I had to reboot.
Today it bothered me, and I began to look for her. I must say not to no avail.
Having pledged window messages, I began to analyze what went wrong.
It turned out that the Control.pas module has these lines:
function FindControl(Handle: HWnd): TWinControl;
var
OwningProcess: DWORD;
begin
Result := nil;
if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and
(OwningProcess = GetCurrentProcessId) then
begin
if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then
Result := Pointer(GetProp(Handle, MakeIntAtom(ControlAtom)))
else
Result := ObjectFromHWnd(Handle);
end;
end;
and GetProp (Handle, MakeIntAtom (ControlAtom) ) always returns 0. Then it became clear that ControlAtom why is 0, and GlobalFindAtom (PChar (ControlAtomString)) returns 0. also
initialized ControlAtomString and ControlAtom in procedure InitControls , which is called in the initialization section of the unit:
procedure InitControls;
var
UserHandle: HMODULE;
begin
{$IF NOT DEFINED(CLR)}
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));
{$IFEND}
The ControlAtomString is populated correctly, but the ControlAtom is populated with zero. There are no checks for errors here, so it came back much later, alas. If you insert GetLastError after GlobalAddAtom , it will return ERROR_NOT_ENOUGH_MEMORY . And if you still carefully read the remark on MSDN to GlobalAddAtom , you can see:
Global atoms are not deleted automatically when the application terminates. For every call to the GlobalAddAtom function, there must be a corresponding call to the GlobalDeleteAtom function.
Everything immediately becomes clear. If the application is terminated incorrectly, then global atoms will flow away. And the cat wept for named atoms: 0xC000-0xFFFF, that is, only 16383. That is each dll, and each exe-shnik written in Delphi using VCL upon incorrect completion leaves leaked global atoms. To be more precise, then there are 2-3 atoms per instance:
ControlAtom and WindowAtom in Controls.pas, and WndProcPtrAtom in Dialogs.pas
Workaround
See created atoms is not difficult. Here is the code for a simple application listing global string atoms:
program EnumAtomsSample;
{$APPTYPE CONSOLE}
uses
Windows,
SysUtils;
function GetAtomName(nAtom: TAtom): string;
var n: Integer;
tmpstr: array [0..255] of Char;
begin
n := GlobalGetAtomName(nAtom, PChar(@tmpstr[0]), 256);
if n = 0 then
Result := ''
else
Result := tmpstr;
end;
procedure EnumAtoms;
var i: Integer;
s: string;
begin
for i := MAXINTATOM to MAXWORD do
begin
s := GetAtomName(i);
if (s <> '') then WriteLn(s);
end;
end;
begin
try
EnumAtoms;
ReadLn;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
You can make sure that the atoms flow by starting any VCL project, and nailing it through the task manager.
Since the atoms are global, we can nail them, regardless of who they were created. It remains to somehow learn to determine that the atom has leaked.
If we look at the names of atoms, then for
WndProcPtrAtom it is WndProcPtr [HInstance] [ThreadID]
ControlAtom it is ControlOfs [HInstance] [ThreadID]
WindowAtom it is Delphi [ProcessID]
In all cases, we can understand that the atom is most likely created by Delphi by a specific prefix + one or two 32-bit numbers in the HEX. In addition, either ProcessID or ThreadID is written to the HEX. We can easily check if there is such a process or thread in the system. If not, then we have a clearly leaked atom, and we may risk releasing it. Yes, yes, take a chance. The fact is that after we made sure that there is no thread / process with this ID, and are going to delete the atom, this process may appear with exactly the same ID and turn out to be a Delphi process. If in the interval between verification and deletion this happens, then we will beat the atom from the valid application. The situation is extremely unlikely, because in the interval between the checks, a necessarily Delphic process must be created, be sure to use exactly the same ID, and be sure to have time to initialize its atoms.
I wrote a console tool for cleaning such leaked global atoms.
Here is the code for this tool:
program AtomCleaner;
{$APPTYPE CONSOLE}
uses
Windows,
SysUtils;
const
THREAD_QUERY_INFORMATION = $0040;
function OpenThread(dwDesiredAccess: DWORD; bInheritHandle: BOOL; dwThreadId: DWORD): THandle; stdcall; external kernel32;
function ThreadExists(const ThreadID: Cardinal): Boolean;
var h: THandle;
begin
h := OpenThread(THREAD_QUERY_INFORMATION, False, ThreadID);
if h = 0 then
begin
Result := False;
end
else
begin
Result := True;
CloseHandle(h);
end;
end;
function TryHexChar(c: Char; out b: Byte): Boolean;
begin
Result := True;
case c of
'0'..'9': b := Byte(c) - Byte('0');
'a'..'f': b := (Byte(c) - Byte('a')) + 10;
'A'..'F': b := (Byte(c) - Byte('A')) + 10;
else
Result := False;
end;
end;
function TryHexToInt(const s: string; out value: Cardinal): Boolean;
var i: Integer;
chval: Byte;
begin
Result := True;
value := 0;
for i := 1 to Length(s) do
begin
if not TryHexChar(s[i], chval) then
begin
Result := False;
Exit;
end;
value := value shl 4;
value := value + chval;
end;
end;
function GetAtomName(nAtom: TAtom): string;
var n: Integer;
tmpstr: array [0..255] of Char;
begin
n := GlobalGetAtomName(nAtom, PChar(@tmpstr[0]), 256);
if n = 0 then
Result := ''
else
Result := tmpstr;
end;
function CloseAtom(nAtom: TAtom): Boolean;
var n: Integer;
s: string;
begin
Result := False;
s := GetAtomName(nAtom);
if s = '' then Exit;
WriteLn('Closing atom: ', IntToHex(nAtom, 4), ' ', s);
GlobalDeleteAtom(nAtom);
Result := True;
end;
function ProcessAtom(nAtom: TAtom): Boolean;
var s: string;
n: Integer;
id: Cardinal;
begin
Result := False;
s := GetAtomName(nAtom);
n := Pos('ControlOfs', s);
if n = 1 then
begin
Delete(s, 1, Length('ControlOfs'));
if Length(s) <> 16 then Exit;
Delete(s, 1, 8);
if not TryHexToInt(s, id) then Exit;
if not ThreadExists(id) then
Exit(CloseAtom(nAtom));
Exit;
end;
n := Pos('WndProcPtr', s);
if n = 1 then
begin
Delete(s, 1, Length('WndProcPtr'));
if Length(s) <> 16 then Exit;
Delete(s, 1, 8);
if not TryHexToInt(s, id) then Exit;
if not ThreadExists(id) then
Exit(CloseAtom(nAtom));
Exit;
end;
n := Pos('Delphi', s);
if n = 1 then
begin
Delete(s, 1, Length('Delphi'));
if Length(s) <> 8 then Exit;
if not TryHexToInt(s, id) then Exit;
if GetProcessVersion(id) = 0 then
if GetLastError = ERROR_INVALID_PARAMETER then
Exit(CloseAtom(nAtom));
Exit;
end;
end;
procedure EnumAndCloseAtoms;
var i: Integer;
begin
i := MAXINTATOM;
while i <= MAXWORD do
begin
if not ProcessAtom(i) then
Inc(i);
end;
end;
begin
try
EnumAndCloseAtoms;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Just run, the leaked atoms are cleaned. Check, maybe right now you already have leaked atoms in the system.
Finally
Inspection of the code showed that these global atoms are used only for SetProp and GetProp functions. It is completely incomprehensible why the Delphi developers decided to use atoms. After all, both of these functions work fine with pointers to strings. It is enough to transmit a unique string, which in itself already exists, because an atom is initialized with it.
The logic of such comparisons in VCL code is also incomprehensible:
if GlobalFindAtom (PChar (ControlAtomString)) = ControlAtom then
Both variables are initialized in one place. The string is going to be unique (from HInstance and ThreadID). Validation will always return True. Alas, Delphi is now promoting new features, all sorts of FMXs. It is unlikely that they will fix this bug. Personally, I don’t even want to report on QC, knowing how it fixes. But somehow you have to live with it. Those who wish can execute the code of the above tool at the start of their application. In my opinion, this is in every way better than waiting for leaked atoms.
Well, in our own developments, we should try to avoid global atoms, because the OS does not control their leakage.
Tools + sources