Lesson 40 of 77 advanced

FFI & Native Library Binding

Calling C/C++ libraries directly from Dart — maximum performance when platform channels are not enough

Open interactive version (quiz + challenge)

Real-world analogy

Platform channels are like making a phone call to get information from a native department — you explain what you need, they look it up, and they call back. FFI is like being given a direct pass-key to the file room and retrieving the document yourself. It is faster and more direct, but you need to know exactly where things are and clean up after yourself.

What is it?

dart:ffi (Foreign Function Interface) provides a direct bridge between Dart and C/C++ code, bypassing the platform channel mechanism entirely. It is used for performance-critical native integrations and reusing existing C/C++ libraries on both Android and iOS from a single Dart binding.

Real-world relevance

On a field survey app that processed offline map tiles, the team integrated a C-based image compression library via FFI instead of a platform channel. The channel-based approach took 8ms per tile (unacceptable for real-time map rendering). FFI reduced it to 0.3ms — a 26x improvement.

Key points

Code example

// === Simple FFI example: calling C sqrt function ===
import 'dart:ffi';
import 'dart:io' show Platform;

// 1. Define the C function signature in Dart FFI types
typedef SqrtC = Double Function(Double);          // C type
typedef SqrtDart = double Function(double);       // Dart type

// 2. Load the library
final DynamicLibrary stdlib = Platform.isAndroid
    ? DynamicLibrary.open('libm.so')              // Android: libm.so
    : DynamicLibrary.process();                   // iOS: statically linked

// 3. Look up the function and get a Dart callable
final SqrtDart nativeSqrt = stdlib
    .lookup<NativeFunction<SqrtC>>('sqrt')
    .asFunction<SqrtDart>();

void main() {
  print(nativeSqrt(9.0));  // 3.0 — called directly, no channel overhead
}

// === Real-world FFI: custom C library for NFC data parsing ===
// native/nfc_parser.h:
// struct NfcPayload { int32_t length; uint8_t* data; };
// int32_t parse_nfc_ndef(uint8_t* raw, int32_t len, NfcPayload* out);

// Dart binding generated by ffigen or written manually:
final class NfcPayload extends Struct {
  @Int32()
  external int length;

  external Pointer<Uint8> data;
}

typedef ParseNdefC = Int32 Function(
  Pointer<Uint8> raw,
  Int32 len,
  Pointer<NfcPayload> out,
);
typedef ParseNdefDart = int Function(
  Pointer<Uint8> raw,
  int len,
  Pointer<NfcPayload> out,
);

class NfcParser {
  static final _lib = DynamicLibrary.open('libnfc_parser.so');
  static final _parseNdef = _lib
      .lookup<NativeFunction<ParseNdefC>>('parse_nfc_ndef')
      .asFunction<ParseNdefDart>();

  static Uint8List parseNdef(Uint8List rawBytes) {
    // Allocate native memory
    final arena = Arena();
    try {
      final rawPtr = arena<Uint8>(rawBytes.length);
      rawPtr.asTypedList(rawBytes.length).setAll(0, rawBytes);

      final outPtr = arena<NfcPayload>();
      final result = _parseNdef(rawPtr, rawBytes.length, outPtr);

      if (result < 0) throw NfcParseException('Parse failed: $result');

      return outPtr.ref.data.asTypedList(outPtr.ref.length);
    } finally {
      arena.releaseAll(); // Frees all arena-allocated memory
    }
  }
}

// === pubspec.yaml for ffigen ===
// dev_dependencies:
//   ffigen: ^9.0.0
//
// ffigen:
//   output: 'lib/src/native/bindings.dart'
//   headers:
//     entry-points:
//       - 'native/nfc_parser.h'

Line-by-line walkthrough

  1. 1. typedef SqrtC = Double Function(Double) — the C function signature in FFI native types
  2. 2. typedef SqrtDart = double Function(double) — the equivalent Dart callable type
  3. 3. DynamicLibrary.open('libm.so') on Android — loads the C math library from the Android system
  4. 4. stdlib.lookup>('sqrt').asFunction() — finds the 'sqrt' symbol and wraps it as a Dart-callable function
  5. 5. nativeSqrt(9.0) — calls C sqrt directly; no channel overhead, no serialisation
  6. 6. final class NfcPayload extends Struct — maps a C struct to Dart; @Int32() annotation tells FFI the field size
  7. 7. Arena arena — scoped allocator; all allocations freed in the finally block via arena.releaseAll()
  8. 8. rawPtr.asTypedList(rawBytes.length).setAll(0, rawBytes) — copies Dart Uint8List bytes into native memory for the C function to read

Spot the bug

import 'dart:ffi';

typedef AddC = Int32 Function(Int32, Int32);
typedef AddDart = int Function(int, int);

final lib = DynamicLibrary.open('libmath.so');
final add = lib.lookup<NativeFunction<AddC>>('add')
    .asFunction<AddDart>();

void processItems(List<int> items) {
  for (final item in items) {
    final result = add(item, 10);
    print(result);
  }
}
Need a hint?
This code is technically correct but has a performance/architecture problem for large lists. Think about where this is likely running.
Show answer
Bug: If processItems() is called with thousands of items on the main Dart isolate, the FFI calls (even though fast individually) collectively block the event loop. For large lists with heavy native processing, this should be offloaded to a background isolate using Isolate.run() or compute(). Additionally, DynamicLibrary.open() should be called once and cached as a top-level or static variable — calling it inside processItems() on every call would be extremely expensive (though in this code it is already at the top level, which is correct). The fix for the isolate issue: wrap processItems() in Isolate.run(() => processItems(items)) from the caller.

Explain like I'm 5

Normally Flutter asks Android 'can you do this thing for me?' through a polite messenger (platform channel). FFI is different — Dart goes into the C library's house directly, picks up the tool it needs, uses it, and puts it back. It is faster because there is no messenger, but you have to clean up after yourself — or the house gets messy (memory leak).

Fun fact

Dart's FFI was inspired by Python's ctypes and Go's cgo. It became stable in Dart 2.12 (Flutter 2.0). Before FFI, every native integration required both Dart and Kotlin/Swift code — FFI enables pure Dart-and-C libraries that work on Android, iOS, Windows, macOS, and Linux from a single codebase.

Hands-on challenge

Describe in interview-ready language: (1) one real scenario where you would choose FFI over a MethodChannel, (2) how you would handle memory management for a C struct returned from an FFI call, (3) what risk you would warn about when running FFI on the main isolate.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery