fix(native): release the owned ref of an interface-call-result argument
Show(MakeFoo(42)), where MakeFoo returns an interface, handed the borrowing parameter an owned (+1) fat pointer that was never released after the call — --debug reported one leaked instance on native (QBE already released it). Added an akIntfConsume hoist kind: EmitArgHoist drives the interface-returning call in the pre-pass and saves the owned fat pointer (itab then obj, obj on top); the argument reload pushes both halves as the two interface arg slots; and EmitHoistEpilogue releases the obj after the call (mirrors akStrConsume). The 2-slot reload is wired into both EmitArgPush and EmitCall's inline argument loop. --debug leak report is now clean on both backends; two leakcheck tests (QBE + native) are added. All three fixpoints green; 3484 tests pass.
This commit is contained in:
parent
2d8497f2b3
commit
a6378b39e2
|
|
@ -40,6 +40,8 @@ const
|
|||
akRecCall = 2; { record-returning call arg: sret buffer + saved ptr }
|
||||
akStrPin = 3; { const-string pin: saved value, AddRef'd; released after }
|
||||
akStrConsume = 4; { const-string consume: saved +1 value; released after }
|
||||
akIntfConsume = 5; { interface-returning-call arg: owned (+1) fat pointer
|
||||
saved (itab then obj, obj on top); obj released after }
|
||||
|
||||
type
|
||||
{ Per-call bookkeeping for hoisted call arguments — see EmitArgHoist.
|
||||
|
|
@ -12284,6 +12286,31 @@ begin
|
|||
Continue;
|
||||
end;
|
||||
|
||||
{ Interface-returning call argument (Show(MakeFoo(42))): the callee hands
|
||||
back an OWNED (+1) fat pointer that the borrowing parameter does not
|
||||
release. Hoist it here so EmitHoistEpilogue can release the obj after the
|
||||
call (mirrors akStrConsume). EmitIntfSretCall leaves the fat pointer at
|
||||
(%rsp) over a 16-byte buffer: obj@0, itab@8. Save it as a contiguous
|
||||
16-byte pair (itab pushed first, obj on top); depth records the obj. }
|
||||
if (not IsVarPos) and
|
||||
((Arg is TFuncCallExpr) or (Arg is TMethodCallExpr)) and
|
||||
(Arg.ResolvedType <> nil) and (Arg.ResolvedType.Kind = tyInterface) then
|
||||
begin
|
||||
if Arg is TFuncCallExpr then
|
||||
Self.EmitIntfSretCall(TFuncCallExpr(Arg))
|
||||
else
|
||||
Self.EmitIntfSretMethodCall(TMethodCallExpr(Arg));
|
||||
Self.Emit(#9'movq (%rsp), %rax'); { obj }
|
||||
Self.Emit(#9'movq 8(%rsp), %rcx'); { itab }
|
||||
Self.Emit(#9'addq $16, %rsp'); { drop the sret buffer }
|
||||
Self.Emit(#9'pushq %rcx'); { save itab }
|
||||
Self.Emit(#9'pushq %rax'); { save obj (on top) }
|
||||
Result := Result + 16;
|
||||
ADepths.Add(Result);
|
||||
AKinds.Add(akIntfConsume);
|
||||
Continue;
|
||||
end;
|
||||
|
||||
{ Const-string argument needing caller protection. }
|
||||
ConstStr := False;
|
||||
if Par <> nil then
|
||||
|
|
@ -12357,6 +12384,12 @@ begin
|
|||
Self.Emit(Format(#9'movq %d(%%rsp), %%rdi', [Off]));
|
||||
Self.Emit(#9'callq _StringRelease');
|
||||
end
|
||||
else if K = akIntfConsume then
|
||||
begin
|
||||
{ Release the owned (+1) obj of a hoisted interface-call argument. }
|
||||
Self.Emit(Format(#9'movq %d(%%rsp), %%rdi', [Off]));
|
||||
Self.Emit(#9'callq _ClassRelease');
|
||||
end
|
||||
else
|
||||
begin
|
||||
{ Hoisted record temp: release its managed fields; the buffer
|
||||
|
|
@ -12390,6 +12423,21 @@ begin
|
|||
APushed := APushed + 16;
|
||||
Exit;
|
||||
end;
|
||||
if AKind = akIntfConsume then
|
||||
begin
|
||||
{ Hoisted interface-returning-call argument: the saved 16-byte fat pointer
|
||||
has obj on top (at depth) and itab just below. Push both as the two
|
||||
interface arg slots (obj first, itab on top), matching the layout
|
||||
EmitMethodArgPush emits for an interface argument. }
|
||||
Self.Emit(Format(#9'movq %d(%%rsp), %%rax',
|
||||
[ATotal - ADepth + APushed])); { obj }
|
||||
Self.Emit(Format(#9'movq %d(%%rsp), %%rcx',
|
||||
[ATotal - ADepth + APushed + 8])); { itab }
|
||||
Self.Emit(#9'pushq %rax'); { push obj }
|
||||
Self.Emit(#9'pushq %rcx'); { push itab }
|
||||
APushed := APushed + 16;
|
||||
Exit;
|
||||
end;
|
||||
if AKind >= akRecCall then
|
||||
begin
|
||||
{ Hoisted record-call or const-string argument: reload the saved value. }
|
||||
|
|
@ -12564,7 +12612,19 @@ begin
|
|||
ParamType := TMethodParam(ADecl.Params.Items[I]).ResolvedType;
|
||||
if ParamType = nil then
|
||||
ParamType := Arg.ResolvedType;
|
||||
if (not IsOA) and (OALK.Get(I) >= akRecCall) then
|
||||
if (not IsOA) and (OALK.Get(I) = akIntfConsume) then
|
||||
begin
|
||||
{ Hoisted interface-returning-call argument — reload the saved 16-byte
|
||||
fat pointer (obj on top at depth, itab just below) and push both as
|
||||
the two interface arg slots (obj first, itab on top). }
|
||||
Self.Emit(Format(#9'movq %d(%%rsp), %%rax',
|
||||
[OALTotal - OALD.Get(I) + OALPushed])); { obj }
|
||||
Self.Emit(Format(#9'movq %d(%%rsp), %%rcx',
|
||||
[OALTotal - OALD.Get(I) + OALPushed + 8])); { itab }
|
||||
Self.Emit(#9'pushq %rax');
|
||||
Self.Emit(#9'pushq %rcx');
|
||||
end
|
||||
else if (not IsOA) and (OALK.Get(I) >= akRecCall) then
|
||||
begin
|
||||
{ Hoisted record-call or const-string argument — reload the saved
|
||||
value from the pre-pass region. }
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ type
|
|||
refers to the template source), not the instantiating unit. }
|
||||
procedure TestDebug_GenericAllocSite_ReportsDefiningUnit;
|
||||
procedure TestDebug_GenericAllocSite_ReportsDefiningUnit_Native;
|
||||
{ An interface-returning call passed directly as an argument — Show(Make())
|
||||
— hands the callee an owned (+1) fat pointer it borrows; the caller must
|
||||
release it after the call or one instance leaks. }
|
||||
procedure TestDebug_IntfCallResultArg_NoLeak;
|
||||
procedure TestDebug_IntfCallResultArg_NoLeak_Native;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -563,6 +568,65 @@ begin
|
|||
AssertTrue('no leak report (native)', Pos('leak', Output) < 0);
|
||||
end;
|
||||
|
||||
const
|
||||
{ Interface-returning call result passed positionally — Show(MakeFoo(42)).
|
||||
MakeFoo returns an owned (+1) interface; the borrowing parameter does not
|
||||
release it, so the caller must. One TFoo leaked on native before the
|
||||
akIntfConsume hoist released it after the call. }
|
||||
SrcIntfCallResultArg = '''
|
||||
program P;
|
||||
type
|
||||
IFoo = interface
|
||||
function Val: Integer;
|
||||
end;
|
||||
TFoo = class(TObject, IFoo)
|
||||
FN: Integer;
|
||||
function Val: Integer;
|
||||
end;
|
||||
function TFoo.Val: Integer;
|
||||
begin Result := FN end;
|
||||
function MakeFoo(N: Integer): IFoo;
|
||||
var F: TFoo;
|
||||
begin
|
||||
F := TFoo.Create();
|
||||
F.FN := N;
|
||||
Result := F
|
||||
end;
|
||||
procedure Show(F: IFoo);
|
||||
begin
|
||||
WriteLn(F.Val())
|
||||
end;
|
||||
begin
|
||||
Show(MakeFoo(42))
|
||||
end.
|
||||
''';
|
||||
|
||||
procedure TE2ELeakCheckTests.TestDebug_IntfCallResultArg_NoLeak;
|
||||
var
|
||||
Output: string;
|
||||
ExitCode: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
|
||||
AssertTrue('compile+run',
|
||||
CompileAndRunWithRTLDebugOn(beQBE, SrcIntfCallResultArg, Output, ExitCode, True));
|
||||
AssertEquals('exit 0', 0, ExitCode);
|
||||
AssertEquals('stdout', '42' + LE, Output);
|
||||
AssertTrue('no leak report, got: ' + Output, Pos('leak', Output) < 0);
|
||||
end;
|
||||
|
||||
procedure TE2ELeakCheckTests.TestDebug_IntfCallResultArg_NoLeak_Native;
|
||||
var
|
||||
Output: string;
|
||||
ExitCode: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
|
||||
AssertTrue('compile+run',
|
||||
CompileAndRunWithRTLDebugOn(beNative, SrcIntfCallResultArg, Output, ExitCode, True));
|
||||
AssertEquals('exit 0', 0, ExitCode);
|
||||
AssertEquals('stdout', '42' + LE, Output);
|
||||
AssertTrue('no leak report, got: ' + Output, Pos('leak', Output) < 0);
|
||||
end;
|
||||
|
||||
initialization
|
||||
RegisterTest(TE2ELeakCheckTests);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue