dart:ffi in Flutter: Calling Native C/C++ Code the Safe Way
A practical guide to Dart FFI: DynamicLibrary lookup, pointers and structs, memory you must free yourself, generating bindings with ffigen, and when to choose FFI over platform channels.
Published on • October 3, 2026
AI Assistant

Sometimes Dart isn’t where the work should happen: a crypto library, a codec, a simulation core, an existing C/C++ dependency you can’t rewrite. dart:ffi (Foreign Function Interface) lets Dart call into native code and read, write, allocate and deallocate native memory — no serialization, no platform-channel marshalling, no copies.
Sources: Dart C interop, Dart interop overview, dart:ffi API.
The three-step call pattern
import 'dart:ffi';
// 1. Native signature (what the C function actually looks like)
typedef HelloNative = Void Function();
// 2. Dart signature (what you'll call from Dart)
typedef HelloDart = void Function();
void main() {
// 1. Open the library
final dylib = DynamicLibrary.open('libgreet.so'); // .dylib on macOS, .dll on Windows
// 2. Look up the symbol and cast it
final hello = dylib
.lookup<NativeFunction<HelloNative>>('hello_world')
.asFunction<HelloDart>();
// 3. Call it like any Dart function
hello();
}
You always need two typedefs — the ABI-correct native one and the ergonomic Dart one — because asFunction is what bridges them. On Flutter mobile, DynamicLibrary.process() opens the current executable, which is how you reach symbols already linked into the app.
The type system mirrors C: Bool, Float, Double, Int8–Int64, Uint8–Uint64, Void, plus ABI-specific IntPtr, Size, Long. For aggregates you get four instantiable types: Pointer, Struct, Union, and Array.
Memory: the part that bites
Dart’s GC manages Dart objects. Native heap memory is not garbage collected — you must free it yourself.
import 'dart:ffi';
final Pointer<Uint8> p = calloc<Uint8>(64); // zeroed native memory
final Pointer<Uint8> m = malloc<Uint8>(64); // uninitialized
p[0] = 42; // element-size aware indexing
calloc.free(p);
malloc.free(m);
Rules of thumb:
- Use
calloc/malloc(the globalAllocators), not raw Cmalloc, unless you know the ownership crosses the boundary. - Every
allocateneeds a matchingfree— on every path, including error paths. - Prefer
NativeFinalizerto tie cleanup to a Dart object’s lifetime when you can’t free eagerly. - Pointer arithmetic is element-size aware:
p[i]moves byi * sizeOf<Uint8>(), notibytes.
Get this wrong and you don’t get an exception — you get a corrupted heap three screens later. This is the single biggest reason FFI code needs more review than the rest of your Dart.
Skip the typing: ffigen
Hand-writing typedefs doesn’t scale. package:ffigen reads C headers and generates the Dart bindings:
dart run ffigen --config ffigen.yaml
You point it at a header and a library, and it emits lookup wrappers, struct definitions and enums. For building/bundling the native code itself, Dart’s build hooks (the successor to the “native assets” experiment) compile and register native libraries with your app so DynamicLibrary.open just works across platforms.
Two platform gotchas from the docs: macOS only loads signed libraries, and FFI needs the library present per-target (an .so won’t help on Windows).
FFI or platform channels?
dart:ffi | Platform channels / Pigeon | |
|---|---|---|
| Best for | Existing C/C++ libs, CPU-heavy work (codecs, crypto, math) | OS APIs exposed through Kotlin/Swift/Obj-C |
| Data crossing | Pointers, structs — near zero-cost | Serialized messages — some overhead |
| Setup | Native lib + bindings | Method/channel declarations |
| Memory | Manual (you own it) | Managed by the platform side |
| Examples | SQLite, image filters, game engines | Battery level, sensors, intents, Keychain |
A common hybrid: FFI for the compute core, a thin platform-channel or Pigeon wrapper for OS features.
The Web (and Wasm) caveat
dart:ffi cannot be imported when compiling to WebAssembly — it’s a compile error, not a runtime limitation. For web builds, use dart:js_interop / package:web, or community shims such as wasm_ffi/universal_ffi if you need an FFI-shaped API. Structure your code so the native layer sits behind an interface with a web implementation.
A checklist before you ship
- Wrap every native call behind a Dart API — never let
Pointerescape into widgets. - Audit every allocation for a
free, and considerNativeFinalizer. - Generate bindings with
ffigen; don’t hand-maintain struct layouts. - Handle
DynamicLibrary.openfailure gracefully (missing lib = crash at startup). - Test on each target OS — ABI and library-naming differences are real.
- Have a web strategy from day one.
Done right, FFI is the fastest door out of Dart — and with ffigen plus build hooks, the door is far less scary than it was a few years ago.