Question

I have defined the following :

  • a method pointer that returns 0 if a verification is OK or an error code

    TValidationFunc = Function(AParam: TAnObject): integer Of Object;
    
  • a list of functions to perform:

    Functions: TObjectList < TValidationFunc>;
    

I put several functions with this signature in my Functions list.

To execute them I perform:

For valid In Functions Do
Begin
  res := -1;
  Try
    res := valid(MyObject);
  Except
    On E: Exception Do
      Log('Error in function ??? : ' + E.Message, TNiveauLog.Error, 'PHVL');
  End;
  Result := Result And (res = 0);
End;

How can I get the name of my original function in my log, in case this function raises an exception?

Was it helpful?

Solution 2

Well, never say never :-). This function will return the method name (in the form <ClassType>.<MethodName> ie. TMainForm.FormCreate) for an event that you pass it as a parameter. Unfortunately, you can't use an untyped parameter to allow any kind of event to be passed in, but must code a specific routine for each method signature you want to be able to "decode":

FUNCTION MethodName(Event : TValidationFunc) : STRING;
  VAR
    M   : TMethod ABSOLUTE Event;
    O   : TObject;
    CTX : TRttiContext;
    TYP : TRttiType;
    RTM : TRttiMethod;
    OK  : BOOLEAN;

  BEGIN
    O:=M.Data;
    TRY
      OK:=O IS TObject;
      Result:=O.ClassName
    EXCEPT
      OK:=FALSE
    END;
    IF OK THEN BEGIN
      CTX:=TRttiContext.Create;
      TRY
        TYP:=CTX.GetType(O.ClassType);
        FOR RTM IN TYP.GetMethods DO
          IF RTM.CodeAddress=M.Code THEN
            EXIT(O.ClassName+'.'+RTM.Name)
      FINALLY
        CTX.Free
      END
    END;
    Result:=IntToHex(NativeInt(M.Code),SizeOf(NativeInt)*2)
  END;

Use it like this:

For valid In Functions Doc Begin
  res := -1;
  Try
    res := valid(MyObject);
  Except
    On E: Exception Do
      Log('Error in function '+MethodName(valid)+' : ' + E.Message, TNiveauLog.Error, 'PHVL');
  End;
  Result := Result And (res = 0);
End;

I haven't tried it with the above code, but have tried it with my MainForm's FormCreate.

There is a slight caveat: This will only work for methods that have RTTI generated, and only from Delphi 2010 and up (where they heavily increased the the amount of data available to RTTI). So to make sure it works, you should put the methods you want to track in a PUBLISHED section, as these methods always (by default) will have RTTI generated.

If you want it to be a bit more general, you can use this construct:

FUNCTION MethodName(CONST M : TMethod) : STRING; OVERLOAD;
  VAR
    O   : TObject;
    CTX : TRttiContext;
    TYP : TRttiType;
    RTM : TRttiMethod;
    OK  : BOOLEAN;

  BEGIN
    O:=M.Data;
    TRY
      OK:=O IS TObject;
      Result:=O.ClassName
    EXCEPT
      OK:=FALSE
    END;
    IF OK THEN BEGIN
      CTX:=TRttiContext.Create;
      TRY
        TYP:=CTX.GetType(O.ClassType);
        FOR RTM IN TYP.GetMethods DO
          IF RTM.CodeAddress=M.Code THEN
            EXIT(O.ClassName+'.'+RTM.Name)
      FINALLY
        CTX.Free
      END
    END;
    Result:=IntToHex(NativeInt(M.Code),SizeOf(NativeInt)*2)
  END;

FUNCTION MethodName(Event : TValidationFunc) : STRING; OVERLOAD; INLINE;
  BEGIN
    Result:=MethodName(TMethod(Event))
  END;

Then you only need to code a specific MethodName for each event that simply calls on to the general implementation, and if you mark it as INLINE there's a good chance that it won't even incur an extra function call, but instead call it directly.

BTW: My reply is heavily influenced by the code given by Cosmin Prund a year ago in this question: RTTI information for method pointer

In case your Delphi doesn't have NativeInt defined (can't remember when exactly they implemented it), just define it as:

{$IFNDEF CPUX64 }
TYPE
  NativeInt = INTEGER;
{$ENDIF }

OTHER TIPS

The easiest solution would be - as David hints at - to store the name along with the function pointer, like this:

TYPE
  TValidationFunc = Function(AParam: TAnObject): integer Of Object;
  TFunctions = TDictionary<TValidationFunc,String>;

VAR
  Functions : TFunctions;

populate the list:

Functions:=TFunctions.Create;
Functions.Add(Routine1,'Routine1');
Functions.Add(Routine2,'Routine2');

and then when you run through it:

For valid In Functions.Keys Do Begin
  Try
    res := valid(MyObject);
  Except
    On E: Exception Do Begin
      Log('Error in function ' + Functions[valid] + ' : ' + E.Message, TNiveauLog.Error, 'PHVL');
      res := -1;
    End;
  End;
  Result := Result And (res = 0);
End;

This way, you "link" the validation function with a name in a TDictionary, so that when you have the one, you can obtain the other.

One way you could do this would be to enumerate possible functions looking for one with an address that matched the target. You can decode the instance pointer from the method pointer. From there you can obtain the type. And then RTTI can do the rest.

You might consider storing the name as well as the method pointer in your list. That would be a simple and easy way to proceed.

My final thought is that you could include the executable map in your project and look the function up there. For example this would be quite simple if you already use madExcept, EurekaLog or some similar tool.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top