Delphi Defer

Versão deste artigo em Português

Few weeks ago I was refactoring my framework “Foundation 4 Delphi”, I had to extend the TComponent.TRecursiveEnumerator to be used in other classes, it was a very old implementation and I decided to rewrite with generics and take advantage of smart records to simplify the integration with my TComponentHelper class helper.

In the record’s implementation, I had to create a “IInterface” field just to be able to simulate a destructor in the record, when I thought how great would be if we had the Defer function as in Golang. When I was writing the unit tests, I felt again the need for Defer, so I decided to think more about the pattern and behavior of Defer, which led me to try to implement it in Delphi just as a logic exercise, even thinking that it would not be something useful for production, but in the end of the day, I got something very useful.

What is Defer?

Postpone image Defer defines the “postpone procedure” pattern, this postpone should schedule a “procedure: TProc” to run it after the end of the caller method that executed the call to the Defer function (Proc: TProc).

At the end of the caller method, Defer must execute all stacked procedures in the reverse order in which they were scheduled.

Defer according to the Golang documentation :

Go’s defer statement schedules a function call (the deferred function) to be run immediately before the function executing the defer returns. It’s an unusual but effective way to deal with situations such as resources that must be released regardless of which path a function takes to return. The canonical examples are unlocking a mutex or closing a file.

It is important to know that Golang is a language with garbage collection, so it is not necessary to destroy the created objects, only to deallocate resources.

And regardless of the executed flow in the function, Defer should always execute the already scheduled methods, so if there is an Exit before the end or if an exception occurs, all the scheduled methods must be executed.

In my research, I discovered that Defer was implemented for Apple’s Swift, version 2.0. You can read more about that in this article with a nice title “try / finally done right”:

I also read on reddit that this feature exists in D language, but I haven’t found any references.

Some uses for Defer

The Implementation

After understanding how a Defer implementation should work, I implemented it using Interfaces, in this way the object would be automatically deallocated in the “end” statement of the caller method.

  IDeferrer = Interface(IInterface)
    function Defer(Proc: TProc): IDeferrer;
  end;

I followed the Golang standard for naming interfaces, adding the suffix “er”. This pattern differs from what we usually see in Delphi, add the suffix “able”, like in “Deferrable”.

Internally, it has a stack with the scheduled TProc list, these methods will be executed when the TDefer object is destroyed, in the reverse order in which they were scheduled. Delphi already have a TStack<T> implementation in System.Generics.Collections, I just had to decorate TProc with a TStackItem record before storing.

type
  TDeferrer = class(TInterfacedObject, IDeferrer)
  strict private
    type
      TStackItem = record
        Proc: TProc;
      end;
  strict private
    FStack: TStack< TStackItem >;
    procedure Push(Proc: TProc);
    procedure ProcessStack;
  public
    constructor Create; overload;
    constructor Create(Proc: TProc); overload;
    destructor Destroy; override;
    function Defer(Proc: TProc): IDeferrer;
  end;

To finish, I created a global function that returns the created instance already storing the first method:

function Defer(Proc: TProc): IDeferrer;
begin
  Result := TDeferrer.Create(Proc);
end;

How it works

The purpose of Defer is to defer methods execution, it not to manage the lifetime of objects, but as in Delphi Win32/Win64 objects are manually managed, we can use Defer to destroy objects, as well to finalize other resources.

uses
  Foundation.System;
var
  Database: TDatabase;
  Exec: IDeferrer;
  Query: TQuery;
  Transaction: TTransaction;
begin
  Database := TDatabase.Create(FWriter);
  Exec := Defer(Database.Free);

  Database.Open('foundation-db');
  Exec.Defer(Database.Close);
  
  Transaction := Database.StartTransaction;
  Exec.Defer(Transaction.Free);
  Exec.Defer(Transaction.Commit);
  
  Query := Transaction.Query;
  Exec.Defer(Query.Free);
  
  if Query.Open('select value from table') then
  begin
    Exec.Defer(Query.Close);
  end;
end;

This code is part of the unit tests. The objects are just Mock, the FWriter object passed in the constructor is used to write the steps so the results can be validated, this was the way I found to create tests for Defer, as it will only run when being destroyed.

This is the case test log used to check the results:

  TDatabase.Open('foundation-db')
  TDatabase.StartTransaction
  TTransaction.Query
  TQuery.Create
  TQuery.Open('select value from table')

