FFI (Foreign Function Interface)

Medium PriorityAsked in ~45% of senior interviews

4 min read

Platform Integration

Platform ChannelsFFI
Call styleAsync (Future)Synchronous
Overhead per call~100µs (thread hop + codec)~10ns (direct native call)
TargetsSwift / KotlinC / C++ (or anything exposing a C ABI: Rust, Zig, Go)
Memory managementCodec handles itManual — you allocate / free pointers
SetupSimpleMore involved (build native lib, link, sign)
Codegen helperPigeonffigen (parses C headers, generates bindings)

Rule of thumb: Platform Channels for "API call to the OS"; FFI for "tight CPU loop in C."


Code in action — minimal binding

// native_math.c
int32_t add(int32_t a, int32_t b) {
  return a + b;
}
// Dart side
import 'dart:ffi';
import 'dart:io' show Platform;

typedef _AddC    = Int32 Function(Int32 a, Int32 b);
typedef _AddDart = int   Function(int a, int b);

class NativeMath {
  NativeMath() {
    _lib = Platform.isAndroid
        ? DynamicLibrary.open('libnative_math.so')
        : DynamicLibrary.process();             // iOS — statically linked
    add = _lib.lookupFunction<_AddC, _AddDart>('add');
  }

  late final DynamicLibrary _lib;
  late final _AddDart add;
}

// Usage — synchronous, fast
NativeMath().add(2, 3);                          // 5

For anything non-trivial, use package:ffigen — point it at a C header file and it generates the bindings automatically.


Working with strings, structs, and memory

import 'package:ffi/ffi.dart';                   // for malloc + Utf8

// Allocate a C string, call native, free it
final cStr = 'hello'.toNativeUtf8();              // allocates in C heap
try {
  someNativeFn(cStr);
} finally {
  malloc.free(cStr);                              // MUST free or leak
}

// Define a struct
final class Point extends Struct {
  @Double() external double x;
  @Double() external double y;
}

final p = malloc<Point>();
p.ref..x = 1..y = 2;
malloc.free(p);

Mistakes here = native crashes or quiet leaks. Treat FFI memory the same way you'd treat C: every allocation has a matching free.


When to use FFI

SituationUse FFI?
Image / video processing (OpenCV, FFmpeg)
Crypto / signing operations
Existing C/C++ codebase you need to reuse
Audio DSP, physics, ML inference (TensorFlow Lite)
Calling a Swift/Kotlin API❌ Use Platform Channels
One-off platform feature (camera, contacts)❌ Platform Channels
You need it to run on a background isolate✅ FFI works in isolates; channels need extra wiring

Common mistakes to avoid

❌ Forgetting to free memory
   malloc<Foo>(), use it, never free it. Leaks accumulate, app dies on long sessions.
   ✅ Use try/finally + malloc.free()

❌ Holding Dart Pointers across function calls without keeping them alive
   GC can move/collect the Dart side if you're not careful.
   ✅ Use Pointer<T> values, not raw addresses. Convert with .address only briefly.

❌ Calling FFI on the UI thread for long work
   FFI is sync — the UI thread BLOCKS while the C code runs.
   ✅ Run heavy native work in an Isolate (Isolate.run + FFI works well).

❌ Hand-writing bindings for a big API
   String typos, missing typedefs, wrong types → segfaults.
   ✅ Use ffigen with a YAML config to autogenerate.

❌ Shipping the wrong CPU architecture
   ARM vs x86_64 mismatches → "library not found" at runtime.
   ✅ Build for all targets your CI ships (arm64, x86_64 for sims, etc).

❌ Assuming FFI is automatically faster
   The call IS fast, but if the C function itself is slow, you saved nothing.
   Measure before reaching for it.

Interview follow-ups

  1. What's ffigen and why use it? ffigen is a Dart tool that reads a C header file and generates Dart bindings — types, function signatures, struct layouts — automatically. For any non-trivial native library, it eliminates the string-name lookup typos and type mismatches that make hand-written bindings fragile.

  2. How do you call FFI from an isolate? DynamicLibrary handles are isolate-safe — you can open the same library in each isolate, or use DynamicLibrary.executable()/DynamicLibrary.process() which share. For heavy work, wrap the FFI call in Isolate.run(() => nativeFn(...)) so it doesn't block the UI isolate.

  3. What kinds of values can cross the FFI boundary? Primitives (int, double, bool), pointers (Pointer<T>), and structs (Struct subclasses) with primitive fields. Dart objects can't be passed directly — you serialise (e.g., copy bytes into a Pointer<Uint8>) on each side.

  4. When would you NOT use FFI even for performance work? When the equivalent Dart code is fast enough (Dart's AOT is good for tight loops on primitives), when the API surface is large enough that the binding maintenance cost exceeds the perf win, or when the algorithm is dominated by I/O (channels with async are fine). Profile first.


How helpful was this content?

Please sign in to rate this article.