Skip to main content

Structs

Win32 APIs frequently use C structs to pass data between functions. In Dart FFI, these are represented as subclasses of Struct and can live in either Dart memory or native memory. However, most Win32 APIs expect structs to be allocated in native memory.

Allocating Structs​

All Win32 structs must be allocated in native memory before being passed to an API.

The simplest and safest way is to use an Arena:

using((arena) {
final status = arena<SYSTEM_POWER_STATUS>();
});

This allocates zero-initialized native memory for the struct. The memory is automatically freed when the using() scope exits.

This is equivalent to:

final status = adaptiveCalloc<SYSTEM_POWER_STATUS>();
free(status);

...but without requiring manual cleanup.

Accessing Struct Fields​

Struct fields are accessed by dereferencing the pointer using StructPointer.ref.

final batteryFlag = status.ref.BatteryFlag;
final lifePercent = status.ref.BatteryLifePercent;

You can also destructure the struct for clarity:

final SYSTEM_POWER_STATUS(:BatteryFlag, :BatteryLifePercent) = status.ref;

This avoids repetitive status.ref.field access and makes it obvious which fields are being used.

Initializing Size Fields​

Some Win32 structs include a size field (typically named cbSize). This is used to disambiguate struct variants or versions.

For example, WNDCLASSEX must have cbSize initialized:

using((arena) {
final wc = arena<WNDCLASSEX>()..ref.cbSize = sizeOf<WNDCLASSEX>();
RegisterClassEx(wc);
});
NOTE

If a struct has a cbSize (or similar) field, you must initialize it before passing the struct to any Win32 API. Failure to do this is a common source of subtle bugs.

Common Struct Usage Patterns​

Win32 APIs use structs in three distinct ways.

Output-only Structs​

The API fills the struct with data.

As an example, consider GetSystemPowerStatus, which retrieves the current power state of the system.

sysinfo.dart
void main() {
using((arena) {
final lpSystemPowerStatus = adaptiveCalloc<SYSTEM_POWER_STATUS>();

final Win32Result(:value, :error) = GetSystemPowerStatus(
lpSystemPowerStatus,
);
if (!value) throw WindowsException(error.toHRESULT());

final SYSTEM_POWER_STATUS(:BatteryFlag, :BatteryLifePercent) =
lpSystemPowerStatus.ref;

if (BatteryFlag >= 128) {
// This value is only less than 128 if a battery is detected.
print('No system battery detected.');
} else {
if (BatteryLifePercent <= 100) {
print('Battery detected with $BatteryLifePercent% remaining.');
} else {
// Windows sets this value to 255 if it can't detect remaining life.
print('Battery detected but status unknown.');
}
}
});
}

Here:

  • The struct is allocated by the caller
  • The API writes into it
  • The fields are read afterward

Input-only Structs​

You populate the struct before passing it to the API.

using((arena) {
final point = arena<POINT>();
point.ref
..x = 10
..y = 20;
ClientToScreen(hwnd, point);
});

Input/Output Structs​

You initialize some fields, and the API may overwrite fields:

using((arena) {
final rect = arena<RECT>();
rect.ref
..left = 0
..top = 0
..right = 800
..bottom = 600;
AdjustWindowRect(rect, WS_OVERLAPPEDWINDOW, false);
final RECT(:left, :top, :right, :bottom) = rect.ref;
});