Get Unit Name from InitTable - hack System.pas
-
21-12-2019 - |
Question
For debugging our slow-starting Delphi XE3 application, we would like to log the initialization-phase of all used units in our system.
To complete this task, we need the Unit Name for each initialization call.
Analyzing the Data, when stepping through InitNames, we find the first Unit Name inside:
InitContext.InitTable^.TypeInfo.UnitNames
but we do not know how to fetch the appropriate Name from the Unit ID I before calling the Initialization procedure. Code Documentation says, TypeInfo.UnitNames contains the concatenated Unit Names of all Units.. But how do i travel between them? It is not an array nor a long string with seperators.
The code, where i would like to insert the log routine.
procedure InitUnits;
var
Count, I: Integer;
Table: PUnitEntryTable;
P: Pointer;
begin
if InitContext.InitTable = nil then
exit;
Count := InitContext.InitTable^.UnitCount;
I := 0;
Table := InitContext.InitTable^.UnitInfo;
{$IFDEF LINUX}
Inc(PByte(Table), InitContext.Module^.GOT);
{$ENDIF}
try
while I < Count do
begin
/////////////////////////////////////
MyLogCode( 'Unit: ' + Get UnitName here )
/////////////////////////////////////
P := Table^[I].Init;
Inc(I);
InitContext.InitCount := I;
if Assigned(P) and Assigned(Pointer(P^)) then
begin
{$IF defined(MSWINDOWS)}
TProc(P)();
{$ELSEIF (defined(POSIX) and defined(CPUX86))}
CallProc(P, InitContext.Module^.GOT);
{$ELSE}
TProc(P)();
{$ENDIF}
end;
end;
except
FinalizeUnits;
raise;
end;
end;
Recompiling the System.pas will be done via Arnaud Bouchez suggested solution.
Solution
UnitNames
contains multiple unit names. They are Pascal short strings, concatenated. However, as we shall see, they are not the names that you need.
Break in the debugger in InitUnits
and evaluate:
PAnsiChar(InitContext.InitTable.TypeInfo.UnitNames)
In my simple test project, a console app that just uses SysUtils
, you see the following:
#$F'System.SysUtils'#6'System'#$18'System.Internal.ExcUtils'#$F'System.SysConst' #7'SysInit'#$10'System.Character'#$E'Winapi.Windows'#$E'System.UITypes' #$C'System.Types'#$10'System.RTLConsts'#$C'Winapi.PsAPI'#$F'Winapi.SHFolder' #$F'Winapi.ImageHlp‹À'
The first character is the length of the string. They are concatenates one after the other, with a total of InitContext.InitTable.TypeInfo.UnitCount
names. For my simple project InitContext.InitTable.TypeInfo.UnitCount
evaluates to 13.
However, these names do not correspond to the units that are initialised. In my test project InitContext.InitTable^.UnitCount
has value 18, and the units are initialized in a quite different order from that listed above. As I'm sure you know, SysInit
always comes first. As you can see from the above, it is in the middle of the list. So, whilst InitContext.InitTable.TypeInfo.UnitNames
gives you a list of certain units, it bears no relation to the units that require initialization, nor the order of initialization.
So as I read it, UnitNames
cannot help you here. My belief is that you will need to use the detailed map file to decode this. You need to look up the name of the function Table^[I].Init
. If you were using, for instance, madExcept, that would be easy to do.
Of course, you might not be able to perform the lookup inside InitUnits
. You are faced with a chicken and egg situation. You may need for at least some units to be initialized before you get started with your logging.
For instance, it looks like you are attempting to allocate a string variable. That will fail because the RTL heap allocator has not been initialized. Your logging code cannot perform any dynamic allocation using the RTL heap if you expect to call it before the RTL has been initialized.
This does all seem over the top to me. If I were you I would:
- Identify the units by index when logging. That is log the value of
I
. - Use the results of your profiling to work out which indexes are the problematic ones.
- Under the debugger, use a conditional break point to break at the call to
TProc(P)()
associated with the index you identified in the previous step. - Step into
P
to find out which unit it is attached to.
OTHER TIPS
In general it is best to NOT rely on the order of class constructors, initialization sections, class destructors, and finalization sections.
In a large (1.2M LOC) project one will not be able to deduce which is the order that Delphi will use.
It is best to refactor your code to avoid such dependencies or use Singleton pattern side effect (initialize when first needed).
Yet if you really have to output the order you can manually log all
Initialization section entries
Finalization section entries
class constructors
class destructors
One safe way to do it is by using the Windows API - OutputDebugString()
You can use the following unit to help you with the logging
unit unt_Debug;
{$SCOPEDENUMS ON}
interface
uses
// System
System.SysUtils
{$IFDEF MACOS}
,FMX.Types
{$ELSE}
,Winapi.Windows
{$ENDIF};
{$WARN SYMBOL_PLATFORM OFF}
/// <remarks>
/// Output debug string. Output debug string can be seen in Delphi
/// View|Debug Windows|Event Log or with 3-rd party programs such as
/// dbgview.exe from SysInternals (www.sysinternals.com)
/// </remarks>
procedure ODS(const Text: string);
procedure ReportClassConstructorCall(const C: TClass);
procedure ReportClassDestructorCall(const C: TClass);
procedure ReportInitializationSection(const UnitName: string);
procedure ReportFinalizationSection(const UnitName: string);
implementation
procedure ReportClassConstructorCall(const C: TClass);
begin
ODS(Format('%0:s class constructor invoked.', [C.ClassName]));
end;
procedure ReportClassDestructorCall(const C: TClass);
begin
ODS(Format('%0:s class destructor invoked.', [C.ClassName]));
end;
procedure ReportInitializationSection(const UnitName: string);
begin
ODS(Format('Unit %0:s - entering Initialization section.', [UnitName]));
end;
procedure ReportFinalizationSection(const UnitName: string);
begin
ODS(Format('Unit %0:s - entering Finalization section.', [UnitName]));
end;
procedure ODS(const Text: string);
begin
{$IFDEF DEBUG}
{$IFDEF MACOS}
// http://stackoverflow.com/questions/12405447/outputdebugstring-with-delphi-for-macosunit unt_Debug;
Log.d(Text);
{$ENDIF}
{$IFDEF LINUX}
__write(stderr, AText, Length(AText));
__write(stderr, EOL, Length(EOL));
{$ENDIF}
{$IFDEF MSWINDOWS}
OutputDebugString(PWideChar(Text));
{$ENDIF}
{$ENDIF}
end;
{$WARN SYMBOL_PLATFORM ON}
end.
It can be used for example this way:
unit Sample;
interface
type
TSample = class
public
class constructor Create;
class destructor Destroy;
end;
implementation
uses
unt_Debug;
class constructor TSample.Create;
begin
ReportClassConstructorCall(TSample);
// Do stuff
end;
class destructor TSample.Destroy;
begin
ReportClassDestructorCall(TSample);
// Do stuff
end;
initialization
ReportInitializationSection(TSample.UnitName);
// do stuff
finalization
ReportFinalizationSection(TSample.UnitName);
// do stuff
end.