Raw WinAPI calls in Delphi tend to spread handle management and error checking across the whole codebase, which makes them painful to maintain and easy to get wrong. This post expands on my earlier notes on dot-notation wrappers, showing how to wrap WinAPI handles in thin classes that keep the low-level calls in one place without hiding what’s actually happening.
The problem
Delphi gives direct access to WinAPI, which is one of its real advantages over more sandboxed platforms — but using it directly, function by function, spreads resource management everywhere. A typical piece of code ends up looking like this:
// Typical direct WinAPI usage — handle lifetime is the caller's problem
var
hFile: THandle;
BytesRead: DWORD;
Buf: array[0..255] of Byte;
begin
hFile := CreateFile('data.bin', GENERIC_READ, FILE_SHARE_READ, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if hFile <> INVALID_HANDLE_VALUE then
try
ReadFile(hFile, Buf, SizeOf(Buf), BytesRead, nil);
// ... use Buf ...
finally
CloseHandle(hFile);
end;
end;
Nothing wrong with this on its own, but multiply it by every place in the codebase that touches a file handle, a registry key, or a device context, and you get inconsistent error handling, forgotten CloseHandle calls in less disciplined hands, and no single place to add logging or diagnostics later. I covered the basic idea of wrapping these calls behind dot notation in an earlier post; this one goes further into the patterns that make the wrappers actually worth having.
The core pattern: thin RAII-style wrapper
Delphi doesn’t have destructors that run deterministically the way C++ does outside of reference counting, but a class with a constructor that acquires and a destructor that releases, used inside a try/finally, gets you the same effect. The wrapper should do exactly one thing: own the handle and expose the calls that operate on it as methods.
type
TWinFile = class
private
FHandle: THandle;
public
constructor Create(const FileName: string; Access, ShareMode,
CreationDisposition: DWORD);
destructor Destroy; override;
function Read(var Buf; Count: DWORD): DWORD;
function Write(const Buf; Count: DWORD): DWORD;
property Handle: THandle read FHandle;
end;
constructor TWinFile.Create(const FileName: string; Access, ShareMode,
CreationDisposition: DWORD);
begin
inherited Create;
FHandle := CreateFile(PChar(FileName), Access, ShareMode, nil,
CreationDisposition, FILE_ATTRIBUTE_NORMAL, 0);
if FHandle = INVALID_HANDLE_VALUE then
RaiseLastOSError;
end;
destructor TWinFile.Destroy;
begin
if FHandle <> INVALID_HANDLE_VALUE then
CloseHandle(FHandle);
inherited;
end;
function TWinFile.Read(var Buf; Count: DWORD): DWORD;
begin
if not ReadFile(FHandle, Buf, Count, Result, nil) then
RaiseLastOSError;
end;
function TWinFile.Write(const Buf; Count: DWORD): DWORD;
begin
if not WriteFile(FHandle, Buf, Count, Result, nil) then
RaiseLastOSError;
end;
The call site collapses to the part that actually matters:
var
F: TWinFile;
Buf: array[0..255] of Byte;
begin
F := TWinFile.Create('data.bin', GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING);
try
F.Read(Buf, SizeOf(Buf));
// ... use Buf ...
finally
F.Free;
end;
end;
Nothing magic happened here — RaiseLastOSError still calls GetLastError under the hood, and the handle is still a raw THandle. The wrapper just moved the boilerplate to one place instead of every call site.
Using interfaces for automatic cleanup
If manually pairing try/finally with .Free throughout a codebase feels like exactly the kind of repetition the wrapper was supposed to remove, use an interface instead of a class. Delphi interfaces are reference-counted, so a local interface variable going out of scope releases automatically:
type
IWinFile = interface
['{B1F2E3D4-1234-4321-9999-000000000001}']
function Read(var Buf; Count: DWORD): DWORD;
function Write(const Buf; Count: DWORD): DWORD;
end;
TWinFile = class(TInterfacedObject, IWinFile)
private
FHandle: THandle;
public
constructor Create(const FileName: string; Access, ShareMode,
CreationDisposition: DWORD);
destructor Destroy; override;
function Read(var Buf; Count: DWORD): DWORD;
function Write(const Buf; Count: DWORD): DWORD;
end;
// constructor/destructor/Read/Write bodies identical to above
function OpenForRead(const FileName: string): IWinFile;
begin
Result := TWinFile.Create(FileName, GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING);
end;
Now the call site has no explicit cleanup at all:
var
F: IWinFile;
Buf: array[0..255] of Byte;
begin
F := OpenForRead('data.bin');
F.Read(Buf, SizeOf(Buf));
// F released automatically when it goes out of scope
end;
This is more convenient, but it’s worth being honest about the trade-off: reference counting means the handle’s lifetime is now tied to how many references exist, and circular references between interfaced objects will leak just like they do anywhere else reference counting is used. For a short-lived local resource like a file handle this is rarely a problem in practice, but it’s not free — I wouldn’t reach for it on something long-lived and shared across the object graph.
Grouping related calls, not individual functions
A mistake I made early on was wrapping WinAPI functions one-to-one — a class per function instead of a class per resource. That just renames the API without simplifying anything. The wrapper earns its place when it groups everything that operates on the same handle or the same conceptual resource, the way TWinFile groups CreateFile, ReadFile, and WriteFile around one THandle. The same approach works for registry keys:
type
TRegKey = class
private
FKey: HKEY;
public
constructor Create(Root: HKEY; const SubKey: string; Access: REGSAM);
destructor Destroy; override;
function ReadString(const ValueName: string): string;
procedure WriteString(const ValueName, Value: string);
end;
constructor TRegKey.Create(Root: HKEY; const SubKey: string; Access: REGSAM);
begin
inherited Create;
if RegOpenKeyEx(Root, PChar(SubKey), 0, Access, FKey) <> ERROR_SUCCESS then
RaiseLastOSError;
end;
destructor TRegKey.Destroy;
begin
if FKey <> 0 then
RegCloseKey(FKey);
inherited;
end;
function TRegKey.ReadString(const ValueName: string): string;
var
Buf: array[0..1023] of Char;
Size: DWORD;
ValueType: DWORD;
begin
Size := SizeOf(Buf);
if RegQueryValueEx(FKey, PChar(ValueName), nil, @ValueType,
@Buf, @Size) <> ERROR_SUCCESS then
RaiseLastOSError;
Result := string(Buf);
end;
Same shape as the file wrapper, deliberately — once the pattern is established, every new WinAPI resource type follows the same three-part structure: constructor acquires, destructor releases, methods operate.
When not to bother
Not every WinAPI call needs a wrapper. One-off calls like GetTickCount or MessageBeep have no handle to manage and no state to own — wrapping them adds a layer with nothing behind it. The pattern pays off specifically for anything that returns a handle you’re responsible for closing: files, registry keys, device contexts, mutexes, events. If there’s no CloseXxx or ReleaseXxx counterpart to worry about, plain function calls are clearer than a class would be.
Conclusion
Wrapping WinAPI resource handles in thin classes or interfaces doesn’t hide the WinAPI — it just gives handle lifetime one home instead of scattering CloseHandle calls across the codebase, and that alone is worth the small amount of boilerplate the wrapper itself costs.




Leave a Reply