Search

Search all blog posts and tutorials by any word or term

← NewsAugust 5, 20268 min read

Delphi

RTTI Part 3: Invoking Methods — and a Real Tool

  • Delphi
  • RTTI
  • Reflection
  • RTL
  • Object Pascal

We've come a long way. Part 1 explained what RTTI is — reflection, code that inspects types it didn't know at compile time — and introduced TRttiContext and TValue. Part 2 read an object's properties, fields, and attributes, and wrote values back by name with GetValue/SetValue. There's one verb left, and it's the most dramatic: calling a method by its name, as a string.

If reading a property by name felt like a small magic trick, invoking a method by name is the whole show. It means you can call DoThing on an object when the string 'DoThing' only arrives at runtime — from a script, a config file, a message from a server, a plugin. It's how command dispatchers, scripting bridges, and test runners work.

Part 3 covers invocationTRttiMethod.Invoke, passing arguments and reading return values as TValue — and then, to tie the whole series together, we'll build one small, genuinely useful tool with everything we've learned: a generic object-to-CSV exporter that works on any list of any class, using nothing but reflection. Let's finish strong.

Calling a method by name

The setup mirrors Part 2 exactly: open a context, get the type, but this time ask for a method and Invoke it. Suppose we have a class with a method that does something and returns a result:

type
  TCalculator = class
  public
    function Add(A, B: Integer): Integer;
    procedure Reset;
  end;

Here's how you call Add(2, 3) when 'Add' is a runtime string — note that the arguments go in as an array of TValue, and the result comes back as a TValue:

uses
  System.Rtti;
 
var
  Ctx: TRttiContext;
  Calc: TCalculator;
  Method: TRttiMethod;
  Res: TValue;
begin
  Calc := TCalculator.Create;
  Ctx := TRttiContext.Create;
  try
    Method := Ctx.GetType(TCalculator).GetMethod('Add');
    Res := Method.Invoke(Calc, [2, 3]);      // call Add(2, 3)
    ShowMessage(Res.AsInteger.ToString);      // '5'
  finally
    Ctx.Free;
    Calc.Free;
  end;
end;

Look at the signature the RTL gives us: Invoke(Instance: TObject; const Args: array of TValue): TValue. Every argument is a TValue (the integers 2 and 3 box themselves via the implicit operators from Part 1), and the return value is a TValue you unwrap with AsInteger, AsString, or whichever accessor fits. For a procedure like Reset that returns nothing, you simply ignore the (empty) result.

Invoke: name + boxed args go in, a boxed result comes back

The diagram is the entire mechanism: a method name plus boxed arguments go in, the method runs on your live object, and a boxed result comes back. A command dispatcher is just this call with the name and args pulled from an incoming message.

The capstone: a generic CSV exporter

Let's put all three parts to work on something you might actually ship. The goal: export any TObjectList<T> to CSV — header row from the property names, one line per object — with a single routine that knows nothing about the classes it's handed. This is the kind of small win reflection is perfect for.

We'll even let a class opt a property out of the export with a custom attribute, tying in Part 2. First the attribute:

type
  // Mark a property to be skipped by the exporter (Part 2's attributes)
  NoExportAttribute = class(TCustomAttribute);
 
  TEmployee = class
  private
    FName: string;
    FSalary: Currency;
    FPasswordHash: string;
  public
    property Name: string read FName write FName;
    property Salary: Currency read FSalary write FSalary;
    [NoExport]                                   // never export this
    property PasswordHash: string read FPasswordHash write FPasswordHash;
  end;

Now the exporter. It walks the properties once to build the header (skipping anything marked [NoExport]), then walks each object's values to build the rows — using GetProperties, GetValue, HasAttribute<T>, and TValue.ToString from across the series:

uses
  System.Rtti, System.SysUtils, System.Classes, System.Generics.Collections;
 
function ExportToCsv<T: class>(const Items: TObjectList<T>): string;
var
  Ctx: TRttiContext;
  Typ: TRttiType;
  Props: TArray<TRttiProperty>;
  Prop: TRttiProperty;
  Item: T;
  Line: TArray<string>;
  SB: TStringBuilder;
 
  // include a property only if it's readable and not marked [NoExport]
  function Exported(const P: TRttiProperty): Boolean;
  begin
    Result := P.IsReadable and not P.HasAttribute<NoExportAttribute>;
  end;
 
begin
  Ctx := TRttiContext.Create;
  SB := TStringBuilder.Create;
  try
    Typ := Ctx.GetType(TClass(T));
    Props := Typ.GetProperties;
 
    // 1. Header row — property names
    Line := [];
    for Prop in Props do
      if Exported(Prop) then
        Line := Line + [Prop.Name];
    SB.AppendLine(string.Join(',', Line));
 
    // 2. One row per object — property values via reflection
    for Item in Items do
    begin
      Line := [];
      for Prop in Props do
        if Exported(Prop) then
          Line := Line + [Prop.GetValue(Item).ToString];
      SB.AppendLine(string.Join(',', Line));
    end;
 
    Result := SB.ToString;
  finally
    SB.Free;
    Ctx.Free;
  end;
end;

Feed it a list of employees and you get:

Name,Salary
Ada Lovelace,95000
Grace Hopper,102000

PasswordHash is absent — the [NoExport] attribute did its job — and, crucially, this routine will export TCustomer, TInvoice, or any class you write next year, with zero changes. That's the whole promise of reflection delivered in about forty lines: write the walker once, reap it forever. Here's the flow at a glance.

One generic walker: property names become the header, values become rows

The diagram is the tool in one line: a list of unknown objects goes in, the reflection walk turns property names into a header and property values into rows, and CSV comes out — no per-class code anywhere.

The other side: is reflection the right call here?

Guarantee to the reader — even for a nice tool, ask whether reflection earns its place. For a CSV exporter that must handle arbitrary classes across your codebase, yes: the alternative is a hand-written exporter per class, which is exactly the boilerplate reflection exists to erase. But if you only ever export one known class, a plain hand-written function is simpler, faster, and safer — no TValue boxing, full compile-time checking. The rule from Part 1 holds all the way to the end: reflect when you're writing code that must span many types you don't control; write direct code when you know the type. The exporter qualifies because "any class" is in its very signature.

Takeaways — the whole series

Three parts took us from "what is RTTI" to a working tool. The complete picture:

  • Invoke a method by name with TRttiMethod.Invoke(Instance, [args]) — arguments go in as TValue, the result comes back as TValue. The overloads even let you call constructors and class methods reflectively (the heart of DI containers).
  • Reflection loses compile-time checking: wrong arguments raise EInvocationError at runtime. Guard invocations built from external input.
  • The capstone exporter shows the payoff: GetProperties + GetValue + HasAttribute<T> produce a CSV writer that works on any class, written once — the essence of every serializer and mapper.
  • The through-line of all three parts: reflect when your code must span types you don't control; write direct code when you know the type.

Invocation completes the trio — read (GetValue), write (SetValue), and call (Invoke) any member by name — turning "types as data" into working tools like a class-agnostic CSV exporter in a few dozen lines.

That closes the RTTI series. If you want the foundations again, Part 1 is the "what and why" and Part 2 is properties and attributes. You now understand the machinery behind Delphi's serializers, ORMs, and DI containers — and you can build your own when the need is genuinely yours. Go reflect on something.

Free to read, not free to make. If this article saved you time or taught you something, there's a way to give back.

How to support