FFI (Foreign Function Interface) is a mechanism of the Dart language, provided by the dart:ffi package, that allows calling functions from native C libraries directly, without intermediate layers in Kotlin, Swift, or Java. The developer loads a dynamic library (.so on Android, .dylib on iOS, .dll on Windows), declares C function signatures, and calls them like regular Dart functions. According to the Dart API Reference (2025), FFI reduces the overhead of cross-language calls to 0.1 µs, which is tens of times faster than using Method Channel.
Key Takeaways
FFI (Foreign Function Interface) is a mechanism that allows a programming language to call functions written in other languages. In the context of Dart and Flutter, FFI means the ability to call functions from C/C++ libraries directly from Dart code, without the need to write platform-specific code in Java (Android) or Swift/Objective-C (iOS).
The dart:ffi package appeared in Dart 2.12 (2021) and has since become a key tool for integrating Flutter with native code. Before dart:ffi, the only way to call a C function from Dart was through Method Channel, an asynchronous mechanism that passed messages via JSON serialization between Dart and the native side. FFI works differently: Dart code directly accesses C library memory, calling functions through the native ABI (Application Binary Interface) without serialization or context switching.
FFI is especially in demand in performance-critical scenarios: image processing (OpenCV), audio (FFmpeg), cryptography (OpenSSL), machine learning (TensorFlow Lite), and databases (SQLite). In all these cases, Method Channel creates unacceptable delays, while FFI provides performance comparable to native C/C++ code. The dart:ffi library also supports memory management: allocation, deallocation, and pointer manipulation.
Method Channel works asynchronously: Dart sends a message to native code, native code processes it and sends the result back. Each call requires serialization of arguments into a Map, passing through a queue, and deserialization. This takes 0.5–5 ms per call. FFI works synchronously and without serialization — a C function call takes 0.01–0.1 µs. A difference of 50–500 times, which is critical for high-frequency operations.
Working with dart:ffi consists of three stages: loading the library, declaring signatures, and calling functions. Each stage uses Dart’s strict typing, minimizing runtime errors.
The first stage loads the dynamic library through the DynamicLibrary class. The library can be loaded by name (libxyz.so, libxyz.dylib, xyz.dll) or by full path. Dart automatically searches for the library in the system’s standard paths. DynamicLibrary provides the lookupFunction method, which binds a Dart function to a C function by symbol name.
In the second stage, a Dart function is declared with type annotations corresponding to the C signature. Special types from dart:ffi are used: Int32, Float, Double, Pointer, NativeFunction, Handle, and others. The lookupFunction annotation takes two generic parameters: the Dart function type (how it will look in Dart) and the native C function type (how it is declared in C).
In the third stage, the generated Dart function is called like a regular function. Arguments are passed directly, the result is returned immediately. If the C function modifies memory through pointers, Dart can read these changes through the Pointer class. Memory management on the C side remains the developer’s responsibility — dart:ffi does not manage memory allocated by malloc in C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// C function declaration: int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)
void main() {
final lib = DynamicLibrary.open('libcalculator.so')
final AddDart add = lib
.lookupFunction<AddNative, AddDart>('add')
print(add(5, 3)) // 8
}
In this example, add is a C function that takes two ints and returns an int. The AddNative typedef describes the C signature using dart:ffi types, while AddDart describes how this function will look in Dart. lookupFunction binds them and returns a Dart function that can be called like a regular one.
dart:ffi provides a set of types corresponding to C types. Each type has a fixed size and conversion rules between Dart and C. Understanding type mapping is critically important for correct FFI operation — an error in type size or sign can cause the application to crash.
| C type | dart:ffi type | Dart type | Size (bytes) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (pointer) |
| void* | Pointer<Void> | Pointer | 8 (pointer) |
| struct | Pointer<T> (Struct) | Pointer | depends on fields |
For working with C strings (char*), dart:ffi uses Pointer<Int8>. Conversion from Dart String to C char* and back is done using toNativeUtf8 (from the ffi package) and fromUtf8. It is important to free C strings after use via calloc.free to avoid memory leaks.
dart:ffi supports declaring C structures as Dart classes that extend Struct. Structure fields are declared with annotations @Int32(), @Float(), @Array(), and others. The size and offset of fields are calculated automatically according to the platform’s ABI. Pointer<Point> can be obtained from a C function that returns a pointer to a structure, or allocated in Dart via calloc.
// C struct: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Calling C function that returns Point*
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)
final Pointer<Point> p = createPoint(10, 20)
print('x: ${p.ref.x}, y: ${p.ref.y}')
calloc.free(p) // free memory
The Point class extends Struct and declares x and y fields with @Int32() annotations. The generated C code will have the exact same memory layout. Pointer.ref provides access to structure fields through getters and setters.
Let’s look at a more complex example — integration with a C library for computing SHA256 hash. This is a typical task where FFI provides significant performance gains compared to Method Channel.
The OpenSSL library provides the SHA256 function, which computes a string hash. Through dart:ffi, we can call it directly, without writing Java or Swift wrappers. This is an example of how FFI allows reusing existing C libraries in Flutter.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Signature: unsigned char* SHA256(
// const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
Pointer<Uint8>, int, Pointer<Uint8>)
String sha256(String input) {
final lib = DynamicLibrary.open('libcrypto.so')
final Sha256Dart sha256Fn = lib
.lookupFunction<Sha256Native, Sha256Dart>('SHA256')
final inputPtr = input.toNativeUtf8()
final outputPtr = calloc(Uint8)(32) // SHA256 = 32 bytes
sha256Fn(inputPtr, input.length, outputPtr)
final digest = outputPtr.asTypedList(32)
final hex = digest.map((b) => b.toRadixString(16)
.padLeft(2, '0')).join()
calloc.free(inputPtr)
calloc.free(outputPtr)
return hex
}
In this example, the sha256 function loads the libcrypto.so library, finds the SHA256 symbol, and calls it with pointers to input and output data. toNativeUtf8 converts a Dart String to a C string (allocates memory), and asTypedList allows reading the result byte array. Memory is freed after use — this is a mandatory step to prevent leaks.
The ffi package provides the calloc function for allocating C-compatible memory. Allocated memory must be freed via calloc.free, otherwise a leak will occur. For automatic memory management, you can use the Arena class from the ffi package, which frees all memory allocated within it when arena.release() is called. This is especially convenient for a large number of temporary allocations.
Despite the power of FFI, it has limitations that must be considered when designing Flutter application architecture. The main limitations are related to type safety, memory management, and platform compatibility.
FFI does not perform runtime type checking. If a C function expects a pointer but receives a number, the application will crash with a segmentation fault. It is recommended to use FFIgen, a tool that generates type-safe Dart wrappers based on C header files (.h). FFIgen analyzes C function declarations and creates Dart code with correct types, eliminating errors at the code-writing stage.
The names and paths of dynamic libraries differ across platforms: libxyz.so on Android/Linux, libxyz.dylib on iOS/macOS, xyz.dll on Windows. For cross-platform libraries, conditional compilation is used through dart:io (Platform.isAndroid, Platform.isIOS) or abstractions like package:ffi. It is recommended to create a factory method that returns the correct library for the current platform.
FFI does not manage C-side memory. If a C function allocates memory via malloc, it must be freed via free, otherwise a leak will occur. Dart has no garbage collector for C memory. Recommendation: always free memory in the same method where it was allocated, or use Arena for group deallocation.
FFI calls execute in the same thread as Dart code. Long synchronous operations (over 10 ms) block the UI thread and cause frame drops. For long operations, you should call the C function in an Isolate or ensure the C function runs work in a background thread and notifies Dart via Port or callback.
// FFI in isolate for long operations
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI call
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Moving FFI calls to an isolate guarantees the UI thread is not blocked. However, note that transferring large amounts of data between isolates requires memory copying. For large buffers (>10 MB), it is preferable to use SharedMemory or memory-mapped files.
Frequently Asked Questions
FFI calls C functions directly, synchronously, and without serialization — latency 0.01–0.1 µs. Method Channel works asynchronously via JSON serialization with a latency of 0.5–5 ms. FFI is suitable for high-performance operations, Method Channel is suitable for simple platform API calls.
Directly — no, dart:ffi only supports C functions. To call C++, you need to create a C wrapper with extern “C” (entry points that are exported as C symbols). C++ classes require an additional layer that translates method calls into C functions.
FFI does not support exceptions — if a C function returns an error code, it must be checked manually. It is recommended to wrap FFI calls in try-catch in Dart and check the return codes of C functions. Critical errors (segfault) cannot be caught.
FFI does not work with libraries that require complex Java (JNI) or Objective-C (Message Dispatch) initialization. For example, UIKit and Android Views are not accessible via FFI. The limitation is because FFI works at the C ABI level, while these APIs require specific runtimes.
Yes, C libraries are compiled separately for each target platform. For Android, .so is built for different ABIs (armeabi-v7a, arm64-v8a, x86_64). For iOS — a universal .dylib (arm64). For Windows — .dll. Flutter automatically packages the correct library version during build.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.