【问题标题】:Access Violation if RTTI is accessed inside package如果在包内访问 RTTI,则访问冲突
【发布时间】:2015-08-20 09:01:57
【问题描述】:

我写了一个简单的控制台程序来施展一些 RTTI 魔法:

program TypeCast;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.RTTI, Generics.Collections;

type
  TSpr = class
  public
    s: string;
    i: Integer;
    b: Boolean;
  end;

var
  Spr: TSpr;
  vCtx: TRTTIContext;
  vType: TRTTIType;
  vField: TRTTIField;
  Dict: TDictionary<string, TRTTIField>;

begin
  try
    Spr := TSpr.Create;
    vType := vCtx.GetType(TSpr.ClassInfo);
    Dict := TDictionary<string, TRTTIField>.Create;
    for vField in vType.GetFields do
      Dict.AddOrSetValue(vField.Name, vField);
    Dict['s'].SetValue(Spr, 'Hello World!');
    Dict['i'].SetValue(Spr, 123);
    Dict['b'].SetValue(Spr, True);
    Writeln(Spr.s);
    Writeln(Spr.i);
    Writeln(Spr.b);
    Spr.Free;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;
end.

输出:

Hello World!
123
TRUE

如果我编译并运行这个程序,它工作正常。但是如果我使用这种技术将这些类型的变量转发到在另一个包中定义的对象,它会给我带来很多麻烦。

MCVE 内容/重现步骤:
...假设您是从空环境开始...

  1. 创建PluginInterface 包。在那里添加UClassManager

    unit UClassManager;
    
    interface
    
    uses
      Classes, Contnrs;
    
    type
      TClassManager = class(TClassList);
    
    function ClassManager: TClassManager;
    
    implementation
    
    var
      Manager: TClassManager;
    
    function ClassManager: TClassManager;
    begin
      Result := Manager;
    end;
    
    initialization
    Manager := TClassManager.Create;
    
    finalization
    Manager.Free;
    
    end.
    

    UPlugin 单位。

    unit UPlugin;
    
    interface
    
    uses RTTI;
    
    type
      TPlugin = class
      public
        procedure Init; virtual; abstract;
        function SetProp(Key: string; Value: TValue): Boolean; virtual; abstract;
      end;
    
      TPluginClass = class of TPlugin;
      IPluginHost = interface
        function RunPlugin(PluginName: string): TPlugin; // Run Plugin by it's ClassName
      end;
    
    var
      Host: IPluginHost;
    
    implementation
    
    end.
    
  2. 创建VCL Forms Application,启用运行时包,添加对PluginInterface的引用并在其上添加TButton。为相应的事件制作这些处理程序:

    procedure TForm1.FormCreate(Sender: TObject);
    begin
      LoadPackage('UniversalSpr.bpl');
      Host := Self;
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    var
      Plugin: TPlugin;
    begin
      Plugin := Host.RunPlugin('TSprPlugin');
      Plugin.Init;
      Plugin.SetProp('SprTableName', 'MATERIALS');
      Plugin.SetProp('EditRights', True);
      Plugin.SetProp('BeforePostValue1', 3);
    end;
    
    function TForm1.RunPlugin(PluginName: string): TPlugin;
    var
      I: Integer;
    begin
      Result := nil;
      for I := 0 to ClassManager.Count - 1 do
        if ClassManager[I].ClassNameIs(PluginName) then begin
          Result := TPluginClass(ClassManager[I]).Create;
          Break;
        end;
    end;
    

    粗略地说,TForm1 应该是IPluginHost 的后代。并且不要忘记将UClassManagerUPlugin 添加到uses 子句中。其他单元将由 IDE 自动添加。

  3. 创建包UniversalSpr 并将其输出文件放置到您的应用程序所在的同一目录中。在TSprPlugin 中实现UPlugin

    unit USprPlugin;
    
    interface
    
    uses
      UPlugin, RTTI, Generics.Collections;
    
    type
      TSpr = class
        SprTableName: string;
        BeforePostValue1: int64;
        EditRights: boolean;
      end;
      TSprPlugin = class(TPlugin)
        procedure Init; override;
        function SetProp(Key: string; Value: TValue): Boolean; override;
        private
          Spr: TSpr;
          PropDict: TDictionary<string, TRTTIField>;
      end;
    
    implementation
    
    procedure TSprPlugin.Init;
    var
      vCtx: TRTTIContext;
      vType: TRTTIType;
      vField: TRTTIField;
    begin
      if not Assigned(Spr) then
        Spr := TSpr.Create;
      vType := vCtx.GetType(Spr.ClassInfo);
      if not Assigned(PropDict) then
        PropDict := TDictionary<string, TRTTIField>.Create;
      for vField in vType.GetFields do
        PropDict.Add(vField.Name, vField);
    end;
    
    function TSprPlugin.SetProp(Key: string; Value: TValue): Boolean;
    var
      Field: TRTTIField;
    begin
      Result := PropDict.TryGetValue(Key, Field);
      if Result then
        // here I get Access Violation
        Field.SetValue(Spr, Value);
    end;
    
    end.
    

单击Button1 后,您可以将指定的值传递给属性设置器/修改器,但如果您尝试在例程中重复我的TypeCast 技巧,您将在尝试访问00000004 时遇到访问冲突。