> "Defer execution starts here"

  TQuery.Close 
  TQuery.Free
  TTransaction.Commit
  TTransaction.Free
  TDatabase.Close
  TDatabase.Free

Note that Defer happens only after TQuery.Open, exactly in the reverse order in which the methods were scheduled in Defer.

Another detail is the declaration of the “Exec: IDeferrer” variable to have only one instance. Declaring the variable is not required, and the code will be cleaner with just a small overhead.

If the IDeferrer reference is not stored in a local variable, a new instance is created for each Defer call. That would be a problem if the order in which the instances are deallocated was not the same as Defer, but the behavior was consistent across all tests. I assume it is the default behavior of FastMM, the Delphi default memory manager, to store the allocated instances in a stack, so the order to deallocate is the same as we need for Defer’s implementation. If your project memory manager is another one like ScaleMM or Nexus Memory Manager I suggest doing tests before using Defer, although I assume this should be the default behaviour for deallocating resources.

An alternative implementation

In the Experimental folder of the repository there is an alternate implementation that extends the Defer in the Foundation.Pattern.Defer.Auto unit.

This implementation captures the thread id and caller method pointer to be used as the key to save the Defer instance in a TDictionary<string, IDeferrer> to always reuse the same instance. I have successfully tested this implementation on Win32, Win64 and in single and multi thread.

To capture the caller method reference, the JclDebug unit from the Jedi Jcl project is used. For win32 I managed to extract only the necessary lines of code to the Foundation.Vendor.JclDebug unit, just to make easy to test, along with the author’s credits and license. For production and Win64, I recommend you to use the latest JclDebug unit.

The advantage is clear, only one instance of Defer is created for each caller method, on the other hand the overhead generated from the implementation of the list using the TDictionary class, the mutex needed to ensure good multi-thread behavior and Jcl dependency, they are bigger than just declaring a variable or having more than one instance of the Defer method caller. Assuming that in common applications, there will not be many instances at the same time.

More examples

Anonymous methods

  Database := TDatabase.Create(FWriter);
  Database.Open('database-name');
  Defer(
    procedure
    begin
      Database.Close;
      Database.Free;
    end
  );

Method tracer

class function TTrace.Method(Writer: TStringsWriter; const MethodName: string; var TraceProc: ITracer): IDeferrer;
var
  {! Workaround "Defer(ITracer.Exit)" : E2010 Incompatible types: 'TProc' and 'procedure, untyped pointer or untyped parameter' }
  Trace: TTrace;
begin
  Trace := TTrace.Create(Writer);
  Trace.Enter(MethodName);
  Result := Defer(Trace.Exit);
  Supports(Trace, ITracer, TraceProc)
end;

var
  Trace: ITracer;
begin
  TTrace.Method(FWriter, 'DelegateDeferToTraceExecute', Trace);
  Trace.Step('First');
  Trace.Step('Second');
  Trace.Step('Third');
end;

The tracer’s output log:

    > Enter DelegateDeferToTraceExecute
        1. DelegateDeferToTraceExecute: First
        2. DelegateDeferToTraceExecute: Second
        3. DelegateDeferToTraceExecute: Third
    < Exit DelegateDeferToTraceExecute

Conditional finalization in anonymous method

Defer(
  procedure(
    if Datatabase.InTransaction then
    begin
      if Object.IsValid then
          Datatabase.Commit
      else
          Datatabase.RollBack;
    end;
  )
);

Handling exceptions

Defer(
  procedure(
    try
      Connection.Close;
    except 
      on E: Exception do
        Log.Write(E); 
    end;

    Connection.Free;
  )
);

Defer as a record destructor

type
  TValueType< T > = record
  private
    FAnyObject: TAnyObject;
    FDeferrer: IDeferrer;
    FValue: T;
  public
    constructor Create(Value: T);
  end;  

constructor TValueType< T >.Create(Value: T);
begin
  FValue := Value;
  FAnyObject := TAnyObject.Create
  FDeferrer  := Defer(FAnyObject.Free);
end;

Advantages

Delaying the execution of procedures has some advantages:

Caveats

Repository

The project is hosted on github, I will make another post just to talk about the framework and what I am preparing for it. A lot is ready and in production, you can expect news soon.

comments powered by Disqus