Obtaining a unique source code location reference

I’m doing a bit of work on a mixed C++ Builder and Delphi project these days and C++ has a nice feature that’s great for fleshing out log file entries.

There’s these two macros FILE and LINE which return the current file name and line number respectively.

A simple example of the sort of things you could do if they were available for Delphi

if (CatsEatingDogs) then
  raise Exception.CreateFmt('Your world is collapsing at line %d in file %s', [__LINE__, __FILE__]);

You can of course put some sort of hardcoded file name and method name in your error messages but they’re vulnerable to drifting away from the actuality should you rename the file or method or if you copy and paste the code. Plus you don’t get granularity right down to the line number unless you’re really :crazy_face: and hard code line numbers into your error messages.

The closest thing I know of is the Assert statement which through compiler magic reports the file and line number.

Once in the past when I wanted to uniquely identify SQL queries in application logs I added a GUID parameter to my RunSQL procedure. The Ctrl+Shift+G shortcut generates a GUID in the editor so that was doable, though far from ideal, for that use case.

Anyone got any better ideas than GUIDs for uniquely referencing code locations? The more resistant to identifier renaming and code copy/pasting the better.

One possibility comes to mind would be if you could get a stack trace using either Eurekalog or MadExcept and extract the file/line number from that.

For File, if you’re in a method, you might be able to use UnitName

https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.TObject.UnitName

1 Like

I chose a bad example. Exception stack tracing is well served as Mark says by EurekaLog and MadExcept.

I’d be looking to use it more for normal execution than when exceptions are raised. Here’s a rough approximation of my SQL with GUIDs example but using a file name and line number instead.

procedure RunSQL(inSQL, inFileName : string; inLineNum : integer);
begin
  Query1.SQL.Text := inSQL;
  Query1.Execute;
  LogToFile(Format('The SQL query at line %d of %s took %d ms to execute',
                   [inLineNum, inFileName, Query1.ExecutionTime]));
end;

I haven’t measured them before, but I’ve been under the impression that the stack tracing done by MadExcept/EurekaLog was more than likely a fairly expensive operation and not something you want to be doing repeatedly in the normal course of events.

Hi Lachlan,

You should be able to get the function’s address pointer, figure out which module that bit of memory belongs to and then use the map file to decode its name. It’s going to have an overhead, but you might be able to cache the name resolution, there shouldn’t be a need to raise an exception.

ReturnAddress may be a magic variable that you can use too.

See the code in System.pas for _Assert and ErrorAt.

It’s not something I’ve done, but maybe that might possibly work.

A quick google and this came up:

Get method’s name as string from the code inside that method - I made this - Delphi-PRAXiS [en] (delphipraxis.net)

1 Like

Just like in our own companies where we would brainstorm new ideas for the products we build I think an exciting role ADUG could play would be to perhaps have a yearly discussion around ways Delphi could be improved and these types of features could be part of those suggestions.

There doesn’t appear to be any good way to do what you’ve described in Delphi across the board and so this feature would be good to suggest as a new one especially if it’s also in C++

I find spending more than 50% of my week in JavaScript nowadays there are so many exciting ideas in that space that I wish would come back to Delphi that make coding so much faster. The latest .NET features both in the language and in visual studio have been massive leaps forward and I suspect the next visual studio will be loaded with AI code generation as well.

1 Like

This is something that I’ve wanted to do in the past as well. We use madExcept (and optionally JCL) to get stack traces but I’m pretty sure that you can only do it (at least easily) if an exception is raised, and there is quite a bit of overhead (slow).

Here’s an idea for a hack though:

procedure GetSourceInfo(const AssertProc: TProc; out SourceFile: string; out LineNumber: Integer);
var
  sourceParts: TArray<string>;
begin
  try
    AssertProc;
  except
    on e: Exception do
    begin
      // e.Message: Assertion failure (<filename>, Line <linenumber>)
      sourceParts := e.Message.Split(['(', ')']); //
      sourceParts := sourceParts[1].Split([',', ' ']);
      SourceFile := sourceParts[0];
      LineNumber := StrToInt(sourceParts[3]);
    end;
  end;
end;

procedure TestSourceInfo;
var
  sourceFile: string;
  lineNumber: Integer;
begin
  GetSourceInfo(procedure begin Assert(False) end, sourceFile, lineNumber);

  ShowMessage(Format('File: %s, Line: %d', [sourceFile, lineNumber]));
end;

You just need to pass that anonymous method procedure begin Assert(False) end from the source location (can’t move to a re-usable procedure).

You also need to have assertions enabled which might not be desirable in a shipping app but would be ok for debugging.

Edit: You could change GetSourceInfo to return a record that contains the file and line number, then use it more easily like: RunSQL('SELECT 1', GetSourceInfo(procedure begin Assert(False) end));

With EurekaLog, if you include EDebugInfo you get access to

__MODULE__
__UNIT__
__FILE__
__FUNCTION__
__LINE__

The code doesn’t appear to use Exceptions, but they are actually implemented as functions that looks up the stack and reads the Eurekalog debug info to obtain the information instead of being constants.

