openlang

Interface references and object references to the same instance

The lifetime mistake I meet most often in older Delphi code comes from mixing two ownership rules on one instance. A class that descends from TInterfacedObject is freed when its interface reference count reaches zero. A variable of the class type is not counted at all. Hold the same instance through both kinds of reference and the two rules disagree about when it dies.

program Greeter;
{$APPTYPE CONSOLE}
type
  IGreeter = interface
    procedure Hello;
  end;

  TGreeter = class(TInterfacedObject, IGreeter)
    procedure Hello;
  end;

procedure TGreeter.Hello;
begin
  WriteLn('hello');
end;

procedure Use(G: IGreeter);
begin
  G.Hello;
end;

var
  Obj: TGreeter;
begin
  Obj := TGreeter.Create; // count 0, a class reference is not counted
  Use(Obj);               // 1 inside Use, back to 0 on the way out
  Obj.Free;               // the object is already gone
end.

The instance starts with a count of zero, because the class reference never touched it. Passing it to Use converts it to IGreeter, the count goes to one for the duration of the call and back to zero on the way out, and at zero the object destroys itself. The Obj.Free that follows is a second destruction of memory that has already been released. The compiler accepts all of it.

My rule is that once a class implements interfaces through TInterfacedObject, every long-lived reference to an instance is an interface reference, and the class type appears only in the line that calls the constructor. I am curious whether others draw the line in the same place.

The conversation continues 5

Follow the reasoning
  1. Permalink

    Let me make sure I have the order right, because the timing is the whole question. One, Create returns with the count at zero. Two, entering Use takes it to one. Three, leaving Use drops it to zero, and the destructor runs right there, in the finalization code of Use, before control is back in the caller. So by the time Obj.Free runs, the destruction has already happened, not merely been scheduled.

    If that is right, does declaring the parameter as const G: IGreeter remove steps two and three entirely? I have read that it does, and that it opens a different problem when the argument is a freshly constructed object.

  2. Permalink

    The line I'd look at is the declaration, Obj: TGreeter. Change it to Obj: IGreeter and the example stops compiling at Obj.Free, because Free belongs to TObject and the interface doesn't declare it. That's a nice property of your rule. The compiler then refuses the second destruction instead of letting it through.

    It only holds while the interface really is narrower than the class, though. If someone adds a Dispose method to IGreeter for convenience, the variable's type stops telling you anything about who frees what.

  3. Permalink

    Agree on the declaration, and there's one more uncounted reference the variable's type won't show you. Assign Obj.Hello to a procedure of object field somewhere and that method pointer holds the instance too, with no count. The interface references drop to zero, the object goes, and the event still fires into it later. In the editor everything looks like interface code.

    C# has a cousin of this with event handlers keeping subscribers alive, the opposite direction, which is why I went looking for it here.

  4. Permalink

    The rule also gives a cheap check. If the class type only appears next to Create, a text search for : TGreeter should come back empty, and whatever it does return is the list to go through.

  5. Permalink

    Your three steps are right, and the destruction does happen inside Use, as the last thing it does before returning. With const G: IGreeter the callee does no counting at all, so steps two and three disappear and the example above stops destroying the object early.

    The other problem you read about concerns Use(TGreeter.Create). With a const parameter nothing in that call owns the new instance, because there is no variable on the caller's side to hold a count. I have seen it described as a leak, and I am not certain every compiler version treats it the same way, so I simply do not write that form. The instance goes into an interface variable first and is passed from there.

    I should add one exception to my own rule. TComponent implements _AddRef and _Release without counting, so a component reached through an interface is still owned by its Owner. The rule applies to TInterfacedObject descendants and not to every class that implements an interface.

Other sides of the same question

  1. The dangling pointer warning went away, the pointer did not

    Passing a name into a C++ library through a wrapper, and the first version was one line, CString::new(name).unwrap().as_ptr()…

    Replies 1rustcpp
  2. getline hands you the buffer even when it fails

    POSIX getline allocates or grows the buffer through the pointer you pass, and the buffer belongs to the caller from the first…

    Replies 4c
  3. Same sink call, a copy on one line and a move on the next

    Nim 2 decides copy or move per call site. Not per proc. Same argument, same proc. The first call copies, because a is still n…

    Replies 2nim