Skip to content
Blog

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 global Allocators), not raw C malloc, unless you know the ownership crosses the boundary.
  • Every allocate needs a matching free — on every path, including error paths.
  • Prefer NativeFinalizer to tie cleanup to a Dart object’s lifetime when you can’t free eagerly.
  • Pointer arithmetic is element-size aware: p[i] moves by i * sizeOf<Uint8>(), not i bytes.

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:ffiPlatform channels / Pigeon
Best forExisting C/C++ libs, CPU-heavy work (codecs, crypto, math)OS APIs exposed through Kotlin/Swift/Obj-C
Data crossingPointers, structs — near zero-costSerialized messages — some overhead
SetupNative lib + bindingsMethod/channel declarations
MemoryManual (you own it)Managed by the platform side
ExamplesSQLite, image filters, game enginesBattery 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

  1. Wrap every native call behind a Dart API — never let Pointer escape into widgets.
  2. Audit every allocation for a free, and consider NativeFinalizer.
  3. Generate bindings with ffigen; don’t hand-maintain struct layouts.
  4. Handle DynamicLibrary.open failure gracefully (missing lib = crash at startup).
  5. Test on each target OS — ABI and library-naming differences are real.
  6. 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.