Technical the same but different generated code

By | October 31, 2017

While debugging the String4D code to hunt down a bug in the CompilerSpeedPack, I saw a lot of CopyRecord/FinalizeRecord calls with a try/finally that the compiler generated.

If you have a record with managed fields (string, interface, …) and use it as a function return type the Delphi compiler will change the function into a procedure with an extra var-parameter. So you would think that the compiler treats the result-parameter like a normal var-parameter, but that isn’t the case. The compiler will generate code that guarantees that the result record isn’t changed if the called function throws an exception. For this is adds a temporary record that is used for the function result and then copies it to the target record.

type
  TMyRec = record
    Value: string;
  end;

function InternalGetRec: TMyRec;
begin
  Result.Value := 'Hello';
end;

function GetRec: TMyRec;
begin
  Result := InternalGetRec;
end;

procedure Test;
var
  R: TMyRec;
begin
  R := GetRec;
end;

The compiler rewrites the “Test” function to:

procedure Test;
var
  R, TempR: TMyRec;
begin
  try
    GetRec(TempR);
    CopyRecord(TempR, R, TypeInfo(TMyRec));
  finally
    FinalizeArray([TempR, R], TypeInfo(TMyRec));
  end;
end;

The same happens if you assign another function’s record result value to your own record result value. The compiler rewrites the “GetRec” function’s code to:

function GetRec: TMyRec;
var
  TempResult: TMyRec;
begin
  try
    InternalGetRec(TempResult);
    CopyRecord(TempResult, Result, TypeInfo(TMyRec));
  finally
    FinalizeRecord(TempRecord, TypeInfo(TMyRec));
  end;
end;

Because the compiler assumes that you may want to use “Result” in the function after the call, it has to guarantee that it is unchanged if an exception is thrown. But if it is the last statement in the function and not secured by an explicit try/finally/except where “Result” is used again, an optimization could be to omit the temporary record, making the code a lot faster.