Skip to main content

Patterns for Memory Management

Consider the following bad example of a function that calls CoCreateGuid to return a string:

guid.dart
import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';

// BAD: Don't do this, since the memory for pGuid may not be released.
String createGUID() {
final pGuid = calloc<GUID>();

final hr = CoCreateGuid(pGuid);
if (FAILED(hr)) throw WindowsException(hr);
final guid = pGuid.ref.toString();
free(pGuid);
return guid;
}
Can you see a potential bug in this code?

Here's the problem: if CoCreateGuid() fails, pGuid will not be released prior to the exception being thrown. In this example, there are simple solutions (e.g., call free in both failure and success scenarios). However, when you're allocating many different objects, this approach can lead to unreadable code with plenty of opportunities for memory leaks.

The try/finally pattern

One effective approach is using the try/finally pattern in Dart, which ensures that the finally clause will be executed regardless of whether an exception is thrown.

// GOOD: This approach is safe and convenient.
String createGUID() {
final pGuid = calloc<GUID>();
try {
final hr = CoCreateGuid(pGuid);
if (FAILED(hr)) throw WindowsException(hr);
return pGuid.ref.toString();
} finally {
free(pGuid);
}
}

In the above example, free will be called regardless of whether CoCreateGuid() fails or not. There's no need to allocate the result to a separate string before returning, as the finally block ensures proper cleanup at the appropriate time.

The using pattern

For simpler methods, the try/finally pattern works well. But as you add more manually-allocated objects, this approach becomes more unwieldy.

For example, consider this function, which queries Windows for the timestamp when the currently-running process was created:

process.dart
import 'dart:ffi';

import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';

DateTime processCreationTime() {
final hProcess = GetCurrentProcess();
final pCreationTime = calloc<FILETIME>();
final pExitTime = calloc<FILETIME>();
final pKernelTime = calloc<FILETIME>();
final pUserTime = calloc<FILETIME>();
final pCreationSystemTime = calloc<SYSTEMTIME>();

try {
// Retrieve timing information for the current process.
var hr = GetProcessTimes(
hProcess, pCreationTime, pExitTime, pKernelTime, pUserTime);
if (FAILED(hr)) throw WindowsException(hr);

// Convert to UTC.
hr = FileTimeToSystemTime(pCreationTime, pCreationSystemTime);
if (FAILED(hr)) throw WindowsException(hr);
final SYSTEMTIME(:wYear, :wMonth, :wDay, :wHour, :wMinute, :wSecond) =
pCreationSystemTime.ref;
return DateTime.utc(wYear, wMonth, wDay, wHour, wMinute, wSecond).toLocal();
} finally {
free(pCreationTime);
free(pExitTime);
free(pKernelTime);
free(pUserTime);
free(pCreationSystemTime);
}
}

There are two problems with this code. Firstly, each variable has to be individually freed. Secondly, the variable pCreationSystemTime has to be allocated even if the first call fails. This can be a nuisance in larger methods.

An alternative approach is to use the using pattern with an Arena object, which is a memory allocator that tracks memory allocations and automatically releases them when the using scope ends.

Here's the same function written this way:

DateTime processCreationTime() {
return using((arena) {
final hProcess = GetCurrentProcess();
final pCreationTime = arena<FILETIME>();
final pExitTime = arena<FILETIME>();
final pKernelTime = arena<FILETIME>();
final pUserTime = arena<FILETIME>();

// Retrieve timing information for the current process.
var hr = GetProcessTimes(
hProcess, pCreationTime, pExitTime, pKernelTime, pUserTime);
if (FAILED(hr)) throw WindowsException(hr);

// Convert to UTC.
final pCreationSystemTime = arena<SYSTEMTIME>();
hr = FileTimeToSystemTime(pCreationTime, pCreationSystemTime);
if (FAILED(hr)) throw WindowsException(hr);
final SYSTEMTIME(:wYear, :wMonth, :wDay, :wHour, :wMinute, :wSecond) =
pCreationSystemTime.ref;
return DateTime.utc(wYear, wMonth, wDay, wHour, wMinute, wSecond).toLocal();
});
}

In the above code, the need for individual free calls is dispensed with. When the using scope ends, the arena releases all the variables that have been allocated. Arenas can also be nested or shared across functions, which can be useful when you need greater control over the lifetime of manually-allocated memory.

TIP

The .toNativeUtf16() String extension method supports passing a custom allocator, making it compatible with Arenas:

final pTitle = 'Window title'.toNativeUtf16(allocator: arena);