Foundation · Patterns · Tips & Tricks · Uncategorized

Automate Restorable Operations with Custom Managed Records

There are those common try..finally blocks that you write over and over again. For example entering and leaving a critical section to protect resources from multiple threads, or calling BeginUpdate and EndUpdate to efficiently bulk-update a FireMonkey control. These kinds of “restorable” operations can be automated with the new Custom Managed Records feature introduced in Delphi 10.4.

About Custom Managed Records

If you are not yet familiar with Custom Managed Records (called CMRs from here on out), then take a look at my previous post about using CMRs to wrap C(++) APIs.

As a short recap, consider the following CMR:

type
  TFoo = record
  public
    class operator Initialize(out ADest: TFoo);
    class operator Finalize(var ADest: TFoo);
    class operator Assign(var ADest: TFoo;
      const [ref] ASrc: TFoo);
  end;

Then Delphi will automatically call the Initialize operator when you declare such a record, and Finalize when the record goes out of scope. The Assign operator is called to copy one record to another one. You don’t have to use all 3 operators, but you need at least one to create a CMR.

Now, when you write the following code:

begin
  var Foo1: TFoo;
  var Foo2 := Foo1;
end;

Then Delphi will convert this to the following code behind the scenes (note that this code does not compile though):

begin
  var Foo1, Foo2: TFoo;
  TFoo.Initialize(Foo1);
  try
    TFoo.Initialize(Foo2);
    try
      TFoo.Assign(Foo2, Foo1);
    finally
      TFoo.Finalize(Foo2);
    end;
  finally
    TFoo.Finalize(Foo1);
  end;
end;

Delphi will automatically insert try..finally statements to make sure that the Finalize operator is always called. This is the key to automate “restorable” operations.

For more information about Custom Managed Records, take a look at the official documentation, my previous post or other resources on the internet.

The remainder of this post shows 3 ways you can use CMRs to automate restorable operations.

1. Automatic BeginUpdate and EndUpdate

Lets start with a simple example: All FireMonkey controls have BeginUpdate and EndUpdate methods that you should call to efficiently bulk-update a control. When methods like these come in pairs, you instinctively use a try..finally block to ensure that EndUpdate is always called:

ListView.BeginUpdate;
try
  ListView.Items.Add.Text := 'Item 1';
  ListView.Items.Add.Text := 'Item 2';
finally
  ListView.EndUpdate;
end;

Whenever you see patterns like this, there is a potential for automating this with a CMR:

type
  TAutoUpdate = record
  private
    FControl: TControl; // Reference
  public
    constructor Create(const AControl: TControl);
  public
    class operator Initialize(out ADest: TAutoUpdate);
    class operator Finalize(var ADest: TAutoUpdate);
    class operator Assign(var ADest: TAutoUpdate;
      const [ref] ASrc: TAutoUpdate);
  end;

constructor TAutoUpdate.Create(const AControl: TControl);
begin
  Assert(Assigned(AControl));
  FControl := AControl;
  FControl.BeginUpdate;
end;

class operator TAutoUpdate.Initialize(out ADest: TAutoUpdate);
begin
  ADest.FControl := nil;
end;

class operator TAutoUpdate.Finalize(var ADest: TAutoUpdate);
begin
  if (ADest.FControl <> nil) then
    ADest.FControl.EndUpdate;
end;

class operator TAutoUpdate.Assign(var ADest: TAutoUpdate;
  const [ref] ASrc: TAutoUpdate);
begin
  raise EInvalidOperation.Create(
    'TAutoUpdate records cannot be copied')
end;

Some notes:

  • The Initialize operator just clears the reference to the control. This is a very common pattern that you see a lot with CMRs. Note that you don’t have to add this operator, but we discuss below why you should.
  • You pass the control you want to auto-update to the constructor. The constructor immediately calls BeginUpdate on it.
  • The Finalize operator is called in the finally section of the hidden try..finally block, so this is where you call EndUpdate. You have to check if the control is assigned first though, since it can be nil if the constructor hasn’t been called.
  • And finally, these kinds of CMRs should not be copyable (which is explained below). So we raise an exception in the Assign operator if you try to copy it anyway.

We can now rewrite the example snippet like this:

var AutoUpdate := TAutoUpdate.Create(ListView);
ListView.Items.Add.Text := 'Item 1';
ListView.Items.Add.Text := 'Item 2';

You just create the auto-update CMR, which will immediately call BeginUpdate. Then when the CMR goes out of scope (at the end of the method), the EndUpdate method will automatically be called because of the hidden try..finally block and Finalize operator.

This CMR saves 4 lines of code every time you use it, which I think is pretty great (unless you get paid by the line of course).

CMRs work great with inline variables, as in this example. When you declare the AutoUpdate variable as a “normal” local variable, then the Initialize operator will be called as soon as you enter the method. When using an inline variable instead, the Initialize operator will be called at location where you declare the inline variable, which gives you more control.

These kind of CMRs are very common in C++ (although they are not called CMR in C++). In C++, constructors and destructors are called automatically when an object/struct is declared on the stack. And C++ has copy constructors as well, which are similar to Assign operators in Delphi.

Why the Initialize operator?

So what happens if you don’t write an Initialize operator? In that case, the CMR will not be initialized at all (also not by the compiler). That means that the FControl field in the sample CMR contains random data. This should’t be an issue if you always call the constructor, but what happens if you don’t?

begin
  var AutoUpdate: TAutoUpdate;
end;

In this example, you just declare the CMR without constructing it. If there was no Initialize operator, then its FControl field would contain random data. Then, when the CMR goes out of scope, the Finalize operator gets called, which calls the EndUpdate method on an invalid object, most likely resulting in an access violation.

Why the Assign operator?

As I mentioned above, these kinds of CMRs should not be copyable. But why? Suppose you have the following code:

var AutoUpdate := TAutoUpdate.Create(ListView);
var AutoUpdateCopy := AutoUpdate;
ListView.Items.Add.Text := 'Item 1';
ListView.Items.Add.Text := 'Item 2';

This highlighted line in this example will call the Assign operator if available, or otherwise perform a regular record copy. Now, when the copy goes out of scope, its Finalize operator is called, which calls EndUpdate. Then when the original goes out of scope, EndUpdate is called again, resulting in an unbalanced BeginUpdate/EndUpdate pair.

Fortunately, there is no reason to copy these kinds of CMRs. To make sure you don’t copy the CMR by mistake, you should write an Assign operator and handle that situation. In this example, I raise an exception if you attempt to make a copy. But you could also handle this situation in different ways. For example, you could set the FControl field of target CMR to nil instead, which would prevent a call to EndUpdate.

2. Automatic Locking

A common design pattern in multi-threading is to enter a lock to protect a resource, then use that resource and finally release the lock again. Of course, you wrap this inside a try..finally block to ensure that the lock is always released. Consider this simple locked list:

type
  TLockedList<T> = class
  private
    FList: TList<T>;
    FLock: TSynchroObject;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Add(const AItem: T);
  end;

constructor TLockedList<T>.Create;
begin
  inherited;
  FList := TList<T>.Create;
  FLock := TCriticalSection.Create;
end;

destructor TLockedList<T>.Destroy;
begin
  FLock.Free;
  FList.Free;
  inherited;
end;

procedure TLockedList<T>.Add(const AItem: T);
begin
  FLock.Acquire;
  try
    FList.Add(AItem);
  finally
    FLock.Release;
  end;
end;

The Add method could take advantage of a CMR:

procedure TLockedList<T>.Add(const AItem: T);
begin
  var AutoLock := TAutoLock.Create(FLock);
  FList.Add(AItem);
end;

And the CMR could look like this:

