2
votes

I present you a bug in the Delphi 5 compiler. I know there's not going to be any fix for it; but a workaround would be super

program Project1;

uses
  Dialogs, SysUtils;

{$R *.RES}

type
    IFoo = interface
        ['{D68DA49A-F870-433D-9343-4964BFECFF27}']
        procedure Grob(a: Integer; b: Integer);
    end;

    TFoo = class(TInterfacedObject, IFoo)
    public
        procedure Grob(a: Integer; b: Integer); virtual;
    end;

procedure TFoo.Grob(a: Integer; b: Integer);
begin

end;

function DoStuff(): Integer;
var
    foo: IFoo;
begin
    foo := TFoo.Create;
    try
        Result := 1;
        Exit;
    finally
        foo.Grob(0, 0);
    end;

    Result := 2;
end;

var
    n: Integer;
begin
    n := DoStuff;
    if n <> 0 then
        ShowMessage('Failed: '+IntToStr(n))
    else
        ShowMessage('Passed: '+IntToStr(n));

end.

The real guts is the function DoStuff which should return one:

function DoStuff(): Integer;
var
    foo: IFoo;
begin
    foo := TFoo.Create;
    try
        Result := 1;
        Exit;
    finally
        foo.Grob(0, 0);
    end;

    Result := 2;
end;

The function should return one. Instead it returns the address of the interfaced object:

enter image description here

The assembly

The code actually does start to set the result to one:

Project1.dpr.30: Result := 1;
    mov ebx,$00000001     ; place return value 1 in EBX
Project1.dpr.31: Exit;
    call @TryFinallyExit  ; call the finally block
    jmp DoStuff + $6E

and as the function is about to return, it does copy EBX into EAX in order to return it:

    mov eax,ebx           ;EBX into EAX for return

But finally block (calling the interfaced method) is the problem. It blows away the return value stored in EBX:

We arrive here from the call @TryFinallyExit
Project1.dpr.33: foo.Grob(0, 0);
    xor ecx,ecx
    xor edx,edx
    mov eax,[ebp-$04]
    mov ebx,[eax]  <----- overwriting ebx with interface address
    call dword ptr [ebx+$0c]
    ret

After the "call" to the finally block, it returns to a jump, which sends it to:

Project1.dpr.36: Result := 2;
...
    xor eax,eax
    pop edx
    pop ecx
    pop ecx
    mov fs:[eax],edx
    push $00442e1f
    lea eax,[ebp-$04]
    call @IntfClear
    ret
...
    mov eax,ebx  <----- places overwritten EBX into EAX for return
Project1.dpr.37: end;
    pop ebx
    pop ecx
    pop ebp
    ret

The return value rather than being one, or two, is the address of the interface pointer.

I know none of you have Delphi 5. And even if you did,

"What would you like me to say?"

I know the difficulty. What i actually need is some sort of workaround.

1
The problem I see here, is that we can produce a workaround to this code, but then it likely won't help with the real code ........David Heffernan
I would try making "grob" not virtual and see if it fixes the problem. If it does, you can always make it call a virtual "DoGrod" or something similar.Ken Bourassa

1 Answers

3
votes

As you observed, the compiler is storing the result into EBX, but then overwriting it before it subsequently copies EBX into EAX to return the result to the caller.

The compiler should be doing one of the following:

  1. Using a different register to store the result value temporarily, so that its use of EBX does not destroy the result value, or
  2. Not using EBX in the call to Grob, or
  3. Storing the result value in something more persistent than a register, like on the stack.

Obviously options 1 and 2 are not readily available to you, but the latter is the workaround that you need to implement in this example – use a local variable to hold your intended Result value until you are ready to return it:

function DoStuff(): Integer;
var
  foo: IFoo;
  MyResult: Integer;
begin
  foo := TFoo.Create;
  try
    try
      MyResult := 1;
      Exit;
    finally
      foo.Grob(0, 0);
    end;

    MyResult := 2;
  finally
    Result := MyResult;
  end;
end;