Friday, January 29, 2010

One-liner RAII in Delphi

Some C++ aficionados point at the ability, in C++, to create an object such that the creation allocates some kind of resource, and the destruction frees the resource, with much glee. The idiom is RAII: Resource Acquisition Is Initialization. The technique can be a useful, albeit somewhat opaque, way of acquiring a resource for the duration of a block of code and relinquishing the resource upon exiting the block. It's C++'s equivalent of Delphi / Java / C# / SEH's try / finally blocks (though of course C#'s "using" statement is an even closer analogue).

Delphi has a similar mechanism in the form of deterministic reference counting for interfaces. This can be (ab)used to functionally implement almost exactly the same technique as C++. The degree to which it is useful, however, is moderated by readability issues; RAII doesn't necessarily employ a visible nesting, and rather depends on the implied nesting of the scope in which it's used.

Anyway, here's a way of implementing RAII in Delphi, for people to whom the technique may not have occurred yet. It relies on defining a general-purpose class which implements an interface, and whose death will be triggered by scope exit:

type
  TScopeExitNotifier = class(TInterfacedObject)
  private
    FProc: TProc;
  public
    constructor Create(const AProc: TProc);
    destructor Destroy; override;
  end;

constructor TScopeExitNotifier.Create(const AProc: TProc);
begin
  FProc := AProc;
end;

destructor TScopeExitNotifier.Destroy;
begin
  if Assigned(FProc) then
    FProc;
  inherited;
end;

function MakeScopeExitNotifier(const AProc: TProc): IInterface;
begin
  Result := TScopeExitNotifier.Create(AProc);
end;

I would generally recommend hiding the actual class in the implementation-section of a unit - the functional interface of MakeScopeExitNotifier ought to be sufficient. Using this function, we can now implement a one-liner RAII equivalent of the following code:

  Writeln('Disable controls on ', {ASurface});
  try
    // (user code)
  finally
    Writeln('Enable controls on ', {ASurface});
  end;

Of course, I'm using strings etc. for simplicity, so that the demo translation is compilable. Here is the definition of the function which will be called to form the one-liner:

function DisableEnableControls(const ASurface: string): IInterface;
begin
  Writeln('Disable controls on ', ASurface);
  Result := MakeScopeExitNotifier(procedure
  begin
    Writeln('Enable controls on ', ASurface);
  end);
end;

And here it is in action:

procedure P;
begin
  DisableEnableControls('Foo');
  DisableEnableControls('Bar');
  Writeln('Controls on Foo and Bar are disabled now');
end;

Because of the way Delphi's deterministic reference counting and destruction works, Foo and Bar will be released in reverse order of construction, as expected for RAII. Functions that return an interface value in Delphi are implemented behind the scenes as procedures that take an out Result parameter; these are in effect hidden variables that keep the return values alive until the scope is exited.

The output of the program is as follows:

Disable controls on Foo
Disable controls on Bar
Controls on Foo and Bar are disabled now
Enable controls on Bar
Enable controls on Foo

Of course, it's not necessarily clear what DisableEnableControls(string) would do, and how it protects the scope. That's why I wouldn't necessarily recommend using this technique, except to counter C++-ite arguments. Using anonymous methods directly would seem to be a more readable approach in general, something along these lines:

procedure DisableEnableControls(const ASurface: string; const ACB: TProc);
begin
  Writeln('Disable controls on ', ASurface);
  try
    if Assigned(ACB) then
      ACB;
  finally
    Writeln('Enable controls on ', ASurface);
  end;
end;

procedure P;
begin
  DisableEnableControls('Foo', procedure
  begin
    DisableEnableControls('Bar', procedure
    begin
      Writeln('Controls are disabled now.');
    end);
  end);
end;

Output is the same as earlier. The extra complexity / verbosity of this abstraction over and above a literal try/finally would of course be more justified if the logic in the place of Enable and Disable sections were more involved, such as possibly setting up and rolling back transactions, etc.

However, the functionality of MakeScopeExitNotifier() routine is actually useful in the context of anonymous methods, because by capturing a variable containing the return value, you can get informed as to when the scope that created your anonymous method dies. This can be useful to e.g. free any objects that were allocated when the anonymous method was created:

function FreeOnExit(const Args: array of TObject): IInterface;
var
  toFree: TArray<TObject>;
begin
  // copy open array to toFree because open arrays can't be captured
  SetLength(toFree, Length(Args));
  Move(Args[0], toFree[0], Length(Args) * SizeOf(Args[0]));
  
  Result := MakeScopeExitNotifier(procedure
  var
    i: Integer;
  begin
    Writeln('FreeOnExit is freeing objects');
    for i := 0 to High(toFree) do
      toFree[i].Free;
  end);
end;

procedure Use(const x: IInterface);
begin
end;

function Q: TProc;
var
  onDie: IInterface;
  obj: TObject;
begin
  obj := nil;
  onDie := FreeOnExit([obj]);
  Writeln('Allocating an object');
  obj := TObject.Create;
  Result := procedure
  begin
    Use(onDie); // cause onDie to be captured
    Writeln(Format(
      'Inside callback, obj still alive: %p: %s',
      [Pointer(obj), obj.ToString]));
  end;
end;

procedure P;
var
  r: TProc;
