Flutter FFI: Calling C/C++ Libraries Directly from Dart
Flutter FFI: Calling C/C++ Libraries Directly from Dart
dart:ffi (Foreign Function Interface) lets Dart code call native C and C++ functions without going through a platform channel. Where a platform channel adds asynchronous overhead and requires per-platform Kotlin/Swift host code, FFI is synchronous, has near-zero overhead, and works on Android, iOS, Linux, macOS, and Windows from a single Dart binding.
FFI is the right tool when you need raw performance (audio DSP, image codecs, cryptography), when you already have a battle-tested C library (OpenSSL, SQLite, zlib), or when you want to share a single C/C++ implementation across every platform without writing five plugin wrappers.
dart:ffi is a core Dart SDK library — no extra pub.dev package is needed. It is available in Flutter and standalone Dart programs alike.How FFI Works at a High Level
The workflow has three steps:
- Compile your C/C++ code into a shared library (
.soon Android/Linux,.dylibon macOS/iOS,.dllon Windows). - Load the library at runtime using
DynamicLibrary.open()orDynamicLibrary.process(). - Bind each C function with a Dart typedef pair: one for the native C type signature, one for the Dart callable signature.
Native Types and Dart Equivalents
Every C type maps to a corresponding dart:ffi native type. The most common mappings are:
int(C 32-bit) →Int32/int(Dart)long(C 64-bit) →Int64/int(Dart)double(C) →Double/double(Dart)float(C) →Float/double(Dart)void*/ pointers →Pointer<T>char*strings →Pointer<Utf8>(frompackage:ffi)struct→ subclass ofStruct
package:ffi package (published by the Dart team) adds convenience helpers like Pointer<Utf8>.toDartString(), malloc/calloc allocators, and using() for automatic memory management. Add it with flutter pub add ffi.Example 1 — Calling a Simple C Math Function
Suppose you have a tiny C library with a function that squares an integer:
Native C header (native_math.h) and implementation
// native_math.h
int32_t square(int32_t value);
// native_math.c
#include "native_math.h"
int32_t square(int32_t value) { return value * value; }
Compile this to a shared library (e.g., libnative_math.so), place it in the Flutter project, then bind it in Dart:
Dart FFI binding for square()
import 'dart:ffi';
import 'dart:io' show Platform;
// Step 1 — typedef for the native C signature
typedef SquareNative = Int32 Function(Int32 value);
// Step 2 — typedef for the Dart-callable signature
typedef SquareDart = int Function(int value);
// Step 3 — load the shared library
final DynamicLibrary _nativeLib = Platform.isAndroid
? DynamicLibrary.open('libnative_math.so')
: DynamicLibrary.process(); // macOS/iOS: linked into the process
// Step 4 — look up and cast the symbol
final SquareDart square = _nativeLib
.lookup<NativeFunction<SquareNative>>('square')
.asFunction<SquareDart>();
void main() {
print(square(7)); // prints 49 — synchronous, zero overhead
}
Example 2 — Working with C Structs and Pointers
For more complex data, you map C structs to Dart classes that extend Struct. Fields are declared with the @Int32(), @Double(), etc. annotations.
Dart Struct mapping and pointer usage
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// Maps to: typedef struct { int32_t x; int32_t y; } Point2D;
final class Point2D extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
// C function: int32_t distance_squared(Point2D* a, Point2D* b);
typedef DistanceNative =
Int32 Function(Pointer<Point2D> a, Pointer<Point2D> b);
typedef DistanceDart =
int Function(Pointer<Point2D> a, Pointer<Point2D> b);
final distanceSquared = _nativeLib
.lookup<NativeFunction<DistanceNative>>('distance_squared')
.asFunction<DistanceDart>();
void computeDistance() {
// Allocate two Point2D structs on the native heap
final a = calloc<Point2D>();
final b = calloc<Point2D>();
a.ref.x = 0; a.ref.y = 0;
b.ref.x = 3; b.ref.y = 4;
final d2 = distanceSquared(a, b); // returns 25
print('Distance squared: $d2');
// Always free native memory
calloc.free(a);
calloc.free(b);
}
calloc or malloc is not garbage-collected by Dart. You must call calloc.free(ptr) when you are done. Forgetting to free native memory causes leaks that are invisible to Dart's VM profiler. Use the using() helper from package:ffi to scope allocations automatically.Isolates and FFI Thread Safety
Synchronous FFI calls block the calling isolate. If a C function is slow (e.g., a heavy image-processing routine), call it from a background isolate using Isolate.run() or compute() so the UI thread stays responsive. For extremely long-running C work, the C code can spawn its own OS threads — as long as it never calls back into Dart from those threads without using the Native API (Dart_PostCObject_DL).
Bundling the Shared Library in Flutter
The shared library must be bundled with the app:
- Android: Place
.sofiles inandroid/app/src/main/jniLibs/<ABI>/(e.g.,arm64-v8a,x86_64). Flutter's Gradle plugin picks them up automatically. - iOS/macOS: Add the
.dylibor.xcframeworkto the Xcode target's "Frameworks, Libraries, and Embedded Content". - Linux/Windows/Desktop: Place the
.so/.dllnext to the executable or in a path returned byDynamicLibrary.open().
FFI vs Platform Channels: When to Choose FFI
Use FFI when: you have a pure C/C++ library, you need synchronous calls, or you want a single Dart binding for all platforms.
Use platform channels when: you need to call OS-level APIs (Camera, Bluetooth, notifications) that are only exposed through platform SDKs, or when you need to run Kotlin/Swift code.
dart:ffi enables direct, synchronous calls to compiled C/C++ code from Dart. You load the library with DynamicLibrary, declare native-type typedefs, look up symbols, and call them like regular Dart functions. Manage native heap memory manually, keep long-running calls off the UI isolate, and bundle the compiled .so/.dylib/.dll with your Flutter app.