type
  TAutoLock = record
  private
    FSyncObj: TSynchroObject; // Reference
  public
    constructor Create(const ASyncObj: TSynchroObject);
  public
    class operator Initialize(out ADest: TAutoLock);
    class operator Finalize(var ADest: TAutoLock);
    class operator Assign(var ADest: TAutoLock;
      const [ref] ASrc: TAutoLock);
  end;

constructor TAutoLock.Create(const ASyncObj: TSynchroObject);
begin
  Assert(Assigned(ASyncObj));
  FSyncObj := ASyncObj;
  FSyncObj.Acquire;
end;

class operator TAutoLock.Initialize(out ADest: TAutoLock);
begin
  ADest.FSyncObj := nil;
end;

class operator TAutoLock.Finalize(var ADest: TAutoLock);
begin
  if (ADest.FSyncObj <> nil) then
    ADest.FSyncObj.Release;
end;

class operator TAutoLock.Assign(var ADest: TAutoLock;
  const [ref] ASrc: TAutoLock);
begin
  raise EInvalidOperation.Create(
    'TAutoLock records cannot be copied');
end;

This CMR is very similar to the TAutoUpdate CMR presented earlier, so I won’t go into the details here. Note that you can use it with any type of synchronization object (critical section, mutex, etc…).

This CMR is so common in C++, that it is part of the standard library under the name std::lock_guard.

3. Automatic Object Destruction

So what about the obvious use case: automatic object destruction? You can certainly use a similar CMR for this:

type
  TAutoFree = record
  private
    FInstance: TObject; // Reference
  public
    constructor Create(const AInstance: TObject);
    class operator Initialize(out ADest: TAutoFree);
    class operator Finalize(var ADest: TAutoFree);
    class operator Assign(var ADest: TAutoFree;
      const [ref] ASrc: TAutoFree);
  end;

constructor TAutoFree.Create(const AInstance: TObject);
begin
  Assert(Assigned(AInstance));
  FInstance := AInstance;
end;

class operator TAutoFree.Initialize(out ADest: TAutoFree);
begin
  ADest.FInstance := nil;
end;

class operator TAutoFree.Finalize(var ADest: TAutoFree);
begin
  ADest.FInstance.Free;
end;

class operator TAutoFree.Assign(var ADest: TAutoFree;
  const [ref] ASrc: TAutoFree);
begin
  raise EInvalidOperation.Create(
    'TAutoFree records cannot be copied')
end;

This is similar to an std::unique_ptr in C++

You can use it like this:

begin
  var Writer := TStreamWriter.Create('foo.txt');
  var AutoFree := TAutoFree.Create(Writer);
  Writer.WriteLine('Some Text');
  OpenFile('foo.txt');
end;

This example has a problem though: the TStreamWriter object will automatically be destroyed when the AutoFree CMR goes out of scope. This is at the end of the method, after the OpenFile call. This means that OpenFile call may fail to open the file because the stream writer still has an exclusive lock on it. With the help of inline variables though, we can scope the AutoFree CMR and ensure that the stream writer is destroyed before the file is opened:

begin
  var Writer := TStreamWriter.Create('foo.txt');
  begin
    var AutoFree := TAutoFree.Create(Writer);
    Writer.WriteLine('Some Text');
  end;
  OpenFile('foo.txt');
end;

Now, the AutoFree CMR goes out of scope before OpenFile is called. Note that you can also put the stream writer construction inside the inner scope.

While this CMR can be useful, it cannot be used to automatically manage the lifetime of an object in situations where the object is shared. For those situations, you would traditionally use object interfaces to take advantage of automatic reference counting.

But with CMRs, there is an alternative now: a CMR can be used to add automatic reference counting to regular objects.

Which would be similar to an std::shared_ptr in C++

And that is were we enter the subject of “smart pointers”, which is outside the scope of this article, but could be come the subject of a future article…

3 thoughts on “Automate Restorable Operations with Custom Managed Records

Leave a Reply to Malmosi Péter Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s