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?
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
- Closing database connection, transaction, or query
- Closing socket connection
- Closing opened files
- Unlocking critical section/mutex
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:
Ensures that you will not forget to finalize a resource, a very common error while doing code maintenance and new conditions are added creating a new flow.
The finalization code is next to the initialization code, which is more readable than putting at the end of the method;
No need for try/finally blocks to ensure that a resource is finalized.
Even if an exception occurs, all scheduled methods will be executed.
Caveats
Defer adds a small overhead by the created “Defer” object, the finalization will accumulate the execution of procedures, code that should be executed anyway. I consider that not being a high performance code, the use of defer brings more advantages than disadvantages.
Avoid using defer inside loops, if it is necessary to allocate and deallocate resources within a loop, the deallocation must be deterministic. Excessive usage can greatly increase memory consumption and cause slowness.
When using with anonymous methods keep in mind that the state is captured, any variable used will have the value captured at the time the defer is declared and not the value changed during the procedure flow. Read more in Understanding Anonymous Methods
This project was inspired by Golang Defer function and has no relation to “Deferred/Promise” and “Deferred Choice” patterns.
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.