DEV Community

Cover image for Maximizing Performance in Flutter with Dart FFI
مصعب يوسف
مصعب يوسف

Posted on Originally published at Medium on

Maximizing Performance in Flutter with Dart FFI

Maximizing Performance in Flutter with Dart FFI

Introduction

Peace be upon you. In some attempts to learn, I was watching a clip from the Flutter Herros channel about Maximize Speed ​​with Dart FFI: Beginner’s Guide to High-Performance Integration. I tried hard to understand and simplify this matter. Let’s begin…

Flutter is known for its expressive UI and fast development cycle, but when performance-critical tasks enter the equation — like image processing, cryptography, or running machine learning models — pure Dart may not always be enough. That’s where Dart FFI (Foreign Function Interface) comes in.

Dart FFI allows Flutter apps to call native code written in C, C++, or Rust directly, bypassing the overhead of platform channels. This opens the door to:

  • Performance-critical code that requires execution close to the metal.
  • Reusing existing native libraries (C, C++, Rust) instead of rewriting them in Dart.
  • Accessing system-level APIs not exposed by the Flutter or Dart standard libraries.

In short, FFI bridges the gap between Dart and the native world, giving Flutter apps the flexibility and speed they need.

How Dart FFI Works Under the Hood

At its core, Dart FFI works by dynamically loading a shared library (.so on Android, .dylib on macOS/iOS, .dll on Windows). This process relies on system-level functions:

  • dlopen: Opens a shared library at runtime.
  • dlsym: Looks up a function’s address by its symbol name.

Once a symbol is resolved, Dart can call it as if it were a regular Dart function.

FFI vs. MethodChannel

  • MethodChannel: Involves JSON serialization, message passing, and hopping between Dart and platform threads. Great for flexibility but introduces latency.
  • FFI: Calls the native function directly in process. The overhead is typically ~100 ns per call — orders of magnitude faster than MethodChannel.

ABI Stability & Calling Conventions

When dealing with FFI, it’s critical to respect the Application Binary Interface (ABI) — the low-level rules that dictate how functions, parameters, and memory are laid out across platforms. Dart FFI aligns with the platform’s C ABI. Any mismatch (e.g., struct packing differences between compilers) can cause crashes.

Step-by-Step Implementation Guide

Let’s walk through a simple example:

1. Write a Native C Function

// native_add.c
#include <stdint.h>

int32_t add(int32_t a, int32_t b) {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

2. Compile and Package

  • Android (NDK):
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_ABI=arm64-v8a
Enter fullscreen mode Exit fullscreen mode
  • iOS/macOS:
clang -dynamiclib native_add.c -o libnative_add.dylib
Enter fullscreen mode Exit fullscreen mode

You’ll end up with libnative_add.so (Android) or libnative_add.dylib (iOS/macOS).

3. Generate Bindings with ffigen

Add ffigen to pubspec.yaml:

dev_dependencies:
  ffigen: ^11.0.0
Enter fullscreen mode Exit fullscreen mode

Run:

dart run ffigen
Enter fullscreen mode Exit fullscreen mode

This generates Dart bindings (e.g., native_add_bindings.dart) from your C header.

4. Call from Dart

import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'native_add_bindings.dart';

final dylib = DynamicLibrary.open("libnative_add.so");
final bindings = NativeAddBindings(dylib);

void main() {
  final result = bindings.add(40, 2);
  print("Result from native code: $result"); // 42
}
Enter fullscreen mode Exit fullscreen mode

Example with C++

Sometimes, you’ll want to integrate C++ code (for example, performance-heavy algorithms or existing C++ libraries). In that case, you must ensure functions are exported in C ABI format using extern “C”. Otherwise, Dart won’t be able to find the symbols due to name mangling.

Step 1: Write a C++ function

// native_math.cpp
#include <cstdint>


// Without extern "C", Dart cannot link properly
extern "C" {
    int32_t multiply(int32_t a, int32_t b) {
        return a * b;
    }
    double average(double a, double b) {
        return (a + b) / 2.0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Build as Shared Library

  • Android (NDK / CMake):
clang++ -shared -fPIC native_math.cpp -o libnative_math.so
Enter fullscreen mode Exit fullscreen mode
  • iOS/macOS:
clang++ -dynamiclib native_math.cpp -o libnative_math.dylib
Enter fullscreen mode Exit fullscreen mode

Step 3: Generate Bindings

Use ffigen again to generate Dart bindings from a native_math.h header file:

// native_math.h
#include <stdint.h>

int32_t multiply(int32_t a, int32_t b);
double average(double a, double b);
Enter fullscreen mode Exit fullscreen mode
dart run ffigen
Enter fullscreen mode Exit fullscreen mode

Step 4: Call from Dart

import 'dart:ffi';
import 'native_math_bindings.dart';

final dylib = DynamicLibrary.open("libnative_math.so");
final mathLib = NativeMathBindings(dylib);
void main() {
  print("3 * 4 = ${mathLib.multiply(3, 4)}"); // 12
  print("Average of 10 and 20 = ${mathLib.average(10, 20)}"); // 15.0
}
Enter fullscreen mode Exit fullscreen mode

Memory Management & Data Passing

Pointers and Structs

FFI exposes raw pointers via Pointer. For example:

final ptr = calloc<Int32>();
ptr.value = 123;
// Use ptr in FFI call
calloc.free(ptr);
Enter fullscreen mode Exit fullscreen mode

Zero-Copy Transfers

For large arrays (e.g., images, audio buffers), copying between Dart and native memory can kill performance. Instead, use Uint8List backed by a native buffer to achieve zero-copy interop.

Finalizers

Dart’s Finalizer API can clean up native memory automatically when Dart objects are garbage collected — helping avoid leaks.

Performance & Optimization Tips

  • Know when FFI is worth it: If the code runs fast enough in Dart, stay in Dart. FFI is best for CPU-intensive work.
  • Batch calls: Avoid making thousands of tiny FFI calls. Instead, process data in chunks.
  • Measure performance: Use Flutter DevTools and FrameTiming to monitor frame jank and CPU usage.

Real-World Use Cases

  • Image/Video Processing: Libraries like libjpeg-turbo or FFmpeg integrated via FFI.
  • Cryptography: Reusing hardened C libraries instead of implementing in Dart.
  • Machine Learning: Running Rust models with flutter_rust_bridge.
  • Compression: Integrating zlib or other native compressors for better performance.

Challenges

  • ABI Mismatch: Different compilers may pack structs differently — always check alignment.
  • Missing extern “C”: C++ code must expose functions with extern “C” to avoid symbol mangling.
  • Cross-Platform Builds: Android requires building for multiple ABIs (armeabi-v7a, arm64-v8a, x86_64). iOS requires handling simulators vs. devices.
  • Debugging Crashes: Native crashes don’t produce Dart stack traces — use Xcode, Android Studio, or LLDB for debugging.

Conclusion

Dart FFI is a powerful tool that lets Flutter developers:

  • Unlock near-native performance.
  • Reuse decades of existing C/C++/Rust libraries.
  • Access low-level system APIs not available in Dart.

Use FFI wisely: stick to Dart for everyday code, but reach for FFI when performance or interoperability demands it.

Recommended Resources

Top comments (0)