Thursday, June 14, 2012

Bug or no bug - that is the question

Or with other words: when something is not what it looks to be - and you have no clue why.

Let me explain: Recently over on Nicks blog Márton mentioned that TRttiMethod.Invoke cannot handle var and out parameters. While I already created a runtime patch for the bug in the QC entry for 2010 and XE I was not sure about the handling of var and out parameters. I remembered I ran into some problem with calling the Invoke routine and Barry gave me the correct hint on how to handle passing values by reference. So I tried it:


program Project1;

{$APPTYPE CONSOLE}

uses
  Rtti;

type
  TTest = class
  public
    procedure CallVar(var i: Integer);
  end;

procedure TTest.CallVar(var i: Integer);
begin
  Inc(i);
end;

var
  test: TTest;
  ctx: TRttiContext;
  t: TRttiType;
  m: TRttiMethod;
  i: Integer;
begin
  test := TTest.Create;
  t := ctx.GetType(TTest);
  m := t.GetMethod('CallVar');
  i := 42;
  m.Invoke(test, [TValue.From<Integer>(i)]);
  Writeln(i);
  test.Free;
  Readln;
end.

It showed 42. First thought: yes, he is right, it does not handle them correctly. Second thought: wait, TValue is not taking any kind of reference to i. It just takes the value and stores it. So I changed the program a bit to check what was in the passed TValue argument.


var
  [...]
  v: TValue;
begin
  [...]
  v := TValue.From<Integer>(i);
  m.Invoke(test, [v]);
  Writeln(v.AsInteger);
  [...]
end.

Output remained 42. I messed around with passing the pointer to i inside the TValue but then the invoke method raised the EInvalidCast exception telling me: 'VAR and OUT arguments must match parameter type exactly'. I knew that this method checks this (not like the Invoke routine mentioned earlier) and passes them correctly. So what was going on? I changed it again:

var
  [...]
  v: TArray<TValue>;
begin
  [...]
  SetLength(v, 1);
  v[0] := TValue.From<Integer>(i);
  m.Invoke(test, v);
  Writeln(v[0].AsInteger);
  [...]
end.

Hooray, it showed the expected 43. What happened here? In the case where it did not work I used the open array constructor. The documentation says that it equivalent to passing a static array filled with the values passed to the open array constructor. Ok, I tested that:

var
  [...]
  v: array[0..0] of TValue;
begin
  [...]
  v[0] := TValue.From<Integer>(i);
  m.Invoke(test, v);
  Writeln(v[0].AsInteger);
  [...]
end.

Guess what? It returns 43. Seems it is not equivalent. Could it be that the code the compiler creates for an open array constructor does not handle the nested record inside of TValue - TValueData where the actual value is stored in - correctly? I was staring at the assembler code but as you know I pretty much suck reading something out there. While I was glad that TRttiMethod.Invoke actually handles var and out parameters correctly I could make a fix for DSharp mocks. But I still have no clue why passing the value with the open array constructor does not keep the value. Any ideas?