Platform Channels & Native Integration

Flutter FFI: Calling C/C++ Libraries Directly from Dart

16 min Lesson 10 of 11

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.

Note: 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 (.so on Android/Linux, .dylib on macOS/iOS, .dll on Windows).
  • Load the library at runtime using DynamicLibrary.open() or DynamicLibrary.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> (from package:ffi)
  • struct → subclass of Struct
Tip: The 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);
}
Warning: Native memory allocated with 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 .so files in android/app/src/main/jniLibs/<ABI>/ (e.g., arm64-v8a, x86_64). Flutter's Gradle plugin picks them up automatically.
  • iOS/macOS: Add the .dylib or .xcframework to the Xcode target's "Frameworks, Libraries, and Embedded Content".
  • Linux/Windows/Desktop: Place the .so/.dll next to the executable or in a path returned by DynamicLibrary.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.

Summary: 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.