此外,调查和高级调试表明 Field.FieldType 评估正确(这解释了为什么不抛出 InsufficientRTTI),但如果我想得到 Field.Fieldtype.Handle,我会得到臭名昭著的 AV。

我可以设置从原始SetValue 方法中跳过Cast 的值:

function TSprPlugin.SetProp(Key: string; Value: TValue): Boolean;
var
  Field: TRTTIField;
begin
  Result := PropDict.TryGetValue(Key, Field);
  if Result then
    Value.ExtractRawData(PByte(Spr) + Field.Offset);
end;

Dalija 建议避免使用包,我已经考虑到这一点,这就是我创建TypeCast 来测试 RTTI 的原因。但是我需要包,因为我的应用程序的设计,我不能只是将它重写为单一的。我可以做些什么来避免这种访问冲突不放弃包

【问题讨论】:

  • 这里不清楚的是包的排列方式。我希望任何对不同包中定义的类型执行 RTTI 的尝试都会失败。您是否考虑过进行一些调试以确定哪一行代码失败,以及哪个引用是nil
  • 顺便说一句,你不需要写vCtx := TRTTIContext.Create,你可以删除那行。 TRttiContext 变量会自动初始化。而且你不需要vCtx.Free,你可以删除那行。
  • 嗯,我声称USprPluginUniversalSprUnit 使用相同的包,它是UniversalSpr。并且通过调试发现可以读取PropDict[Key]的属性,除了FieldType
  • 另一个顺便说一句,不要测试ContainsKey 然后GetValue。这执行两个查找。与TryGetValue 合二为一。
  • 你删除了一些不相关的东西,但这远非正确的 MCVE。你没有创建最小的例子,还有很多垃圾,但最重要的是你没有创建完整和可验证的例子。如果不进行大量猜测,我无法获取您的代码并按原样编译它。说ClassManager 是简单的类列表并没有削减它。你如何填充它,你如何初始化主机,你如何从它的名字中找到插件。这就是您的真正问题可能隐藏的所有相关信息。

标签: delphi package delphi-xe2 rtti


【解决方案1】:

无论您是否使用运行时包,您当前的代码都有一些问题。您的 MCVE 并不完全是最小的,并且您从工作的控制台应用程序中添加了太多步骤到无法工作的打包代码中。

在调试您的问题时,您应该从将逻辑封装到 TSprPlugin 类中并直接测试该类开始,而不会弄乱运行时包。当您确定TSprPlugin 代码正常运行时,您可以添加包并查看它的运行情况。

现在您的代码因以下简单的测试项目而失败

program test;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  USprPlugin in 'USprPlugin.pas',
  UPlugin in 'UPlugin.pas';

var
  Plugin: TPlugin;

begin
  Plugin := TSprPlugin.Create;
  Plugin.Init;

  Plugin.SetProp('SprTableName', 'MATERIALS');
  Plugin.SetProp('EditRights', True);
  Plugin.SetProp('BeforePostValue1', 3);
end.

vCtx 从局部变量移动到TSprPlugin 字段解决了上述测试用例的问题。

unit USprPlugin;

interface

uses
  UPlugin, RTTI, UniversalSprUnit, Generics.Collections;

type
  TSprPlugin = class(TPlugin)
    vCtx: TRTTIContext;
    procedure Init; override;
    function SetProp(Key: string; Value: TValue): Boolean; override;
    private
      Spr: TSpr;
      PropDict: TDictionary<string, TRTTIField>;
  end;

implementation

procedure TSprPlugin.Init;
var
  vType: TRTTIType;
  vField: TRTTIField;
begin
  vCtx := TRttiContext.Create;
  if not Assigned(Spr) then
    Spr := TSpr.Create;
  vType := vCtx.GetType(Spr.ClassInfo);
  if not Assigned(PropDict) then
    PropDict := TDictionary<string, TRTTIField>.Create;
  for vField in vType.GetFields do
    PropDict.Add(vField.Name, vField);
end;

function TSprPlugin.SetProp(Key: string; Value: TValue): Boolean;
var
  Field: TRTTIField;
begin
  Result := PropDict.TryGetValue(Key, Field);
  if Result then
    // here I get Access Violation
    Field.SetValue(Spr, Value);
end;

end.

从那里开始,您可以逐步添加其他功能,确保每个步骤都不会破坏功能。

此外,您没有释放 SprPropDict 字段,从而造成内存泄漏,但我不确定是否不包含该代码只是因为它与您遇到的问题没有直接关系,或者您真的那里有内存泄漏。

【讨论】:

  • 其实TRTTIContext是record,不是class,所以我觉得不需要显式创建。当我引入包时出现问题,我不能简单地避免它们,我有很多用插件范式编写的代码。此外,我最初在 TypeCast 程序中设置 s、i、b 之前调用了 vCtx.Free。我可以假设释放对象实际上并没有删除它,但让我挑剔:记录不是显式创建的,
  • 它是记录,所以它的内存不必分配Create,但这并不意味着可以省略其他初始化。同样的方式Free 可以使记录中的数据无效,即使记录仍然存在于内存中。在这种情况下,我更喜欢遵循文档。 Obtaining RTTI Context
猜你喜欢
  • 2012-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-09
  • 2017-01-08
相关资源
最近更新 更多