Calling C Libraries with dart:ffi
Bind to native shared libraries and marshal structs and pointers through dart:ffi.
Calling C Libraries with dart:ffi is a free Flutter Mobile Development lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Flutter Mobile Development learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why dart:ffi?
dart:ffi is Dart's Foreign Function Interface. It lets your Flutter app call functions in native C shared libraries (.so, .dylib, .dll, or the iOS process image) directly, with no platform-channel round-trip.
- Synchronous by default and very low overhead, unlike
MethodChannelwhich serializes messages across an async boundary. - Ideal for CPU-heavy code, existing C/C++/Rust libraries, and OS-level APIs (sqlite, libsodium, image codecs).
- You bind a C signature to a Dart signature, then call it like an ordinary function.
The cost: you manage memory and types yourself. Get a pointer or a struct layout wrong and you crash the whole process.
Opening a DynamicLibrary
Everything starts with a DynamicLibrary. It is the handle to the loaded native code from which you look up symbols.
DynamicLibrary.open(path)loads a shared library by file name. On Android use'libfoo.so'; on iOS/macOS code is usually statically linked, so useDynamicLibrary.process()orDynamicLibrary.executable().- Pick the right name per platform with
Platform.isAndroid/Platform.isIOS.
import 'dart:ffi';
import 'dart:io' show Platform;
DynamicLibrary openNativeLib() {
if (Platform.isAndroid) {
return DynamicLibrary.open('libnative_math.so');
}
if (Platform.isIOS || Platform.isMacOS) {
// Symbols are linked into the app process on iOS.
return DynamicLibrary.process();
}
if (Platform.isWindows) {
return DynamicLibrary.open('native_math.dll');
}
return DynamicLibrary.open('libnative_math.so');
}Native types vs Dart types
FFI uses two type universes. The native type describes the C ABI; the Dart type is what your Dart code actually sees.
Int32,Int64,Uint8,Double,Floatare native marker types — you never instantiate them, they map to Dartint/double.Pointer<T>is a native address.Voidmarks no value.- The C function type is written with
Functionusing native types; the Dart-facing type uses plain Dart types.
Example: C int32_t add(int32_t, int32_t) becomes native Int32 Function(Int32, Int32) and Dart int Function(int, int).
Looking up and calling a function
Use lookupFunction to bind a C symbol to a Dart function in one call. It takes two generic parameters: the native signature and the Dart signature.
- The first type argument must use native types (
Int32,Double, …). - The second is the callable Dart type returned to you.
Below, a pure-Dart simulation shows the call shape that FFI mirrors at runtime.
// Conceptually, FFI does this:
// typedef NativeAdd = Int32 Function(Int32, Int32);
// typedef DartAdd = int Function(int, int);
// final add = lib.lookupFunction<NativeAdd, DartAdd>('add');
// Pure-Dart stand-in so the call site is identical in shape:
int Function(int, int) bindAdd() {
return (int a, int b) => a + b; // native impl returns a + b
}
void main() {
final add = bindAdd();
print('add(20, 22) = ${add(20, 22)}');
}typedef for clean bindings
Real bindings declare the two signatures as typedefs. This keeps lookupFunction readable and lets you reuse signatures.
- Native typedef uses native marker types and the suffix convention
...Native. - Dart typedef uses Dart types.
- The string passed to
lookupFunctionis the exact exported C symbol name.
import 'dart:ffi';
// C: double native_pow(double base, int32_t exp);
typedef NativePowNative = Double Function(Double, Int32);
typedef NativePow = double Function(double, int);
class MathBindings {
final DynamicLibrary lib;
late final NativePow pow;
MathBindings(this.lib) {
pow = lib.lookupFunction<NativePowNative, NativePow>('native_pow');
}
}Allocating native memory
To pass pointers you must allocate native (off-heap) memory. The package:ffi library provides malloc (a calloc variant also exists) plus extensions for strings.
malloc<Int32>()returns aPointer<Int32>; use.valueto read/write.malloc<Int32>(n)allocates an array ofnelements; index withptr[i]orptr.elementAt(i).- You must free what you allocate with
malloc.free(ptr)— the GC does not track native memory.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
void usePointer() {
final ptr = malloc<Int32>(3); // array of 3 int32
try {
ptr[0] = 10;
ptr[1] = 20;
ptr[2] = 12;
var sum = 0;
for (var i = 0; i < 3; i++) {
sum += ptr[i];
}
print('sum = $sum');
} finally {
malloc.free(ptr); // always free
}
}Marshalling strings
C strings are null-terminated char*, represented as Pointer<Utf8> (from package:ffi). Conversion goes both ways:
- Dart → C:
myString.toNativeUtf8()allocates a native buffer (free it later). - C → Dart:
ptr.toDartString()copies the bytes into a DartString.
If the native function returns a pointer it allocated, you typically must call its matching free export — never malloc.free memory you did not allocate with malloc.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// C: int32_t count_chars(const char* text);
typedef CountNative = Int32 Function(Pointer<Utf8>);
typedef Count = int Function(Pointer<Utf8>);
int countChars(Count nativeCount, String text) {
final cStr = text.toNativeUtf8();
try {
return nativeCount(cStr);
} finally {
malloc.free(cStr);
}
}Defining a Struct
To marshal C structs, declare a Dart class extending Struct. Each field is annotated with its native type so the FFI runtime computes the exact memory layout/offsets.
- Scalar fields get annotations like
@Int32(),@Double(). - Field order and types must match the C struct exactly, including padding/alignment rules.
- You never construct a
Structwithnew; you obtain one via aPointer<T>.refbacked by native memory.
import 'dart:ffi';
// C:
// typedef struct { double x; double y; } Point;
final class Point extends Struct {
@Double()
external double x;
@Double()
external double y;
}Passing structs by pointer
Most C APIs take a Point*. Allocate the struct, fill it through .ref, pass the pointer, then read results back.
malloc<Point>()gives aPointer<Point>sized correctly for the layout.ptr.refis a view onto that native memory; writingptr.ref.x = 3.0mutates the C struct in place.- The native function reads/writes the same memory — this is how you get values out by reference.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// C: void translate(Point* p, double dx, double dy);
typedef TranslateNative = Void Function(Pointer<Point>, Double, Double);
typedef Translate = void Function(Pointer<Point>, double, double);
final class Point extends Struct {
@Double()
external double x;
@Double()
external double y;
}
void moveOrigin(Translate translate) {
final p = malloc<Point>();
try {
p.ref.x = 0;
p.ref.y = 0;
translate(p, 4.0, 5.0);
print('moved to (${p.ref.x}, ${p.ref.y})');
} finally {
malloc.free(p);
}
}Don't block the UI thread
FFI calls are synchronous: they run on the calling isolate's thread. A long native computation called from the main isolate freezes Flutter's UI.
- For heavy work, run the FFI call inside an
Isolate(e.g.Isolate.runon modern Dart) or a worker isolate. - Note: a
DynamicLibraryhandle and native pointers can be passed between isolates as addresses, but each isolate must re-open or share carefully — treat pointers as plain integers across boundaries. - Native code that calls back into Dart must use
NativeCallable/ send ports, not arbitrary threads.
import 'dart:isolate';
// Simulates offloading a heavy native FFI computation off the UI thread.
int _heavyNativeWork(int n) {
var acc = 0;
for (var i = 0; i < n; i++) {
acc = (acc + i) % 1000003;
}
return acc;
}
Future<void> main() async {
final result = await Isolate.run(() => _heavyNativeWork(5000000));
print('result = $result');
}Memory safety and ownership
FFI bugs are process crashes, not exceptions. Discipline matters:
- Ownership: whoever allocates must free. Memory from
malloc→malloc.free. Memory from a C library → that library's destructor export. - Wrap allocate/use/free in
try/finallyso you free even on error. - For long-lived native objects, attach a
NativeFinalizerso the destructor runs when the Dart wrapper is GC'd. - Never read
.ref/.valueon a pointer after it is freed — that is a use-after-free.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
class SafeBuffer {
final Pointer<Uint8> ptr;
final int length;
SafeBuffer(this.length) : ptr = malloc<Uint8>(length);
void dispose() => malloc.free(ptr);
}
void main() {
final buf = SafeBuffer(16);
try {
buf.ptr[0] = 255;
print('first byte = ${buf.ptr[0]}');
} finally {
buf.dispose();
}
}Quick Check
Answer based on dart:ffi struct and memory rules.
Recap
You can now bind to native C libraries from Flutter with dart:ffi:
- Load code with
DynamicLibrary.open/.process(), choosing the path per platform. - Bind symbols with
lookupFunction<Native, Dart>, declaring native vs Darttypedefs. - Allocate off-heap memory with
malloc, marshal strings viatoNativeUtf8/toDartString, and pass arrays as pointers. - Define structs by extending
Structwith native-type annotations; pass them by pointer and read results through.ref. - Keep heavy calls off the UI isolate, and enforce strict ownership: free what you allocate, use
try/finallyandNativeFinalizer, and never touch freed pointers.
FFI trades safety for speed and reach — correct types, layout, and lifetimes are entirely your responsibility.
Frequently asked questions
Is the “Calling C Libraries with dart:ffi” lesson free?
Yes — the full text of “Calling C Libraries with dart:ffi” is free to read here on the web, and the Flutter Mobile Development course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Flutter Mobile Development course, upgrade to CoddyKit PRO.
What will I learn in “Calling C Libraries with dart:ffi”?
Bind to native shared libraries and marshal structs and pointers through dart:ffi. You practise Flutter Mobile Development with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Flutter Mobile Development?
No prior experience is required. Flutter Mobile Development on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Calling C Libraries with dart:ffi” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Flutter Mobile Development lesson?
Yes. Every Flutter Mobile Development lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.