Skip to main content

Callbacks

Some Win32 APIs invoke caller-supplied functions to report progress, stream results, or notify events. These are not Dart-style async callbacks — they are native calls back into Dart code.

From Dart FFI's perspective, a Win32 callback is a function pointer that the native API stores and later invokes on an arbitrary thread.

Creating Callbacks​

Win32 API may invoke your callback from:

  • The same thread
  • A worker thread
  • A thread pool thread
  • A GUI thread

Dart therefore provides multiple callback modes with different safety and threading guarantees to create native-callable callbacks.

ConstructorCallable FromReturn TypeRestrictions
NativeCallable.isolateLocalSame thread onlyAnyMust be invoked on the creating thread
NativeCallable.listenerAny threadvoid-onlyNo return values
NativeCallable.isolateGroupBoundAny threadAnyMay only access isolate-group-shared state

NativeCallable.isolateLocal​

Use this when the native API guarantees that the callback runs on the same thread that registered it.

final callback = NativeCallable<FONTENUMPROC>.isolateLocal(
enumerateFonts,
exceptionalReturn: 0,
);

NativeCallable.listener​

Use this when the callback:

  • May be invoked from any thread
  • Returns void
  • Is used for notifications or events
final callback = NativeCallable<Void Function(Int32)>.listener(onEvent);

NativeCallable.isolateGroupBound (Preview)​

Requires --experimental-shared-data flag to be passed to the Dart VM in order to access isolate-group-shared state.

For example: dart --experimental-shared-data run example.dart

Use this when the callback:

  • May be invoked from any thread
  • Must return a value
final callback = NativeCallable<FONTENUMPROC>.isolateGroupBound(
enumerateFonts,
exceptionalReturn: 0,
);

This allows invocation from arbitrary threads, but with the restriction that the callback may only access isolate-group-shared state.

Defining a Callback Function​

The Dart function must match the native callback signature exactly.

For example, let's look at the EnumFontFamiliesEx function, which enumerates all uniquely-named fonts in the system that match a specified set of font characteristics. EnumFontFamiliesEx takes a LOGFONT struct which contains information about the fonts to enumerate.

The Dart function signature looks like this:

int EnumFontFamiliesEx(
int hdc,
Pointer<LOGFONT> lpLogfont,
Pointer<NativeFunction<FONTENUMPROC>> lpProc,
int lParam,
int dwFlags,
) { ... }

Notice the third parameter — a pointer to the callback function. This is called once for every enumerated font, and is defined as:

typedef FONTENUMPROC =
Int32 Function(
Pointer<LOGFONT> param0,
Pointer<TEXTMETRIC> param1,
Uint32 param2,
IntPtr param3,
);

Define a Dart function using Dart equivalents:

int enumerateFonts(
Pointer<LOGFONT> logFont,
Pointer<TEXTMETRIC> _,
int _,
int _,
) {
final logFontEx = logFont.cast<ENUMLOGFONTEX>();
print(logFontEx.ref.elfFullName);
return TRUE; // continue enumeration
}

Registering a Callback​

Example using EnumFontFamiliesEx:

fonts.dart
int enumerateFonts(
Pointer<LOGFONT> logFont,
Pointer<TEXTMETRIC> _,
int _,
int _,
) {
final logFontEx = logFont.cast<ENUMLOGFONTEX>();
print(logFontEx.ref.elfFullName);
return TRUE; // continue enumeration
}

void main() {
final hDC = GetDC(null);
final searchFont = adaptiveCalloc<LOGFONT>()
..ref.lfCharSet = HANGUL_CHARSET;

final callback = NativeCallable<FONTENUMPROC>.isolateLocal(
enumerateFonts,
exceptionalReturn: 0,
);

EnumFontFamiliesEx(
hDC,
searchFont,
callback.nativeFunction,
const LPARAM(0),
0,
);

callback.close();
free(searchFont);
}

Lifetime And Ownership​

Callbacks are native resources.

You must close them when no longer needed:

final callback = NativeCallable<FONTENUMPROC>.isolateLocal(...);
...
callback.close();

Choosing the Right Callback Mode​

SituationUse
Callback runs on the registering threadNativeCallable.isolateLocal
Callback runs on arbitrary threads, returns voidNativeCallable.listener
Callback runs on arbitrary threads, returns a valueNativeCallable.isolateGroupBound
TIP

Prefer NativeCallable.isolateLocal. Use the other two only when forced by the API's threading behavior.

Common Failure Modes​

Wrong Thread​

Cannot invoke a native callback outside an isolate.

Cause:

You used NativeCallable.isolateLocal, but Win32 API invoked the callback from another thread.

Fix:

Switch to NativeCallable.isolateGroupBound or NativeCallable.listener.

Use-After-Close​

Crash or access violation.

Cause:

You closed the callback while native code still held its function pointer.

Fix:

  • Ensure the callback outlives all native uses
  • Close only after the native API is done