Wednesday, February 27, 2013

Running “Any” Code When a Method Ends

Just like (some) other programmers, I like to abuse the fact that the Delphi compiler only destroys local interfaces when a method ends. If you don’t know what I’m talking about, check out the latest Nick’s post in the Fun Code of the Week series.

As an example, Nick put together a function which changes application cursor and at the end of the method reverts it back to the previous state.

procedure test;
begin
  AutoCursor(crHourGlass);
  // some long operation
  // cursor is automatically reverted when ‘test’ exits
end;

My approach to this pattern is usually slightly different. I like to mark the scope of the local interface with a with statement.

procedure test;
begin
  with AutoCursor(crHourGlass) do begin
    // some long operation
  end;
  // cursor is automatically reverted when ‘test’ exits
end;

It’s just too bad that compiler still destroys the interface on the final end of the method and not when the with statements ends. Still, this approach allows me to implement an interface method that restores the previous state so I can call it explicitly in the code. Let’s say that the interface returned from the AutoCursor call implements the Restore method which restores the original cursor. Then I’d use the AutoCursor in a following manner.

procedure test;
begin
  with AutoCursor(crHourGlass) do begin
    // some long operation
    Restore;
  end;
  // cursor is automatically reverted when ‘test’ exits
end;

Few days ago I was working on a multithreaded lock manager which will shortly appear in the OmniThreadLibrary. Unimportant details aside, this lock manager supports Lock and Unlock functions and a global function Lock. The latter locks the resource and creates an interface which will unlock the resource through the automatically destroyed interface, just like the AutoCursor from the Nick’s example does.

The code would look approximately like this …

function Lock(const lockManager: ISttdbDatabaseLockManager; const key: string;
  timeout_ms: cardinal): IAutoUnlock;
begin
  if not lockManager.Lock(key, timeout_ms) then
    Result := nil
  else
    Result := CreateAutoUnlock(lockManager, key);
end;

… but just before writing CreateAutoUnlock I had an idea. Actually, there’s no need to write a new auto-something interface for every occasion. I can create just one such interface which executes an anonymous method when it is destroyed. A magic of anonymous methods and variable capturing will take care of the rest.

To bring this (already too long) story to the end – the open-sourced GpStuff unit now contains such a multi-purpose run-code-on-destroy interface called IGpAutoExecute. An instance of this interface is created by the AutoExecute function.

IGpAutoExecute = interface
end;

function AutoExecute(proc: TProc): IGpAutoExecute;

TGpAutoExecute = class(TInterfacedObject, IGpAutoExecute)
strict private
  FProc: TProc;
public
  constructor Create(proc: TProc);
  destructor  Destroy; override;
  procedure Run;
end;

constructor TGpAutoExecute.Create(proc: TProc);
begin
  inherited Create;
  FProc := proc;
end;

destructor TGpAutoExecute.Destroy;
begin
  Run;
  inherited;
end;

procedure TGpAutoExecute.Run;
begin
  if assigned(FProc) then
    FProc;
  FProc := nil;
end;

function AutoExecute(proc: TProc): IGpAutoExecute;
begin
  Result := TGpAutoExecute.Create(proc);
end;

With this helper, I could rewrite the Lock function.

function Lock(const lockManager: ISttdbDatabaseLockManager; const key: string;
  timeout_ms: cardinal): IGpAutoExecute;
begin
  if not lockManager.Lock(key, timeout_ms) then
    Result := nil
  else
    Result := AutoExecute(
      procedure
      begin
        lockManager.Unlock(key);
      end
    );
end;

Regardless to everything said above, at the end I created a special unlocking interface which will be used in the OmniThreadLibrary, just because I could then implement Unlock function in it. Still, I think that AutoExecute approach will come handy in the future.

8 comments:

  1. Anonymous23:52

    After 30 years of programming, I don't often learn something as ingenious as this.

    Thanks for posting, and opening up a whole new way of doing things.

    ReplyDelete
  2. Anonymous08:39

    This is simply great! We can write less try...finally blocks now.

    ReplyDelete
  3. So instead of:
    Lock(LockManager, 'key', 100)
    try

    finally
    Unlock
    end;
    One will write
    Lock(LockManager, 'key', 100)

    // unlock will be called at the end of method

    Second approach looks to me more like "Where the hell is unlock - this will newer work"

    ReplyDelete
  4. For me, this sort of approach isn't better than the explicit try/finally. And I think you agree because you write with. But the with doesn't actually change the lifetime of the protected object. So to me that's actually worse.

    In order to use true RAII style you need language support. Like Python's with context managers, or C# using, or C++ scoped lifetime. But the smallest Delphi scope is the function. And functions are too heavy for RAII. In my view.

    ReplyDelete
  5. Marcin, David, actually I agree with you on many points - as you can also see from the fact that I try to introduce some logic into the code by using the with statement. I will, for example, never use such code if protected area is more then a few lines long and if it is followed with a code that doesn't need to be protected. Still, I find this approach immensely useful for short, self-contained methods.

    ReplyDelete
  6. I think this is one of the coolest Delphi featrures ever, and should have been documented a lot more prominently.
    Use with care though (:

    The fun thing is I have been using this so long, that I didn't even write a Delphi blog article about it. I think I started using it in the Delphi 6 or 7 era, long before starting a blog (:

    I did write a C# article about it. C# has the `using` statement, which makes the behaviour more local than just a method, and makes the pattern more clear. I agree with David Heffernan, that abusing this language feature can make your code a lot less obvious.
    http://wiert.me/2012/01/26/netc-using-idisposable-to-restore-temporary-settrings-example-temporarycursor-class/

    Good writeup!

    ReplyDelete
  7. From my point of view it is far better to use try finally blocks. You write a little bit more but it is directly visible what is happening. I doubt that this behaviour of the compiler is an explicit part of the language definition and I think one day it could be changed.

    For example it could be possible that one day the compiler destroys interfaces at the end of the with-block and not at the end of the function. The changed behaviour could cause serious problems in some cases and no line of code was changed.

    Regards

    ReplyDelete
  8. Anonymous methods are already TInterfacedObject by implementation. So here we're using one explicit TIO just tot keep a reference to a secondary implicit TIO. It does not look elegant... If this actually was brought on the language level, kind of

    Procedure X;
    var
    lazy Delayed: TProc;
    Y: LockRef;
    begin
    Y := DoLock()
    lazy := procedure begin FreeLock(Y); end;
    ....
    end;

    Or maybe some sugar over TPair where 1sy one is locker and the 2nd one is the freer

    ReplyDelete