Since about a decade, TThread
has a few overloaded [WayBack] Synchronize
methods which all allow some specified synchronised method to run in the context of the main thread:
Any exceptions raised in that methods are caught using [WayBack] System.AcquireExceptionObject and re-raised in the calling thread.
If that happens, you loose a piece of the stack information. I knew that, but found out the hard way that it does because I had to hunt for bugs through inherited code written by people that did not know.
This was part of the stack trace that code would show during an exception:
Exception EAccessViolation at $004D732F: Access violation at address $00409174 in module ''.....exe''.
Read of address 80808080
StackTrace:
(000D632F){.....exe} [004D732F] System.Classes.TThread.Synchronize$qqrp41System.Classes.TThread.TSynchronizeRecordo (Line 14975, "System.Classes.pas" + 40) + $0
(000D6430){.....exe} [004D7430] System.Classes.TThread.Synchronize$qqrxp22System.Classes.TThreadynpqqrv$v (Line 15007, "System.Classes.pas" + 9) + $A
(005D6E61){.....exe} [009D7E61] IdSync.DoThreadSync$qqrp18Idthread.TIdThreadynpqqrv$v (Line 281, "IdSync.pas" + 21) + $6
(005D6E87){.....exe} [009D7E87] IdSync.TIdSync.SynchronizeMethod$qqrynpqqrv$v (Line 326, "IdSync.pas" + 2) + $8
Exception EAccessViolation at $00409174: Access violation at address $00409174 in module ''.....exe''. Read of address $80808080 with StackTrace
(00008174){.....exe} [00409174] System.@IsClass$qqrxp14System.TObjectp17System.TMetaClass + $C
The first exception has a different address than the one in the exception message.
Which means that you miss the whole stack path to the _IsClass
call (the underlying method implementing the as
keyword) that the actual exception was initiated at.
And yes: the $80808080
is the FastMM4
marker for freed memory, so this was a use-after-free
scenario.
A simple wrapper like this using a central logging facility gave much more insight in the actual cause:
procedure RunLoggedMethod(AMethod: TMethod);
begin
try
AMethod();
except
on E: Exception do
begin
Logger.LogExceptionDuringMethod(E, AMethod);
raise; // mandatory to stay compatible with the old un-logged code
end;
end;
end;
Then call it like this inside a thread descendant:
Synchronize(RunLoggedMethod(MethodToRunInMainThread));
The old code was like this:
Synchronize(MethodToRunInMainThread);
This was quite easy to change, as I already had boiler code around exported DLL functions that had a similar construct (without the raise;
as exceptions cannot pass DLL boundaries unless very specific circumstances hold).
Similar methods are needed to encapsulate procedure TIdSync.Synchronize()
, procedure TIdSync.SynchronizeMethod
, procedure TIdThread.Synchronize(Method: TThreadMethod)
and [WayBack] Queue
overloads:
–jeroen