Introduction
Dart FFI (Foreign Function Interface) lets you call C libraries directly from Dart and Flutter. Writing performance-critical code — image processing, cryptography, audio decoding — in Pure Dart is often impractical. FFI lets you leverage the entire C ecosystem instead. This article covers everything from loading a shared library to packaging your FFI code as a reusable Flutter plugin.
1. Calling C Functions with dart:ffi
Start by examining the C library's function signatures:
// native/image_processor.h
int grayscale(uint8_t* pixels, int width, int height);
void free_buffer(uint8_t* buf);
Load and call them from Dart:
import 'dart:ffi';
import 'dart:io';
// Define native and Dart type aliases
typedef GrayscaleNative = Int32 Function(Pointer<Uint8>, Int32, Int32);
typedef GrayscaleDart = int Function(Pointer<Uint8>, int, int);
typedef FreeBufferNative = Void Function(Pointer<Uint8>);
typedef FreeBufferDart = void Function(Pointer<Uint8>);
class ImageProcessorFFI {
late final DynamicLibrary _lib;
late final GrayscaleDart _grayscale;
late final FreeBufferDart _freeBuffer;
ImageProcessorFFI() {
// Load the shared library per platform
if (Platform.isAndroid) {
_lib = DynamicLibrary.open('libimage_processor.so');
} else if (Platform.isIOS || Platform.isMacOS) {
_lib = DynamicLibrary.process(); // iOS uses static linking
} else if (Platform.isWindows) {
_lib = DynamicLibrary.open('image_processor.dll');
} else {
_lib = DynamicLibrary.open('libimage_processor.so');
}
_grayscale = _lib
.lookup<NativeFunction<GrayscaleNative>>('grayscale')
.asFunction();
_freeBuffer = _lib
.lookup<NativeFunction<FreeBufferNative>>('free_buffer')
.asFunction();
}
}
2. Memory Operations with Pointer and Struct
To work with C structs from Dart, extend the Struct class:
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// Dart mirror of a C struct
final class ImageInfo extends Struct {
@Int32()
external int width;
@Int32()
external int height;
@Int32()
external int channels;
external Pointer<Uint8> data;
}
void processImage(Pointer<ImageInfo> imgPtr) {
final info = imgPtr.ref;
print('Width: ${info.width}, Height: ${info.height}');
// Convert native pixel buffer to a Dart TypedData view
final pixelCount = info.width * info.height * info.channels;
final pixels = info.data.asTypedList(pixelCount);
// In-place brightness reduction
for (int i = 0; i < pixels.length; i++) {
pixels[i] = (pixels[i] * 0.8).clamp(0, 255).toInt();
}
}
asTypedList gives you a zero-copy Uint8List view into native memory — mutations affect the C buffer directly.
3. Scoped Memory Management with Arena Allocator
Manual malloc/free is error-prone. The Arena allocator from package:ffi automatically frees everything when the scope exits.
import 'package:ffi/ffi.dart';
Future<void> runWithArena() async {
using((Arena arena) {
// Allocate a native UTF-8 string
final nativeStr = 'Hello FFI'.toNativeUtf8(allocator: arena);
// Allocate a struct
final imageInfo = arena<ImageInfo>();
imageInfo.ref.width = 640;
imageInfo.ref.height = 480;
imageInfo.ref.channels = 3;
// Allocate a pixel buffer
final pixelBuf = arena<Uint8>(640 * 480 * 3);
imageInfo.ref.data = pixelBuf;
// Call C function
processNativeImage(imageInfo, nativeStr);
// All allocations freed automatically when `using` block exits
});
}
Use calloc/malloc directly only when you need allocations to outlive a scope. In that case, always pair with an explicit calloc.free() call in a finally block or a Finalizer.
4. NativeCallable — Passing Dart Callbacks to C
When a C library requires a progress callback, use NativeCallable to pass a Dart function as a C function pointer:
typedef ProgressCallbackNative = Void Function(Int32 current, Int32 total);
typedef EncodeWithProgressNative = Void Function(
Pointer<Uint8> data,
Int32 len,
Pointer<NativeFunction<ProgressCallbackNative>>);
void encodeWithProgress(Uint8List data) {
// Wrap a Dart closure as a C function pointer
final callback = NativeCallable<ProgressCallbackNative>.listener(
(int current, int total) {
final percent = (current / total * 100).toStringAsFixed(1);
print('Encoding: $percent%');
},
);
using((arena) {
final nativeData = arena<Uint8>(data.length);
nativeData.asTypedList(data.length).setAll(0, data);
final encodeWithProgressFn = _lib
.lookup<NativeFunction<EncodeWithProgressNative>>('encode_with_progress')
.asFunction<
void Function(Pointer<Uint8>, int,
Pointer<NativeFunction<ProgressCallbackNative>>)>();
encodeWithProgressFn(nativeData, data.length, callback.nativeFunction);
});
// Always close to release the underlying port
callback.close();
}
NativeCallable.listener dispatches the callback on the Dart isolate, making it thread-safe for use with multithreaded C libraries.
5. Packaging FFI Code as a Flutter Plugin
To share your FFI wrapper across projects, package it as a Flutter plugin with ffiPlugin: true:
# pubspec.yaml (plugin side)
name: image_processor_ffi
description: Flutter plugin wrapping libimage_processor via dart:ffi
flutter:
plugin:
platforms:
android:
ffiPlugin: true
ios:
ffiPlugin: true
macos:
ffiPlugin: true
windows:
ffiPlugin: true
linux:
ffiPlugin: true
With ffiPlugin: true, Flutter's build system automatically handles CMakeLists.txt on Android/Windows/Linux and Podspec on iOS/macOS. Place your C source in src/ and it gets compiled automatically.
image_processor_ffi/
lib/
image_processor_ffi.dart # Public Dart API
src/
image_processor.c # C implementation
image_processor.h
android/
CMakeLists.txt
ios/
Classes/
windows/
CMakeLists.txt
Practical Example: OpenSSL AES Encryption in Flutter
// AES-256-CBC encryption via OpenSSL FFI wrapper
Uint8List aesEncrypt(Uint8List plaintext, Uint8List key, Uint8List iv) {
return using((arena) {
final ptPtr = arena<Uint8>(plaintext.length);
ptPtr.asTypedList(plaintext.length).setAll(0, plaintext);
final keyPtr = arena<Uint8>(32); // 256-bit key
keyPtr.asTypedList(32).setAll(0, key);
final ivPtr = arena<Uint8>(16);
ivPtr.asTypedList(16).setAll(0, iv);
// Extra 16 bytes for PKCS7 padding
final ctPtr = arena<Uint8>(plaintext.length + 16);
final ctLen = arena<Int32>();
_opensslAesEncrypt(ptPtr, plaintext.length, keyPtr, ivPtr, ctPtr, ctLen);
return Uint8List.fromList(ctPtr.asTypedList(ctLen.value));
});
}
Summary
| Feature | Purpose |
|---|---|
DynamicLibrary.open |
Load a shared library at runtime |
Pointer<T> / Struct
|
Manipulate C memory from Dart |
Arena allocator |
Scoped, exception-safe memory management |
NativeCallable |
Pass Dart closures as C function pointers |
ffiPlugin: true |
Package FFI code as a distributable Flutter plugin |
Dart FFI dramatically expands what you can do with Flutter. Performance-sensitive workloads that would be impractical in Pure Dart — image processing, audio codecs, cryptography — become straightforward by leveraging the proven C ecosystem. Start with Arena for memory safety and NativeCallable for callbacks, and you'll avoid most of the pitfalls that make FFI feel intimidating.
This article was originally published by DEV Community and written by kanta13jp1.
Read original article on DEV Community