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;
}
2. Compile and Package
- Android (NDK):
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_ABI=arm64-v8a
- iOS/macOS:
clang -dynamiclib native_add.c -o libnative_add.dylib
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
Run:
dart run ffigen
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
}
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;
}
}
Step 2: Build as Shared Library
- Android (NDK / CMake):
clang++ -shared -fPIC native_math.cpp -o libnative_math.so
- iOS/macOS:
clang++ -dynamiclib native_math.cpp -o libnative_math.dylib
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);
dart run ffigen
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
}
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);
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.

Top comments (0)