https://www.eurekalog.com/help/eurekalog/index.php?topic_function_edebuginfo_getlocationinfo.php

1 Like

Here is an alternative code fragment using only jcl. It needs to have at least “Limited Debug Information” set for “Debug Information” under “Compiling” options for it to get the line numbers.

implementation

{$R *.dfm}

uses jcldebug;

function GetEIP: Pointer; assembler;
{$IFDEF CPUX86}
asm
  POP  EAX
  PUSH EAX
end;
{$ENDIF}
{$IFDEF CPUX64}
asm
  POP  RAX
  PUSH RAX
end;
{$ENDIF}


procedure TForm5.Button1Click(Sender: TObject);
var
  Info: TJclLocationInfo;
  P: Pointer;
begin
  P := GetEIP;
  GetLocationInfo(p, Info);
  ShowMessage(Info.UnitName + ':' + Info.LineNumber.ToString);
end;
1 Like

Hi Geoff

Just looked at the definition of TJclLocationInfo and see that it contains a field called procedure name which I am hoping will contain the name of the executing procedure.

TJclLocationInfo = record
Address: Pointer; // Error address
UnitName: string; // Name of Delphi unit
ProcedureName: string; // Procedure name
OffsetFromProcName: Integer; // Offset from Address to ProcedureName symbol location
LineNumber: Integer; // Line number
OffsetFromLineNumber: Integer; // Offset from Address to LineNumber symbol location
SourceName: string; // Module file name
DebugInfo: TJclDebugInfoSource; // Location object
BinaryFileName: string; // Name of the binary file containing the symbol
end;

I’m hoping that this gives what I want.

I’ve been hunting around for a way to get the procedure/method/function name for some time now.

Sometimes I wonder why the Delphi people don’t introduce a function called, say ProcedureName (returns string), whose value is determined at compile time and returns the name of the procedure/method/function currently being executed.

Regards
Graeme

Even better, have a function that returns a record similar to the TJclLocationInfo above and you could then pass that into various logging methods.

In theory, I think that you could combine the techniques already described here and elsewhere to create a function that makes logging methods as simple as adding a line at the start of any method that you want to know about:

LogCurrentMethod;

Such a function would log the start of the method and return an interface which when it went out of scope would log the end of the method - as per many similar logging systems already.

The tricky bit would be writing a GetEIP function similar to the above that would walk the call stack back far enough to get the address pointer for the location that called the calling function - shouldn’t actually be that hard, but would require a bit of experimentation to do.

@Graeme that record does contain the procedure name in it. After some playing around some more I was able to get it so you can call a single logging function and it records the line that it was called from, like below

Unit5:Unit5.TForm5.SomeNestedProcedure:60 LogMsg

Simply call the function

  LogMsg('LogMsg');
procedure LogMsg(str:string);
var
  Info: TJclLocationInfo;
  P: Pointer;
begin
  P := Pointer(NativeUInt(ReturnAddress) - SizeOf(Pointer)); // works on both 32-bit and 64-bit windows
  GetLocationInfo(p, Info);
  ShowMessage(Info.UnitName +':' + Info.ProcedureName + ':' + Info.LineNumber.ToString + ' ' + str);
end;
3 Likes

Hi Mark Using your suggestion, I would be thinking that there would be an ‘internal’ function called GetLocationInfo that returns a record containing UnitName, ProcedureName, LineNumber and SourceFileName (I don’t know what the other fields are used for so I am not considering them.
The value of these fields are determined at compile time. That is when GetLocationInfo.ProcedureName the compiler replaces the call by a string constant containing the procedure name and similarly for the other fields. These values are NOT calculated at runtime. Doing it this way does not impose an extra processing to evaluate the values.

@Graeme Yes, having an internal function that returns a record with values determined at compile time would be the best solution IMO.

I use JCL to give me stack trace, unit - method and I think line number. Basically you are using one of the calls from the JCL exception handler, so go look at that jvclexeptionDialogue

{-----------------------------------------------------------------------------
 GetCallStack
 Auth: Jason Chapman Date: 07/11/2007
  Pur: From the JCL project.
-----------------------------------------------------------------------------}
function GetCallStack(vMaxLines: Integer = 1000; const vDelim: String = crlf):
    string;
const cmethod = '.GetCallStack';
var
  StackList: TJclStackInfoList;
  Details: TStringList;
  iCount : Integer;
  iCountRows : Integer;
begin
  // Ignore 2 levels to avoid this function and the call to
  // JclCreateStackList in the result
  result:='';
  StackList := JclCreateStackList(False, 2, nil);
  Details := TStringList.Create;
//  try
    StackList.AddToStrings(Details, True, True, True, True);
    iCountRows:=Details.Count-1;
    iCountRows:=min(iCountRows,vMaxLines);
    for icount := 0 to iCountRows do
      result:=result+vdelim+Details[icount];
//  finally
    StackList.Free;
    Details.Free;
//  end;
end;

TJclStackInfoList in jclDebugs

1 Like

@Paul_McGee thanks for editing and making my code look better - I’ll remember for next time.

No problem … I wasn’t sure if it would let me. :slight_smile: