Object Pascal, or better known as Delphi is a great programming language with built-in memory safety measures that make it easy to avoid memory-based vulnerabilities. Despite that, there are still mistakes that some make when handling memory in Delphi.
Tackling Memory Management
Let’s look at this snippet:
procedure WriteStringList(s:TStringList);
begin
WriteLn(s.Text);
end;
procedure Main();
var
words : TStringList;
begin
words.Create();
words.Add('This word');
words.Add('This other word');
words.Add('Some other other word');
WriteStringList(words);
end;
To some Pascal developers, any issues may seem obvious, but to others they may be oblivious of the issues at bay.
Creating objects
One common misconception is that var.Create()
is a valid constructor call,
this often leads to confusing errors. When creating objects in Pascal, one must
make sure to reference the type of the object they are creating. It seems
obvious, since other languages such as C++ and Java do not let you compile a
program using an invalid constructor call, the fact that Pascal lets you compile
this, makes it trivial to make this mistake.
The issue is that doing this creates a nil
object, which as one can tell, has
no data. I like to think of it as creating something from nothing, as the object
has no data, calling the constructor for nil
would result in well, nil. As
opposed to creating something from a template which has the layout for how the
object.
The correct syntax would rather be
procedure Main();
var
words : TStringList;
begin
- words.Create();
+ words := TStringList.Create();
words.Add('This word');
words.Add('This other word');
words.Add('Some other other word');
WriteStringList(words);
end;
This is a mistake that is easy to miss, for not only novice Pascal developers, but experienced Pascal programmers as well. Furthermore, it can be a difficult mistake to debug.
Freeing Objects
Another error in my example is not freeing my objects. One thing we must also
remember to do is to free every object that we create. When you call the
constructor of an object its destructor must follow, I like using a
creation is followed by destruction
motto to remember this.
procedure WriteStringList(s:TStringList);
begin
WriteLn(s.Text);
end;
procedure Main();
var
words : TStringList;
begin
words := TStringList.Create();
try
words.Add('This word');
words.Add('This other word');
words.Add('Some other other word');
WriteStringList(words);
finally
words.Free();
end;
end;
This can be easy to miss. This is mainly because when using Delphi, you are technically allowed to create an object without freeing it, 9 times out of 10 times your program will not crash, in my own experience. Avoiding this pattern is important to avoid crashes that come from un-freed objects, because an access voilation error can occur and those can be difficult to debug.
Objects passed between forms
When you create an object, it is allocated to the heap
in memory, and it is
easy to assume that anything that is allocated will be freed from memory
automatically, much like the expectation in previous example. This is a mistake
I made a lot when passing data between forms.
Take this as an example
Form 1:
procedure Tform1.ToForm2();
var
User : TUser;
Form2 : TForm2;
begin
// Assigning our name and age
User := TUser.Create('Daru',12);
// Not form2.create()!
form2 := TForm2.Create();
// The User object is a property of form 2
form2.User := User;
form2.ShowModal();
end;
Form 2:
procedure TForm2.DoStuff();
begin
WriteLn('User is ' + User.name);
WriteLn('Age is ' User.age);
User.Age := 30;
DoClose();
end;
procedure TForm2.DoClose();
begin
self.ModalResult := mrClose;
end;
Here, the toForm2
function creates the User
object, calling the constructor,
which assigns the user-name and their age. The second form is created and the
User
property of the second form is assigned to the User object in the first
one, this is a common way to pass objects between forms in Delphi.
The issue here is that the user object is not freed from memory, when we pass
the object, we pass a reference to the original TUser
object to TForm2
,
don’t see the problem?
When our function, DoClose()
is called, the Form2 is closed, as you can see,
we assign our modal result to mrClose
.
self.ModalResult := mrClose;
This creates a problem, when we set our User
property to the user from Form1,
we passed a reference to the object, which means that the object was supposedly
freed at this point, closing a form frees the objects attached to the form, the
user object is de-allocated.
When we change the user.age
value to 30, we mutate the original variable, this
causes a change in the reference to the object. When we close a form, the form’s
destructor is called along with free, causing the form to close. The issue is,
we still have the User as a property of the form, and we cannot free the form
when we still hold a reference to the original object, at least, not without an
access violation.
Form2 cannot destroy an object passed from Form1,since that form acts as its owner. Thus, one should free it manually.
procedure TForm2.DoStuff();
begin
WriteLn('User is ' + User.name);
WriteLn('Age is ' User.age);
User.Age := 30;
DoClose();
end;
procedure TForm2.DoClose();
begin
User.Free; //NOT User.Destroy;
self.ModalResult := mrClose;
end;
Of course, there are better ways to send data between forms such as shared units which store those objects and variables globally, but passing data between forms via properties is another common method. Generally, destroying an object owned by a parent form is not good practice.
Destroy vs Free
Another common error is calling the destructor procedure over the free procedure. While not being an error for say, it can lead to mistakes down the road. The destructor of an object is a virtual function that is determined at run-time based on the objects type. Free, is a method that has a known memory location, and thus we can call it after the object is freed. This is a bit different from C or C++ where a double free would cause a segmentation fault.
begin
User.Destroy();
// Acces violation
User.Destroy();
// This works
User.Free();
User.Free();
end
Further learning
I recommend that any Pascal developer or programmer should try other languages such as Rust or Zig. While Delphi may have decent memory management facilities and can be better than C in this right, learning from memory safe languages can be beneficial in helping one understand how other languages deal with memory and how they can improve their own methods.
Thanks for reading, I hope you gained insights into how you could improve your memory management techniques in Delphi and write safer programs.