FFI (Foreign Function Interface)
4 min read
Platform Integration
| Platform Channels | FFI | |
|---|---|---|
| Call style | Async (Future) | Synchronous |
| Overhead per call | ~100µs (thread hop + codec) | ~10ns (direct native call) |
| Targets | Swift / Kotlin | C / C++ (or anything exposing a C ABI: Rust, Zig, Go) |
| Memory management | Codec handles it | Manual — you allocate / free pointers |
| Setup | Simple | More involved (build native lib, link, sign) |
| Codegen helper | Pigeon | ffigen (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
| Situation | Use 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
-
What's
ffigenand why use it?ffigenis 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. -
How do you call FFI from an isolate?
DynamicLibraryhandles are isolate-safe — you can open the same library in each isolate, or useDynamicLibrary.executable()/DynamicLibrary.process()which share. For heavy work, wrap the FFI call inIsolate.run(() => nativeFn(...))so it doesn't block the UI isolate. -
What kinds of values can cross the FFI boundary? Primitives (
int,double,bool), pointers (Pointer<T>), and structs (Structsubclasses) with primitive fields. Dart objects can't be passed directly — you serialise (e.g., copy bytes into aPointer<Uint8>) on each side. -
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.