Skip to content

Latest commit

 

History

History
194 lines (165 loc) · 10 KB

Rtti.md

File metadata and controls

194 lines (165 loc) · 10 KB

Concept

The basis of the RTTI units we laid the following conceptual features:

  • Standard types, including FreePascal, old and new versions of Delphi, convenient functions for managing them.
  • Universal data representation, regardless of programming language and compiler version.
  • Convert standard RTTI to universal data representation.
  • Minimizing dependencies on heavy units like Classes, Variants, Generics, etc.
  • Cross-platform functions invoke, including FreePascal and old versions of Delphi, creation of function and interface interpreters.
  • Marshalling (serialization and deserialization of data) through different formatters: JSON, XML, FlexBin and others.
  • Easy to use unit testing library that does not access the memory manager.

The following sections deserve special attention:

Compatibility

The library is created with the prospect of using in different programming languages, but primarily for Delphi, FreePascal and C++Builder. The main unit is Tiny.Rtti.pas, it contains the main types and functions of the library. One of the key ideas of the unit is to ensure code compatibility on different versions of Delphi or FreePascal, therefore, RTTI types are TTypeInfo, TTypeData, etc. reduced to a single naming and interface. Another feature of the library is that the internal types and functions match the units of System.TypInfo.pas and System.Rtti.pas as closely as possible. For example, there are the usual GetTypeData, GetEnumName, IsManaged and HasWeakRef functions, there is a TValue type, including for old Delphi and FreePascal.

Universal data representation

Despite the fact that a significant part of the library works with RTTI, its essence boils down to the universal representation of data: types and information about them. To store the base type, the TRttiType enumeration is used. To classify the types, the TRttiTypeGroup enumeration is used. There is a standard set of types and groups, but you may always extend this set with the RttiTypeIncrease and RttiTypeIncreaseGroup functions. For a detailed description of the type, the TRttiExType structure is used - it stores the base type, pointer depth, options and additional meta information.

Context

Typically, context is used to convert TypeInfo into the universal data representation. You may use the DefaultContext variable for these purposes. The context functionality can be expanded, for example, to store information about classes, interfaces, properties and methods. For storing and caching a namespace, there is a type TRttiNamespace (Tiny.Namespace.pas).

On older versions of Delphi, RTTI is not generated for some types, for example, PAnsiChar. Therefore, the library supports the concept of PTypeInfo equivalents that will be correctly converted by the context. Use dummy constants (TYPEINFO_PANSICHAR, TYPEINFO_UINT64, etc.) or the DummyTypeInfo function for such cases.

TValue benchmark

TValue is a lightweight analogue of the Variant type. This type supports almost all types available in Tiny.Rtti, an exception is made only by all pointer types, they are all cast to Pointer. The functional largely repeats the System.Rtti implementation, the difference is only in increasing the number of As-properties and reducing the functions of the casts. In addition, much attention was paid to optimizations, you may see this on the benchmark below. Type TValue and the benchmark were created with the participation of Alexander Zhirov.

Invoke benchmark

In some cases, for example, when binding code with scripts, or when performing automatic tests, there is a need to invoke your native functions. In older versions of Delphi, this functionality was not available, but in newer versions, the call occurs with low performance. The Tiny.Invoke.pas unit allows you to invoke functions at 3 levels of abstraction (values, arguments, direct), the benchmark below shows how to do this and measures performance.

TRttiSignature structure stores service information about the function signature: calling convention, argument description, register and stack information. The TRttiInvokeDump structure is used to store arguments in memory.

procedure TForm1.SomeMethod(const X, Y, Z: Integer);
begin
  Tag := X + Y + Z;
end;

procedure TForm1.Button1Click(Sender: TObject);
const
  COUNT = 1000000;
var
  i: Integer;
  LStopwatch: TStopwatch;
  LContext: System.Rtti.TRttiContext;
  LMethod: System.Rtti.TRttiMethod;
  LMethodEntry: Tiny.Rtti.PVmtMethodExEntry;
  LSignature: Tiny.Invoke.TRttiSignature;
  LInvokeFunc: Tiny.Invoke.TRttiInvokeFunc;
  LDump: Tiny.Invoke.TRttiInvokeDump;
  T1, T2, T3, T4: Int64;
begin
  // initialization
  LContext := System.Rtti.TRttiContext.Create;
  LMethod := LContext.GetType(TForm1).GetMethod('SomeMethod');
  LMethodEntry := Tiny.Rtti.PTypeInfo(TypeInfo(TForm1)).TypeData.ClassData.MethodTableEx.Find('SomeMethod');
  LSignature.Init(LMethodEntry^);
  LInvokeFunc := LSignature.OptimalInvokeFunc;

  // System.Rtti
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    LMethod.Invoke(Form1, [1, 2, 3]);
  end;
  T1 := LStopwatch.ElapsedMilliseconds;

  // Tiny.Rtti(Invoke) values
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    LSignature.Invoke(LDump, LMethodEntry.CodeAddress, Form1, {TValue}[1, 2, 3], LInvokeFunc);
  end;
  T2 := LStopwatch.ElapsedMilliseconds;

  // Tiny.Rtti(Invoke) arguments
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    LSignature.Invoke(LDump, LMethodEntry.CodeAddress, Form1, {array of}[1, 2, 3], nil, LInvokeFunc);
  end;
  T3 := LStopwatch.ElapsedMilliseconds;

  // Tiny.Rtti(Invoke) direct
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    PPointer(@LDump.Bytes[LSignature.DumpOptions.ThisOffset])^ := Form1;
    PInteger(@LDump.Bytes[LSignature.Arguments[0].Offset])^ := 1;
    PInteger(@LDump.Bytes[LSignature.Arguments[1].Offset])^ := 2;
    PInteger(@LDump.Bytes[LSignature.Arguments[2].Offset])^ := 3;
    LInvokeFunc(@LSignature, LMethodEntry.CodeAddress, @LDump);
  end;
  T4 := LStopwatch.ElapsedMilliseconds;

  // result
  Caption := Format('System.Rtti: %dms, Tiny.Rtti (values): %dms, ' +
    'Tiny.Rtti (args): %dms, Tiny.Rtti (direct): %dms', [T1, T2, T3, T4]);
end;

Virtual interface benchmark

Virtual interfaces can be used, for example, for high-level marshalling, when a native function call leads to the conversion of the arguments into binary form and sending them to server. The idea of a virtual interface is that you intercept the methods you call and process the arguments as you like. The Tiny.Invoke.pas unit allows you to intercept interface methods at 2 levels of abstraction: values and direct. At the direct level, the structures TRttiSignature and TRttiInvokeDump, which are described above, are important.

Unlike the implementation of System.Rtti.pas, the library allows you to redefine the method context (not TRttiContext) and the callback for each method. The benchmark below demonstrates the functionality of a virtual interface and compares performance.

type
  IMyInterface = interface(IInvokable)
    ['{89EDBA5C-DFBA-48FA-889C-FC857B0ED609}']
    function Func(const X, Y, Z: Integer): Integer;
  end;

procedure TForm1.Button1Click(Sender: TObject);
const
  COUNT = 1000000;
var
  i: Integer;
  LStopwatch: TStopwatch;
  LInterface: IMyInterface;
  LValue: Integer;
  T1, T2, T3: Int64;
begin
  // System.Rtti virtual interface
  LInterface := System.Rtti.TVirtualInterface.Create(TypeInfo(IMyInterface),
    procedure(Method: System.Rtti.TRttiMethod;
      const Args: TArray<System.Rtti.TValue>; out Result: System.Rtti.TValue)
    begin
      Result := Args[1].AsInteger + Args[2].AsInteger + Args[3].AsInteger;
    end) as IMyInterface;
  LValue := LInterface.Func(1, 2, 3);
  Assert(LValue = (1 + 2 + 3), 'System.Rtti virtual interface');
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    LInterface.Func(1, 2, 3);
  end;
  T1 := LStopwatch.ElapsedMilliseconds;

  // Tiny.Rtti(Invoke) virtual interface
  LInterface := Tiny.Invoke.TRttiVirtualInterface.Create(TypeInfo(IMyInterface),
    function(const AMethod: Tiny.Invoke.TRttiVirtualMethod;
      const AArgs: TArray<Tiny.Rtti.TValue>; const AReturnAddress: Pointer): TValue
    begin
      Result := AArgs[1].AsInteger + AArgs[2].AsInteger + AArgs[3].AsInteger;
    end) as IMyInterface;
  LValue := LInterface.Func(1, 2, 3);
  Assert(LValue = (1 + 2 + 3), 'Tiny.Rtti(Invoke) virtual interface');
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    LInterface.Func(1, 2, 3);
  end;
  T2 := LStopwatch.ElapsedMilliseconds;

  // Tiny.Rtti(Invoke) direct virtual interface
  LInterface := Tiny.Invoke.TRttiVirtualInterface.CreateDirect(TypeInfo(IMyInterface),
     procedure(const AMethod: Tiny.Invoke.TRttiVirtualMethod; var ADump: Tiny.Invoke.TRttiInvokeDump)
     var
       LSignature: Tiny.Invoke.PRttiSignature;
     begin
       LSignature := AMethod.Signature;
       ADump.OutInt32 := PInteger(@ADump.Bytes[LSignature.Arguments[0].Offset])^ +
         PInteger(@ADump.Bytes[LSignature.Arguments[1].Offset])^ +
         PInteger(@ADump.Bytes[LSignature.Arguments[2].Offset])^;
     end) as IMyInterface;
  LValue := LInterface.Func(1, 2, 3);
  Assert(LValue = (1 + 2 + 3), 'Tiny.Rtti(Invoke) direct virtual interface');
  LStopwatch := TStopwatch.StartNew;
  for i := 1 to COUNT do
  begin
    LInterface.Func(1, 2, 3);
  end;
  T3 := LStopwatch.ElapsedMilliseconds;

  // result
  Caption := Format('System.Rtti: %dms, Tiny.Rtti (values): %dms, Tiny.Rtti (direct): %dms', [T1, T2, T3]);
end;