begin
  Writeln('Before Q');
  r := Q();
  Writeln('Invoking result of Q');
  r;
  Writeln('Clearing result of Q');
  r := nil;
  Writeln('Result is cleared');
end;

The output is something like:

Before Q
Allocating an object
Invoking result of Q
Inside callback, obj still alive: 00970D20: TObject
Clearing result of Q
FreeOnExit is freeing objects
Result is cleared

This technique, capturing a variable of an interface or other deterministically lifetime-managed type, is essential for flexibility in cleaning up any resources that an anonymous method needs to for its lifetime. Note, however, that there can be a gotcha: the following alternative, using MakeScopeExitNotifier directly, will not work:

// <BROKEN CODE DO NOT USE>
function Q: TProc;
var
  onDie: IInterface;
begin
  onDie := MakeScopeExitNotifier(procedure
  begin
    Writeln('died!');
  end);
  Result := procedure
  begin
    Use(onDie);
  end;
end;
// </BROKEN CODE DO NOT USE>

The reason it's broken is that the anonymous method that's handed off to MakeScopeExitNotifier will itself keep the scope alive. So in order to make use of MakeScopeExitNotifier for this particular purpose, the anonymous method passed to it needs to be in a different scope.

14 comments:

Unknown said...

I have a procedure that does something similar I keep handy. It's extremely useful for avoiding lots of nested try/finally blocks for things like temporary objects, BeginUpdate/EndUpdate, Open/Close, cursor changing, etc.

Unknown said...

How advisable is it to rely on the fact that the hidden interface variables from your first "procedure P" life throughout the whole routine? When implementenig similar things, I already noticed that it seems to work, but to be sure I always used explicit variables, like in

procedure P;
var
  FooNotifier: IInterface;
  BarNotifier: IInterface;
begin
  FooNotifier := DisableEnableControls('Foo');
  BarNotifier := DisableEnableControls('Bar');
  Writeln('Controls on Foo and Bar are disabled now');
end;

Unknown said...

Interface scoping rules are fairly well defined. If you're not maintaining any references to the interface at all, they'll go out of scope when the procedure exits.

Unknown said...

@Anthony: Do you have a quote to make me feel secure about this? I don't want to rely on implementation details that might change with the next compiler release. :-)

Barry Kelly said...

uli - the situation is rather the reverse. We (Embarcadero) can't really change the implementation semantics because of the backward compatibility impact for people relying on the behaviour.

There is a situation where the scope is different to that expected in the post: if the method creating the interface is inlined into a different method, then the scope will be extended into the destination method. The result temporaries get turned into locals of the destination method.

Barry Kelly said...

As to your earlier comment, about explicit variables: in the hypothetical (and breaking change) case, where we optimized interface variable usage, we would likely not only break the described RAII-like functionality but also the explicit variable approach too; the values assigned to FooNotifier and BarNotifier are not used, so "in theory" they can be freed up sooner, and potentially even reuse the same storage.

But of course, destruction of the interface can have side-effects, and that's what's being relied upon for the effect in the post. Changing the language such that side-effects like these have visible changes is not something we do willingly.

Unknown said...

Thanks for clearing this up, Barry. So my "fake RAII" instances will get a bit shorter in the future.

Unknown said...
This comment has been removed by the author.
Unknown said...

Somehow this reminds me of this trick with interfaces: http://www.malcolmgroves.com/stories/2004/04/05/writingSolidDelphiCode.html

Ivan Levashew said...

It's XE2 now, and there is still no proper RAII despite Delphi runtime already having all the required functionality for years. Variants copying and destruction is an example.

It's a pity.

I can't understand why can't I declare record destructors and copying constructors. Interfaces and variants have a drawback: they are not initialized at all. I mean, I'd like to write MyEntity.MyFunction without ensuring that MyEntity has been initialized properly.

I have to wrap interface into record and use record methods that forward my requests to the interface field. They also initiallize this field on first access and invoke EnsureUnique on every subsequent accesses.

Barry Kelly said...

Ivan: I don't think a free-for-all on copy constructors, assignment operators and friends like in C++ would be a positive development at all.

It opens up too much of the underlying mechanics of the compiler. Very low level semantics around when and where exactly copies get created and destroyed become exposed and get depended on. People get upset at lack of optimizations on one hand (e.g. redundant copies, long a problem in C++), and unpredictable user code execution on the other (C++ exception safety woes). It's very difficult - far too difficult - to write correct semantics for making a value type work properly in all situations. So I'm reluctant to support anything much more complicated than compiler help for reference counting.

You complain about not being able to ensure MyEntity has been initialized properly; I presume you mean default constructors. I think they are a terrible idea, for any value of default construction other than zero initialization. The issues around creating arrays of the things (needing to keep track of how far construction got), how objects get initialized, handling exceptions in the default constructor, leads to horrific abominations like C++ constructor and function exception handlers.

Anonymous said...

@Barry

>>...People get upset at lack of optimizations on one hand (e.g. redundant copies, long a problem in C++)...

I thought move constructors solved that problem.

Barry Kelly said...

Anonymous: hence why I said it was long a problem, not still a problem.

Whether all implementations are reliably good enough now for it never to be a problem any more, I don't have enough information to say; and as to the difficulty of getting the semantics right, that responsibility is still too heavily on the user (i.e. programmer) IMO.

Guru said...

SIMILAR code here: https://cc.embarcadero.com/item/25108