4

考虑这个用于方法跟踪的典型代码(为说明而简化):

type
  IMethodTracer = interface
  end;

  TMethodTracer = class(TInterfacedObject, IMethodTracer)
  private
    FName: String;
    FResultAddr: Pointer;
    FResultType: PTypeInfo;
  public
    constructor Create(
      const AName: String;
      const AResultAddr: Pointer = nil;
      const AResultType: PTypeInfo = nil);
    destructor Destroy; override;
  end;

constructor TMethodTracer.Create(
  const AName: String;
  const AResultAddr: Pointer;
  const AResultType: PTypeInfo);
begin
  inherited Create();
  FName := AName;
  FResultAddr := AResultAddr;
  FResultType := AResultType;
  Writeln('Entering ' + FName);
end;

destructor TMethodTracer.Destroy;
var
  lSuffix: String;
  lResVal: TValue;
begin
  lSuffix := '';
  if FResultAddr <> nil then
    begin
      //there's probably a more straight-forward to doing this, without involving TValue:
      TValue.Make(FResultAddr, FResultType, lResVal);
      lSuffix := ' - Result = ' + lResVal.AsString;
    end;
  Writeln('Leaving ' + FName + lSuffix);

  inherited Destroy;
end;

function TraceMethod(
  const AName: String;
  const AResultAddr: Pointer;
  const AResultType: PTypeInfo): IMethodTracer;
begin
  Result := TMethodTracer.Create(AName, AResultAddr, AResultType);
end;

//////

function F1: String;
begin
  TraceMethod('F1', @Result, TypeInfo(String));
  Writeln('Doing some stuff...');
  Result := 'Booyah!';
end;

F1();

这是按预期工作的。输出是:

进入 F1
做一些事情...
离开 F1 - 结果 = Booyah!

我现在正在寻找一种方法来最小化调用所需参数的数量TraceMethod(),理想情况下允许我完全跳过 -Result相关的参数。我自己没有汇编程序或堆栈布局的经验,但如果我没记错的话,从我看到其他人所做的“魔法”来看,至少隐含的魔法Result变量的内存地址应该可以通过某种方式获得,不是吗? ? 也可能有人可以从那里获得它的类型信息?

当然,如果甚至可以确定“周围”函数本身的名称,这将消除将参数传递给TraceMethod完全...

我正在使用 Delphi XE2,因此可以使用所有最近引入的语言/框架功能。

在任何人提到它之前:我的实际代码已经使用CodeSite.EnterMethod/ExitMethod而不是Writeln-calls。我也知道这个简化的例子不能处理复杂的类型并且不执行任何错误处理。

4

2 回答 2

3

你最好的选择是真的只是通过@Result。如果你不这样做,那么就不能保证它Result甚至一个地址。返回简单类型的函数,例如Integer并将Boolean结果放入 EAX 寄存器。如果结果没有理由有地址,那么编译器将不会为它分配任何内存。使用表达式@Result 强制编译器给它一个地址。

但是,仅仅知道地址不会为您提供返回类型。可能有一种方法可以通过 RTTI 发现它。这将涉及三个步骤:

  1. 从方法名中提取类名。然后您可以获得该类型的 RTTI。这将要求方法名称包含类的明确名称(包括单元名称)。

  2. Using the list of methods from that type, find the RTTI for the method. This will be complicated by the fact that the name doesn't necessarily uniquely identify a method. Overloads will all show the same name. (Rruz showed how to deal with RTTI of overloaded methods in the context of the Invoke method.) Also, the method name you get from the debug info won't necessarily match the RTTI name.

    Instead of trying to match the name, you could instead loop over all the class's methods, searching for the one whose CodeAddress property matches the address of the caller. Determining how to get the address of the start of the caller (instead of the return address) is proving more difficult to find than I expected, though.

  3. 获取方法的返回类型,使用Handle属性最终得到PTypeInfo你想要的值。

于 2012-05-30T17:15:16.053 回答
1

这个特性中的一些已经包含在我们的TSynLog中,它适用于从 Delphi 5 到 XE2。

它是完全开源的,并且与 Delphi XE2 一起使用,因此您手头有所有需要的源代码。它具有异常拦截和堆栈跟踪功能。

它允许像这样编码跟踪:

procedure TestPeopleProc;
var People: TSQLRecordPeople;
    Log: ISynLog;
begin
  Log := TSQLLog.Enter;
  People := TSQLRecordPeople.Create;
  try
    People.ID := 16;
    People.FirstName := 'Louis';
    People.LastName := 'Croivébaton';
    People.YearOfBirth := 1754;
    People.YearOfDeath := 1793;
    Log.Log(sllInfo,People);
  finally
    People.Free;
  end;
end;

它将被记录为:

20120520 13172261  +    000E9F67 SynSelfTests.TestPeopleProc (784)
20120520 13172261 info      {"TSQLRecordPeople(00AB92E0)":{"ID":16,"FirstName":"Louis","LastName":"Croivébaton","Data":"","YearOfBirth":1754,"YearOfDeath":1793}}
20120520 13172261   -    000EA005 SynSelfTests.TestPeopleProc (794) 00.002.229

那是:

  • 你有行号和方法名;
  • 您有方法内消耗的确切时间 (00.002.229);
  • 显示任何结果都非常容易(这里,该类甚至直接序列化为 JSON,您可以对任何类型的数据,甚至记录进行同样的操作);
  • 方法可以嵌套,我们的日志查看器中还有一个分析工具可用- 也就是说,您可以从日志文件中检索在客户端花费最多时间的方法;
  • 您可以非常轻松地添加堆栈跟踪或任何您想要的信息;
  • 我们的代码对速度进行了非常优化:例如,它使用interface“自动离开”功能,就像您一样,但它不会进行任何内存分配,因为它使用“假引用计数”技巧;
  • 当然,Log在栈上定义一个变量是可选的:如果只需要跟踪方法的 Enter/Leave ,写TSQLLog.Enter就够了。局部变量用于在方法 ( )Log中轻松嵌套附加信息。Log.Log(sllInfo,People)

请注意,调试信息(即方法名称和行号)是从.map编译时生成的文件的专有的非常优化的二进制转换中检索的。它将它本身小很多.map(例如 900 KB .map-> 70 KB .mab,可以很容易地嵌入到 exe 中),因此小于 JCL 或 MadExcept 使用的格式,也小于编译时嵌入的信息德尔福。

我认为在“Enter”方法中硬编码“结果”是不值得的。它将添加大量编码(例如,转换为TValue耗时),而收益微不足道 - 大多数情况下,您需要了解的不仅仅是结果内容。

于 2012-05-30T17:14:58.117 回答