DEV Community

Cover image for Dart FFI for Flutter: Call Native C Directly (Lessons from camera_pro)
Sayed Ali Alkamel
Sayed Ali Alkamel

Posted on

Dart FFI for Flutter: Call Native C Directly (Lessons from camera_pro)

Short version: Dart FFI lets your Flutter app call C functions directly, as a normal synchronous call, instead of passing serialized messages over a platform channel. That is a big deal when you have an existing C or C++ library, or hot per-frame native work. Here is what it is, when to use it, and what I ran into building camera_pro, a camera package with a shared C core.

What is Dart FFI?

FFI stands for foreign function interface. Per the Dart docs, apps on the Dart Native platform can use the dart:ffi library to call native C APIs and to read, write, allocate, and deallocate native memory (dart.dev). In plain terms: you point Dart at a compiled C library, hand it a function signature, and call the function.

You do not have to write those bindings by hand. For anything beyond a couple of functions, package:ffigen generates the Dart wrappers straight from your C header files (dart.dev).

One boundary to remember: dart:ffi runs only on the Dart Native platform. There is no FFI on Flutter web.

FFI or platform channels?

Both let Flutter reach code the Dart SDK does not ship. They work very differently.

A platform channel passes messages between Dart and the host platform. Those messages and responses travel asynchronously, and their values are serialized into a binary format on the way across and deserialized on the other side (docs.flutter.dev). Great for occasional calls like reading the battery level or opening a share sheet.

Dart FFI is a direct call into C. No channel, no per-call serialization, and you can share native memory with the C side. That is what makes it the right tool for tight, frequent, CPU-heavy work.

Two ways to reach native code from Dart: a platform channel passes serialized messages asynchronously, while dart:ffi calls C directly and synchronously.

Rule of thumb: reach for a channel when you occasionally call a platform API. Reach for FFI when you wrap an existing C or C++ library, or when you run native code on every frame.

How the native C actually ships

The old friction with FFI was shipping the compiled library. You had to build the .so, .dylib, or .dll per platform and bundle it yourself.

Build hooks fix that. Formerly called native assets, build hooks let a package carry native code that is transparently built, bundled, and made available at runtime (dart.dev). As of Flutter 3.38, the Flutter docs recommend the package_ffi template with build hooks for C interop, and mark the older plugin_ffi approach as legacy (docs.flutter.dev).

The moving parts, which are the actual dependencies of camera_pro:

Build pipeline: a C core compiled by a build hook via native_toolchain_c, bindings generated by ffigen, then called through dart:ffi at runtime.

ffigen writes the Dart bindings from your headers, a build hook using native_toolchain_c compiles the C at build time, code_assets and hooks bundle it, and package:ffi gives you helpers like Arena for native memory.

What FFI looked like in camera_pro

camera_pro is a Flutter camera package built on a shared C and C++ core over Dart FFI. The C core does the pixel work: YUV to RGBA conversion, histogram, focus peaking, zebra, false color, and a waveform monitor, run on each preview frame. The same bit-exact C runs on macOS, iOS, Linux, and Windows behind one hardware abstraction layer.

The classic FFI shape, which is what ffigen produces for you:

import 'dart:ffi';
import 'package:ffi/ffi.dart'; // toDartString, Arena, using

// Names here are illustrative.
final lib = DynamicLibrary.open('libcamera_pro.dylib'); // .so or .dll elsewhere
final coreVersion = lib
    .lookup<NativeFunction<Pointer<Utf8> Function()>>('camera_pro_version')
    .asFunction<Pointer<Utf8> Function()>();

print(coreVersion().toDartString()); // "0.0.1"
Enter fullscreen mode Exit fullscreen mode

That lookup(...).asFunction() pattern is the core of dart:ffi (docs.flutter.dev). With build hooks and code assets you can skip DynamicLibrary.open and mark the binding with the @Native annotation, letting the toolchain resolve the symbol from the bundled asset (dart.dev).

Why bother? Because the calls are frequent and the payloads are large. On an Apple M1 Pro, the core converts a 1080p YUV420p frame to RGBA in about 0.66 ms. Copying full frames through a serialized channel on every frame would be a poor fit for that.

Three things to know before you start

  1. C++ needs extern "C". FFI binds to C symbols, so C++ functions must be exported as C. Also mark them with __attribute__((visibility("default"))) __attribute__((used)) so link-time optimization does not strip them (docs.flutter.dev).

  2. You own native memory. Anything you allocate for the C side, you free. package:ffi makes this safe with an arena that releases everything when the block ends:

   using((arena) {
     final buf = arena<Uint8>(width * height * 4); // freed on exit
     // hand buf to C, read results back
   });
Enter fullscreen mode Exit fullscreen mode
  1. No web. Since dart:ffi is Dart Native only, plan a fallback. camera_pro uses a conditional import to keep dart:ffi off the web build and reimplements the same kernels in pure Dart for the browser.

FAQ

Is Dart FFI faster than platform channels?
For frequent or CPU-heavy calls, usually yes, because it is a direct synchronous call with no per-call serialization. For a rare one-off platform call, the difference does not matter.

Do I have to write the bindings myself?
No. package:ffigen generates them from your C headers.

Do I have to bundle the .so or .dylib myself?
No. Build hooks compile and bundle the native code for you.

Does Dart FFI work on Flutter web?
No. dart:ffi runs only on the Dart Native platform. Ship a pure-Dart or JS-interop fallback for web.

Sources

Top comments (0)