Managing Object Lifetimes Across Delphi Runtime Packages using Interfaces
Learn how to use Delphi interfaces and TInterfacedObject to prevent memory leaks and access violations when loading and unloading runtime packages.
01 Jul 2026, 00:23 UTC

The Problem: Memory Leaks in Dynamic Package Loading
When a Delphi application loads a runtime package with LoadPackage, the host often instantiates objects inside that package. With plain TObject references the host must call Free manually. If the package unloads while the host still holds a reference, or the host forgets to free, the process hits access violations or leaks.
Interface-based reference counting solves this. Returning an interface instead of a class lets the object manage its own lifetime. When the last interface reference is released, the object destroys itself even across module boundaries.
The Smallest Suitable Design
A shared interface definition, a TInterfacedObject implementation hidden in the package, and a factory that returns the interface.
Shared Interface
Define a pure abstract interface in a unit both host and package can see without pulling in implementation details.
type
IMyService = interface
['{GUID-GOES-HERE}']
function ExecuteTask(const Input: string): string;
end;
Package Implementation
The implementation inherits from TInterfacedObject to get _AddRef and _Release.
type
TMyService = class(TInterfacedObject, IMyService)
public
function ExecuteTask(const Input: string): string;
end;
function CreateMyService: IMyService;
begin
Result := TMyService.Create;
end;
Host Consumption
The host loads the package, obtains the factory via GetProcAddress, and holds the interface. Release is automatic on scope exit.
var
Service: IMyService;
begin
Service := GetProcAddress('CreateMyService');
end;
Trust and Data Boundaries
The host only sees the interface pointer and VMT. It cannot access private fields or memory layout of TMyService. This reduces coupling and lets the package change internals without recompiling the host.
Operational Checks
Validate the factory returns non‑nil, and that reference count increments on assignment and decrements on scope exit.
- Log or breakpoint in
TMyServicedestructor to confirm destruction timing. - Assign the interface inside a limited scope and verify destructor fires after exit.
- Enable
ReportMemoryLeaksOnShutdownand repeat load/unload cycles to confirm no growth.
Failure Modes
| Scenario | Result | Mitigation |
|---|---|---|
Returning TObject cast to interface |
Access violation | Implement _AddRef/_Release via TInterfacedObject |
Manual Free on interface |
Double free | Never call Free on interface variables |
| Circular references | Leak | Break cycles or use weak references before unload |
When the Design Changes
On Win32/Win64 this pattern is stable. On mobile targets prior to Delphi 10.4 Sydney, ARC behavior differed from desktop TInterfacedObject counting, so test on each target platform.
If the host needs deterministic destruction at an exact moment rather than on reference count zero, replace the interface with an explicit lifecycle manager or DisposeOf call.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.