Delphi 的RTTI机制浅探

    技术2022-05-11  78

    目录==========================================================================⊙ RTTI 简介⊙ 类(class) 和 VMT 的关系⊙ 类(class)、类的类(class of class)、类变量(class variable) 的关系⊙ TObject.ClassType 和 TObject.ClassInfo⊙ is 和 as 运算符的原理⊙ TTypeInfo – RTTI 信息的结构⊙ 获取类(class)的属性(property)信息⊙ 获取方法(method)的类型信息⊙ 获取有序类型(ordinal)、集合(set)类型的 RTTI 信息⊙ 获取其它数据类型的 RTTI 信息==========================================================================正文==========================================================================⊙ RTTI 简介==========================================================================RTTI(Run-Time Type Information) 翻译过来的名称是“运行期类型信息”,也就是说可以在运行期获得数据类型或类(class)的信息。这个 RTTI 到底有什么用处,我现在也说不清楚。我是在阅读 Delphi 持续机制的代码中发现了很多 RTTI 的运用,只好先把 RTTI 学习一遍。下面是我的学习笔记。如果你发现了错误请告诉我。谢谢!Delphi 的 RTTI 主要分为类(class)的 RTTI 和一般数据类型的 RTTI,下面从类(class)开始。==========================================================================⊙ 类(class) 和 VMT 的关系==========================================================================一个类(class),从编译器的角度来看就是一个指向 VMT 的指针(在后文用 VMTptr 表示)。在类的 VMTptr 的负地址方向存储了一些类信息的指针,这些指针的值和指针所指的内容在编译后就确定了。比如 VMTptr - 44 的内容是指向类名称(ClassName)的指针。不过一般不使用数值来访问这些类信息,而是通过 System.pas 中定义的以 vmt 开头的常量,如 vtmClassName、vmtParent 等来访问。类的方法有两种:对象级别的方法和类级别的方法。两者的 Self 指针意义是不同的。在对象级别的方法中 Self 指向对象地址空间,因此可以用它来访问对象的成员函数;在类级别的方法中 Self 指向类的 VMT,因此只能用它来访问 VMT 信息,而不能访问对象的成员字段。==========================================================================⊙ 类(class)、类的类(class of class)、类变量(class variable) 的关系==========================================================================上面说到类(class) 就是 VMTptr。在 Delphi 中还可以用 class of 关键字定义类的类,并且可以使用类的类定义类变量。从语法上理解这三者的关键并不难,把类当成普通的数据类型来考虑就可以了。在编译器级别上表现如何呢?为了简化讨论,我们使用 TObject、TClass 和 TMyClass 来代表上面说的三种类型:type  TClass = class of TObject;var  TMyClass: TClass;  MyObject: TObject;begin  TMyClass := TObject;  MyObject := TObject.Create;  MyObject := TClass.Create;  MyObject := TMyClass.Create;end;  在上面的例子中,三个 TObject 对象都被成功地创建了。编译器的实现是:TObject 是一个 VMTPtr 常量。TClass 也是一个 VMTptr 常量,它的值就是 TObject。TMyClass 是一个 VMTptr 变量,它被赋值为 TObject。TObject.Create 与 TClass.Create 的汇编代码完全相同。但 TClass 不仅缺省代表一个类,而且还(主要)代表了类的类型,可以用它来定义类变量,实现一些类级别的操作。==========================================================================⊙ TObject.ClassType 和 TObject.ClassInfo==========================================================================function TObject.ClassType: TClass;begin  Pointer(Result) := PPointer(Self)^;end;TObject.ClassType 是对象级别的方法,Self 的值是指向对象内存空间的指针,对象内存空间的前 4 个字节是类的 VMTptr。因此这个函数的返回值就是类的 VMTptr。class function TObject.ClassInfo: Pointer;begin  Result := PPointer(Integer(Self) + vmtTypeInfo)^;end;TObject.ClassInfo 使用 class 关键字定义,因此是一个类级别的方法。该方法中的 Self 指针就是 VMTptr。所以这个函数的返回值是 VMTptr 负方向的 vmtTypeInfo 的内容。TObject.ClassInfo 返回的 Pointer 指针,实际上是指向类的 RTTI 结构的指针。但是不能访问 TObject.ClassInfo 指向的内容(TObject.ClassInfo 返回值是 0),因为 Delphi 只在 TPersistent 类及 TPersistent 的后继类中产生 RTTI 信息。(从编译器的角度来看,这是在 TPersistent 类的声明之前使用 {$M+} 指示字的结果。)TObject 还定义了一些获取类 RTTI 信息的函数,列举在下,就不一一分析了:  TObject.ClassName: ShortString;   类的名称  TObject.ClassParent: TClass;      对象的父类  TObject.InheritsFrom: Boolean;    是否继承自某类  TObject.InstanceSize: Longint;    对象实例的大小==========================================================================⊙ is 和 as 运算符的原理==========================================================================我们知道可以在运行期使用 is 关键字判断一个对象是否属于某个类,可以使用 as 关键字把某个对象安全地转换为某个类。在编译器的层次上,is 和 as 的操作是由 System.pas 中两个函数完成的。{ System.pas }function _IsClass(Child: TObject; Parent: TClass): Boolean;begin  Result := (Child <> nil) and Child.InheritsFrom(Parent);end;_IsClass 很简单,它使用 TObject 的 InheritsForm 函数判断该对象是否是从某个类或它的父类中继承下来的。每个类的 VMT 中都有一项 vmtParent 指针,指向该类的父类的 VMT。TObject.InheritsFrom 实际上是通过[递归]判断父类 VMT 指针是否等于自己的 VMT 指针来判断是否是从该类继承的。{ System.pas }class function TObject.InheritsFrom(AClass: TClass): Boolean;var  ClassPtr: TClass;begin  ClassPtr := Self;  while (ClassPtr <> nil) and (ClassPtr <> AClass) do    ClassPtr := PPointer(Integer(ClassPtr) + vmtParent)^;  Result := ClassPtr = AClass;end;as 操作符实际上是由 System.pas 中的 _AsClass 函数完成的。它简单地调用 is 操作符判断对象是否属于某个类,如果不是就触发异常。虽然 _AsClass 返回值为 TObject 类型,但编译器会自动把返回的对象改变为 Parent 类,否则返回的对象没有办法使用 TObject 之外的方法和数据。{ System.pas }function _AsClass(Child: TObject; Parent: TClass): TObject;begin  Result := Child;  if not (Child is Parent) then    Error(reInvalidCast);   // loses return addressend;==========================================================================⊙ TTypeInfo – RTTI 信息的结构==========================================================================RTTI 信息的结构定义在 TypInfo.pas 中:  TTypeInfo = record        // TTypeInfo 是 RTTI 信息的结构    Kind: TTypeKind;        // RTTI 信息的数据类型    Name: ShortString;      // 数据类型的名称   {TypeData: TTypeData}    // RTTI 的内容  end;TTypeInfo 就是 RTTI 信息的结构。TObject.ClassInfo 返回指向存放 class TTypeInfo 信息的指针。Kind 是枚举类型,它表示 RTTI 结构中所包含数据类型。Name 是数据类型的名称。注意,最后一个字段 TypeData 被注释掉了,这说明该处的结构内容根据不同的数据类型有所不同。TTypeKind 枚举定义了可以使用 RTTI 信息的数据类型,它几乎包含了所有的 Delphi 数据类型,其中包括 tkClass。  TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,    tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,    tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);TTypeData 是个巨大的记录类型,在此不再列出,后文会根据需要列出该记录的内容。==========================================================================⊙ 获取类(class)的属性(property)信息==========================================================================这一段是 RTTI 中最复杂的部分,努力把本段吃透,后面的内容都是非常简单的。下面是一个获取类的属性的例子:procedure GetClassProperties(AClass: TClass; AStrings: TStrings);var  PropCount, I: SmallInt;  PropList: PPropList;  PropStr: string;begin  PropCount := GetTypeData(AClass.ClassInfo).PropCount;  GetPropList(AClass.ClassInfo, PropList);  for I := 0 to PropCount - 1 do  begin    case PropList[I]^.PropType^.Kind of      tkClass      : PropStr := '[Class] ';      tkMethod     : PropStr := '[Method]';      tkSet        : PropStr := '[Set]   ';      tkEnumeration: PropStr := '[Enum]  ';    else      PropStr := '[Field] ';    end;    PropStr := PropStr + PropList[I]^.Name;    PropStr := PropStr + ': ' + PropList[I]^.PropType^.Name;    AStrings.Add(PropStr);  end;  FreeMem(PropList);end;你可以在表单上放置一个 TListBox ,然后执行以下语句观察执行结果:  GetClassProperties(TForm1, ListBox1.Items);该函数先使用 GetTypeData 函数获得类的属性数量。GetTypeData 是 TypInfo.pas 中的一个函数,它的功能是返回 TTypeInfo 的 TypeData 数据的指针:{ TypInfo.pas }function GetTypeData(TypeInfo: PTypeInfo): PTypeData; assembler;class 的 TTypeData 结构如下:  TTypeData = packed record    case TTypeKind of      tkClass: (        ClassType: TClass;         // 类 (VMTptr)        ParentInfo: PPTypeInfo;    // 父类的 RTTI 指针        PropCount: SmallInt;       // 属性数量        UnitName: ShortStringBase; // 单元的名称       {PropData: TPropData});     // 属性的详细信息  end;其中的 PropData 又是一个大小可变的字段。TPropData 的定义如下:  TPropData = packed record    PropCount: Word;       // 属性数量    PropList: record end;  // 占位符,真正的意义在下一行    {PropList: array[1..PropCount] of TPropInfo}  end;每个属性信息在内存中的结构就是 TPropInfo,它的定义如下:  PPropInfo = ^TPropInfo;  TPropInfo = packed record    PropType: PPTypeInfo;    // 属性类型信息指针的指针    GetProc: Pointer;        // 属性的 Get 方法指针    SetProc: Pointer;        // 属性的 Set 方法指针    StoredProc: Pointer;     // 属性的 StoredProc 指针    Index: Integer;          // 属性的 Index 值    Default: Longint;        // 属性的 Default 值    NameIndex: SmallInt;     // 属性的名称索引(以 0 开始计数)    Name: ShortString;       // 属性的名称  end;为了方便访问属性信息,TypInfo.pas 中还定义了指向 TPropInfo 数组的指针:  PPropList = ^TPropList;  TPropList = array[0..16379] of PPropInfo;我们可以使用 GetPropList 获得所有属性信息的指针数组,数组用完以后要记得用 FreeMem 把数组的内存清除。{ TypInfo.pas }function GetPropList(TypeInfo: PTypeInfo; out PropList: PPropList): Integer;GetPropList 传入类的 TTypeInfo 指针和 TPropList 的指针,它为 PropList 分配一块内存后把该内存填充为指向 TPropInfo 的指针数组,最后返回属性的数量。上面的例子演示了如何获得类的所有属性信息,也可以根据属性的名称单独获得属性信息:{ TypInfo.pas }function GetPropInfo(TypeInfo: PTypeInfo; const PropName: string): PPropInfo;GetPropInfo 根据类的 RTTI 指针和属性的名称字符串,返回属性的信息 TPropInfo 的指针。如果没有找到该属性,则返回 nil。GetPropInfo 很容易使用,举个例子:  ShowMessage(GetPropInfo(TForm, 'Name')^.PropType^.Name);这句调用显示了 TForm 类的 Name 属性的类型名称:TComponentName。==========================================================================⊙ 获取方法(method)的类型信息==========================================================================所谓方法就是以 of object 关键字声明的函数指针,下面的函数可以显示一个方法的类型信息:procedure GetMethodTypeInfo(ATypeInfo: PTypeInfo; AStrings: TStrings);type  PParamData = ^TParamData;  TParamData = record       // 函数参数的数据结构    Flags: TParamFlags;     // 参数传递规则    ParamName: ShortString; // 参数的名称    TypeName: ShortString;  // 参数的类型名称  end;  function GetParamFlagsName(AParamFlags: TParamFlags): string;  var    I: Integer;  begin    Result := '';    for I := Integer(pfVar) to Integer(pfOut) do begin      if I = Integer(pfAddress) then Continue;      if TParamFlag(I) in AParamFlags then        Result := Result + ' ' + GetEnumName(TypeInfo(TParamFlag), I);    end;  end;var  MethodTypeData: PTypeData;  ParamData: PParamData;  TypeStr: PShortString;  I: Integer;begin  MethodTypeData := GetTypeData(ATypeInfo);  AStrings.Add('---------------------------------');  AStrings.Add('Method Name: ' + ATypeInfo^.Name);  AStrings.Add('Method Kind: ' + GetEnumName(TypeInfo(TMethodKind),    Integer(MethodTypeData^.MethodKind)));  AStrings.Add('Params Count: '+ IntToStr(MethodTypeData^.ParamCount));  AStrings.Add('Params List:');  ParamData := PParamData(@MethodTypeData^.ParamList);  for I := 1 to MethodTypeData^.ParamCount do  begin    TypeStr := Pointer(Integer(@ParamData^.ParamName) +      Length(ParamData^.ParamName) + 1);    AStrings.Add(Format('  [%s] %s: %s',[GetParamFlagsName(ParamData^.Flags),      ParamData^.ParamName, TypeStr^]));    ParamData := PParamData(Integer(ParamData) + SizeOf(TParamFlags) +      Length(ParamData^.ParamName) + Length(TypeStr^) + 2);  end;  if MethodTypeData^.MethodKind = mkFunction then    AStrings.Add('Result Value: ' + PShortString(ParamData)^);end;作为实验,在表单上放置一个 TListBox,然后执行以下代码,观察执行结果:type  TMyMethod = function(A: array of Char; var B: TObject): Integer of object;procedure TForm1.FormCreate(Sender: TObject);begin  GetMethodTypeInfo(TypeInfo(TMyMethod), ListBox1.Items);  GetMethodTypeInfo(TypeInfo(TMouseEvent), ListBox1.Items);  GetMethodTypeInfo(TypeInfo(TKeyPressEvent), ListBox1.Items);  GetMethodTypeInfo(TypeInfo(TMouseWheelEvent), ListBox1.Items);end;由于获取方法的类型信息比较复杂,我尽量压缩代码也还是有这么长,让我们看看它的实现原理。GetMethodTypeInfo 的第一个参数是 PTypeInfo 类型,表示方法的类型信息地址。第二个参数是一个字符串列表,可以使用任何实现 TStrings 操作的对象。我们可以使用 System.pas 中的 TypeInfo 函数获得任何类型的 RTTI 信息指针。TypeInfo 函数像 SizeOf 一样,是内置于编译器中的。GetMethodTypeInfo 还用到了 TypInfo.pas 中的 GetEnumName 函数。这个函数通过枚举类型的整数值得到枚举类型的名称。function GetEnumName(TypeInfo: PTypeInfo; Value: Integer): string;与获取类(class)的属性信息类似,方法的类型信息也在 TTypeData 结构中  TTypeData = packed record    case TTypeKind of      tkMethod: (        MethodKind: TMethodKind;            // 方法指针的类型        ParamCount: Byte;                   // 参数数量        ParamList: array[0..1023] of Char   // 参数详细信息,见下行注释       {ParamList: array[1..ParamCount] of          record            Flags: TParamFlags;             // 参数传递规则             ParamName: ShortString;         // 参数的名称            TypeName: ShortString;          // 参数的类型          end;        ResultType: ShortString});          // 返回值的名称  end;TMethodKind 是方法的类型,定义如下:  TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor,    mkClassProcedure, mkClassFunction,    { Obsolete }    mkSafeProcedure, mkSafeFunction);TParamsFlags 是参数传递的规则,定义如下:  TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut);  TParamFlags = set of TParamFlag;由于 ParamName 和 TypeName 是变长字符串,不能直接取用该字段的值,而应该使用指针步进的方法,取出参数信息,所以上面的代码显得比较长。==========================================================================⊙ 获取有序类型(ordinal)、集合(set)类型的 RTTI 信息==========================================================================讨论完了属性和方法的 RTTI 信息之后再来看其它数据类型的 RTTI 就简单多了。所有获取 RTTI 的原理都是通过 GetTypeData 函数得到 TTypeData 的指针,再通过 TTypeInfo.TypeKind 来解析 TTypeData。任何数据类型的 TTypeInfo 指针可以通过 TypeInfo 函数获得。有序类型的 TTypeData 定义如下:TTypeData = packed record  tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (    OrdType: TOrdType;         // 有序数值类型    case TTypeKind of      case TTypeKind of        tkInteger, tkChar, tkEnumeration, tkWChar: (          MinValue: Longint;   // 类型的最小值          MaxValue: Longint;   // 类型的最大值          case TTypeKind of            tkInteger, tkChar, tkWChar: ();            tkEnumeration: (              BaseType: PPTypeInfo;      // 指针的指针,它指向枚举的 PTypeInfo              NameList: ShortStringBase;     // 枚举的名称字符串(不能直接取用)              EnumUnitName: ShortStringBase)); // 所在的单元名称(不能直接取用)          tkSet: (            CompType: PPTypeInfo));            // 指向集合基类 RTTI 指针的指针end;下面是一个获取有序类型和集合类型的 RTTI 信息的函数:procedure GetOrdTypeInfo(ATypeInfo: PTypeInfo; AStrings: TStrings);var  OrdTypeData: PTypeData;  I: Integer;begin  OrdTypeData := GetTypeData(ATypeInfo);  AStrings.Add('------------------------------------');  AStrings.Add('Type Name: ' + ATypeInfo^.Name);  AStrings.Add('Type Kind: ' + GetEnumName(TypeInfo(TTypeKind),    Integer(ATypeInfo^.Kind)));  AStrings.Add('Data Type: ' + GetEnumName(TypeInfo(TOrdType),    Integer(OrdTypeData^.OrdType)));  if ATypeInfo^.Kind <> tkSet then begin    AStrings.Add('Min Value: ' + IntToStr(OrdTypeData^.MinValue));    AStrings.Add('Max Value: ' + IntToStr(OrdTypeData^.MaxValue));  end;  if ATypeInfo^.Kind = tkSet then    GetOrdTypeInfo(OrdTypeData^.CompType^, AStrings);  if ATypeInfo^.Kind = tkEnumeration then    for I := OrdTypeData^.MinValue to OrdTypeData^.MaxValue do      AStrings.Add(Format('  Value %d: %s', [I, GetEnumName(ATypeInfo, I)]));end;在表单上放置一个 TListBox,运行以下代码查看结果:type TMyEnum = (EnumA, EnumB, EnumC);procedure TForm1.FormCreate(Sender: TObject);begin  GetOrdTypeInfo(TypeInfo(Char), ListBox1.Items);  GetOrdTypeInfo(TypeInfo(Integer), ListBox1.Items);  GetOrdTypeInfo(TypeInfo(TFormBorderStyle), ListBox1.Items);  GetOrdTypeInfo(TypeInfo(TBorderIcons), ListBox1.Items);  GetOrdTypeInfo(TypeInfo(TMyEnum), ListBox1.Items);end;(如果枚举元素没有按缺省的 0 基准定义,那么将不能产生 RTTI 信息,为什么?)==========================================================================⊙ 获取其它数据类型的 RTTI 信息==========================================================================上面讨论了几个典型的 RTTI 信息的运行,其它的数据类型的 RTTI 信息的获取方法与上面类似。由于这些操作更加简单,就不一一讨论。下面概述其它类型的 RTTI 信息的情况:LongString、WideString 和 Variant 没有 RTTI 信息;ShortString 只有 MaxLength 信息;浮点数类型只有 FloatType: TFloatType 信息;  TFloatType = (ftSingle, ftDouble, ftExtended, ftComp, ftCurr);Int64 只有最大值和最小值信息(也是 64 位整数表示);Interface 和动态数组不太熟悉,就不作介绍了。 

    最新回复(0)