Dart 57 🎯 Foreign Function Interface (FFI): Calling C/C++ Libraries from Dart (dart:ffi)
Dart code runs in a managed runtime. The garbage collector moves objects, the VM controls memory, and native pointers are abstracted away. Some work does not fit that model: a hardware driver, a codec written in C, an existing library with no Dart equivalent, or a hot loop where the runtime overhead matters. The dart:ffi library is the bridge. It lets Dart code load a native library, look up a function by name, declare the function’s signature, and call it directly, passing and receiving native types. The result is a Dart application that reuses the C ecosystem without giving up the runtime, and a boundary that is fast enough for real work.
Key point: FFI crosses the boundary between managed and native code, and the boundary is where the safety guarantees end. A wrong signature, a freed pointer, or a mismatched struct layout produces undefined behavior, and the runtime cannot detect it. The declarations must match the native code exactly.
Why dart:ffi exists
The existing-library problem. The C and C++ ecosystems contain decades of tested libraries: OpenSSL, SQLite, zlib, libpng, and thousands of others. Rewriting them in Dart is not practical. FFI lets a Dart application call them directly, so the application benefits from the existing code without a rewrite.
The performance problem. Some operations are dominated by tight loops over large buffers, where the Dart runtime’s overhead is measurable. A C function that processes a million elements can be an order of magnitude faster than the equivalent Dart loop, because it avoids the bounds checks and the runtime dispatch. FFI lets the Dart application delegate the hot path to native code.
The platform-API problem. The operating system exposes APIs through C interfaces: libc, libm, the Windows API, and the POSIX functions. Some of these have no Dart equivalent. FFI lets the application call them directly, which makes it possible to access the platform’s full functionality.
The plugin problem. A Flutter plugin that wraps a native SDK, such as a camera, a Bluetooth stack, or a printer, uses FFI to call the SDK’s C API. The plugin’s Dart side is the interface, and the native side is the SDK.
The dart:ffi design. The library is part of the Dart SDK and does not require a package. It provides the DynamicLibrary class for loading libraries, the Pointer type for native memory, the NativeFunction and NativeType annotations for declaring signatures, and the Struct and Union classes for composite types.
The ffigen tool. Writing the FFI bindings by hand is tedious and error-prone. The ffigen package generates the bindings from a C header file, which keeps the declarations in sync with the native code. The tool is the recommended way to bind a large library.
The package:ffi package. The package:ffi package provides utilities that the SDK does not: malloc and calloc for allocating native memory, Utf8 for converting between Dart strings and native strings, and helpers for arrays and structs.
a. Loading a library and looking up a function
The first step is to load the native library and obtain a pointer to the function. The DynamicLibrary class provides the loading, and the lookupFunction method provides the pointer.
The DynamicLibrary.open
The DynamicLibrary.open method loads a library by path.
import 'dart:ffi';
final lib = DynamicLibrary.open('libexample.so');
The path depends on the platform. On Linux, the library is .so. On macOS, it is .dylib. On Windows, it is .dll.
The DynamicLibrary.process
The DynamicLibrary.process method returns the symbols of the running process. It is used on platforms where the symbols are already loaded, such as iOS and the Flutter engine.
final lib = DynamicLibrary.process();
The DynamicLibrary.executable
The DynamicLibrary.executable method returns the symbols of the running executable. It is used when the native code is statically linked into the executable.
final lib = DynamicLibrary.executable();
The lookupFunction
The lookupFunction method returns a Dart function that calls the native function. It takes two type arguments: the native signature and the Dart signature.
typedef NativeAdd = Int32 Function(Int32 a, Int32 b);
typedef DartAdd = int Function(int a, int b);
final add = lib
.lookupFunction<NativeAdd, DartAdd>('add');
final result = add(2, 3); // 5
The native signature uses the FFI types (Int32), and the Dart signature uses the Dart types (int). The lookupFunction method matches them.
The type mapping
Every native type has a corresponding Dart type.
| Native type | Dart type | Size |
|---|---|---|
Int8 | int | 1 byte |
Uint8 | int | 1 byte |
Int16 | int | 2 bytes |
Uint16 | int | 2 bytes |
Int32 | int | 4 bytes |
Uint32 | int | 4 bytes |
Int64 | int | 8 bytes |
Uint64 | int | 8 bytes |
Float | double | 4 bytes |
Double | double | 8 bytes |
Pointer<T> | Pointer<T> | pointer |
Void | void | — |
Bool | bool | 1 byte |
The mapping is not arbitrary. The Int32 native type maps to the Dart int, and the Double native type maps to the Dart double. The wrong mapping produces a runtime error or, worse, undefined behavior.
The isLeaf parameter
The lookupFunction method accepts an isLeaf parameter. When true, the native function does not call back into Dart, and the call avoids setting up the Dart isolate’s state.
final add = lib.lookupFunction<NativeAdd, DartAdd>('add', isLeaf: true);
The parameter is a performance optimization. It is safe only when the native function does not call any Dart callback.
The lookup error
If the symbol is not found, the lookupFunction method throws an ArgumentError.
try {
final fn = lib.lookupFunction<NativeAdd, DartAdd>('missing');
} on ArgumentError catch (e) {
print('Symbol not found: ${e.message}');
}
The error indicates that the symbol is missing from the library, which is usually a name mismatch or a missing export.
b. Memory and pointers
Native code works with raw pointers. Dart’s Pointer<T> type represents a pointer to a native value, and the package:ffi provides the allocation and conversion utilities.
The Pointer type
A Pointer<T> is a typed pointer. The type parameter is the native type it points to.
Pointer<Int32> p = malloc<Int32>();
p.value = 42;
print(p.value); // 42
malloc.free(p);
The value getter and setter read and write the pointed-to value. The malloc and free functions allocate and release the memory.
The allocation
The package:ffi provides malloc, calloc, and malloc.allocate.
import 'package:ffi/ffi.dart';
final p = malloc<Int32>(); // Uninitialized
final q = calloc<Int32>(); // Zero-initialized
final arr = malloc<Int32>(10); // Array of 10
The calloc function zeroes the memory, while malloc does not. The array form takes a count.
The free
The malloc.free function releases the memory.
malloc.free(p);
The memory must be freed exactly once. A double free or a use after free is undefined behavior.
The elementAt
The elementAt method returns a pointer to the element at an index.
final arr = malloc<Int32>(10);
final third = arr.elementAt(3);
third.value = 42;
The method is used to access the elements of an array pointer.
The asTypedList
The asTypedList method creates a typed data list view over the native memory.
final arr = malloc<Int32>(10);
final list = arr.asTypedList(10);
list[0] = 42;
The list shares the native memory, so writing through the list modifies the native buffer.
The Pointer.fromAddress
The Pointer.fromAddress constructor creates a pointer from a raw address.
final p = Pointer<Int32>.fromAddress(0x12345678);
The constructor is used when a native function returns a raw address as an integer.
The address getter
The address getter returns the raw address as an integer.
final addr = p.address;
The getter is used when a native function expects the address as an integer.
The nullptr
The nullptr constant is a pointer with address zero.
Pointer<Int32>? p = nullptr;
The constant is used to represent a null pointer.
The string conversion
The package:ffi provides the Utf8 class for converting between Dart strings and native UTF-8 strings.
final nativeStr = 'Hello'.toNativeUtf8();
try {
final dartStr = nativeStr.toDartString();
print(dartStr); // Hello
} finally {
malloc.free(nativeStr);
}
The toNativeUtf8 method allocates a native string, and the toDartString method reads it back. The native string must be freed.
The struct
A native struct is declared with the Struct class and the @Array annotation for array fields.
import 'dart:ffi';
final class Point extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
final p = calloc<Point>();
p.ref.x = 10;
p.ref.y = 20;
print('${p.ref.x}, ${p.ref.y}'); // 10, 20
calloc.free(p);
The Struct class is extended, and the fields are declared with the FFI type annotations. The ref property returns the struct’s fields.
The struct layout
The struct’s layout is determined by the field types and their order. The @Array annotation declares a fixed-size array field.
final class Buffer extends Struct {
@Uint8()
external int length;
@Array(256)
external Array<Uint8> data;
}
The struct is 257 bytes: one for the length and 256 for the array.
The packed struct
The @Packed annotation controls the struct’s alignment.
@Packed(1)
final class PackedStruct extends Struct {
@Uint8()
external int a;
@Uint32()
external int b;
}
The annotation is used when the native code declares a packed struct.
c. Callbacks and patterns
FFI supports two directions: Dart calling native, and native calling Dart. The second is the callback, and it is more complex.
The native callback
A Dart function can be passed to a native function as a callback. The Pointer.fromFunction method creates a native function pointer from a Dart function.
typedef NativeCallback = Void Function(Int32);
typedef DartCallback = void Function(int);
void onEvent(int value) {
print('Event: $value');
}
final callback = Pointer.fromFunction<NativeCallback>(onEvent, nullptr);
The fromFunction method requires the Dart function’s signature to match the native signature. The second argument is the value to return if the Dart function throws.
The callback restriction
A Pointer.fromFunction callback can only be created from a static or top-level function. A closure that captures variables cannot be converted, because the native code cannot access the Dart isolate’s state.
The DynamicLibrary.lookup
The DynamicLibrary.lookup method returns a pointer to a symbol without creating a Dart function.
final symbol = lib.lookup<NativeFunction<NativeAdd>>('add');
The method is used when the pointer is needed for another native function, not for a direct call.
The function pointer as an argument
A native function can accept a function pointer as an argument.
typedef NativeSetCallback = Void Function(Pointer<NativeFunction<NativeCallback>>);
typedef DartSetCallback = void Function(Pointer<NativeFunction<NativeCallback>>);
final setCallback = lib.lookupFunction<NativeSetCallback, DartSetCallback>('set_callback');
setCallback(callback);
The callback is passed to the native function, which stores it and calls it later.
The string pattern
A common pattern is a native function that returns a C string. The Dart side reads the string and frees it.
typedef NativeGetName = Pointer<Utf8> Function();
typedef DartGetName = Pointer<Utf8> Function();
final getName = lib.lookupFunction<NativeGetName, DartGetName>('get_name');
final namePtr = getName();
try {
print(namePtr.toDartString());
} finally {
malloc.free(namePtr);
}
The Utf8 type is the native string type, and the toDartString method converts it.
The buffer pattern
A native function that fills a buffer is called with a pointer and a length.
typedef NativeRead = Int32 Function(Pointer<Uint8>, Int32);
typedef DartRead = int Function(Pointer<Uint8>, int);
final read = lib.lookupFunction<NativeRead, DartRead>('read');
final buffer = malloc<Uint8>(1024);
try {
final bytesRead = read(buffer, 1024);
final data = buffer.asTypedList(bytesRead);
print(data);
} finally {
malloc.free(buffer);
}
The buffer is allocated on the native heap, passed to the function, and read back as a typed list.
The struct pattern
A native function that returns a struct by value is bound with the struct type.
typedef NativeGetPoint = Point Function();
typedef DartGetPoint = Point Function();
final getPoint = lib.lookupFunction<NativeGetPoint, DartGetPoint>('get_point');
final point = getPoint();
print('${point.x}, ${point.y}');
The struct is returned by value, and the point variable holds the fields directly.
The error pattern
A native function that returns an error code is checked on the Dart side.
typedef NativeOpen = Int32 Function(Pointer<Utf8>);
typedef DartOpen = int Function(Pointer<Utf8>);
final open = lib.lookupFunction<NativeOpen, DartOpen>('open');
final path = 'file.txt'.toNativeUtf8();
try {
final result = open(path);
if (result != 0) {
throw Exception('Failed to open file: $result');
}
} finally {
malloc.free(path);
}
The error code is checked, and the path is freed.
The ffigen tool
The ffigen package generates the bindings from a C header. The configuration file specifies the header and the output.
# ffigen.yaml
name: MyBindings
description: Bindings for mylib
output: 'lib/mylib_bindings.dart'
headers:
entry-points:
- 'src/mylib.h'
The tool reads the header, generates the Dart classes, and writes the output file. The generated code is the FFI binding, and the developer does not write it by hand.
The build integration
The native library must be compiled and bundled with the application. For a Flutter plugin, the native code is in the plugin’s src directory, and the build system compiles it for each platform.
# pubspec.yaml
flutter:
plugin:
platforms:
android:
ffiPlugin: true
ios:
ffiPlugin: true
The ffiPlugin flag tells the build system that the plugin uses FFI, and the native code is compiled accordingly.
Complete Example Session
// ============================================
// PART 1: THE NATIVE LIBRARY
// ============================================
// example.c
// #include <stdint.h>
//
// int32_t add(int32_t a, int32_t b) {
// return a + b;
// }
//
// const char* greet(const char* name) {
// static char buffer[256];
// snprintf(buffer, sizeof(buffer), "Hello, %s!", name);
// return buffer;
// }
// Compile: gcc -shared -fPIC -o libexample.so example.c
// ============================================
// PART 2: LOADING THE LIBRARY
// ============================================
import 'dart:ffi';
import 'package:ffi/ffi.dart';
final lib = DynamicLibrary.open('libexample.so');
// ============================================
// PART 3: CALLING A SIMPLE FUNCTION
// ============================================
typedef NativeAdd = Int32 Function(Int32 a, Int32 b);
typedef DartAdd = int Function(int a, int b);
final add = lib.lookupFunction<NativeAdd, DartAdd>('add');
void main() {
print(add(2, 3)); // 5
}
// ============================================
// PART 4: CALLING WITH A STRING
// ============================================
typedef NativeGreet = Pointer<Utf8> Function(Pointer<Utf8>);
typedef DartGreet = Pointer<Utf8> Function(Pointer<Utf8>);
final greet = lib.lookupFunction<NativeGreet, DartGreet>('greet');
void main() {
final name = 'World'.toNativeUtf8();
try {
final result = greet(name);
print(result.toDartString()); // Hello, World!
} finally {
malloc.free(name);
}
}
// ============================================
// PART 5: ALLOCATING NATIVE MEMORY
// ============================================
void main() {
final p = malloc<Int32>();
p.value = 42;
print(p.value); // 42
malloc.free(p);
}
// ============================================
// PART 6: THE STRUCT
// ============================================
final class Point extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
void main() {
final p = calloc<Point>();
p.ref.x = 10;
p.ref.y = 20;
print('${p.ref.x}, ${p.ref.y}'); // 10, 20
calloc.free(p);
}
// ============================================
// PART 7: THE ARRAY
// ============================================
void main() {
final arr = malloc<Int32>(10);
final list = arr.asTypedList(10);
list[0] = 42;
print(list[0]); // 42
malloc.free(arr);
}
// ============================================
// PART 8: THE CALLBACK
// ============================================
typedef NativeCallback = Void Function(Int32);
typedef DartCallback = void Function(int);
void onEvent(int value) {
print('Event: $value');
}
void main() {
final callback = Pointer.fromFunction<NativeCallback>(onEvent, nullptr);
// Pass the callback to a native function.
}
// ============================================
// PART 9: THE BUFFER PATTERN
// ============================================
typedef NativeRead = Int32 Function(Pointer<Uint8>, Int32);
typedef DartRead = int Function(Pointer<Uint8>, int);
final read = lib.lookupFunction<NativeRead, DartRead>('read');
void main() {
final buffer = malloc<Uint8>(1024);
try {
final bytesRead = read(buffer, 1024);
final data = buffer.asTypedList(bytesRead);
print(data);
} finally {
malloc.free(buffer);
}
}
// ============================================
// PART 10: THE COMPLETE PICTURE
// ============================================
// 1. Load the library with DynamicLibrary.open.
// 2. Look up the function with lookupFunction.
// 3. Declare the native and Dart signatures.
// 4. Call the function with Dart values.
// 5. Allocate native memory with malloc or calloc.
// 6. Convert strings with toNativeUtf8 and toDartString.
// 7. Declare structs with the Struct class.
// 8. Free the memory when it is no longer needed.
// 9. Use ffigen to generate the bindings for large libraries.
// 10. Keep the native and Dart signatures in sync.
The ten parts showed the native library, the loading, the simple function call, the string call, the native memory allocation, the struct, the array, the callback, the buffer pattern, and the complete picture.
Quick Reference
The Library Loading
| Method | Use Case |
|---|---|
DynamicLibrary.open(path) | Load by path |
DynamicLibrary.process() | The running process |
DynamicLibrary.executable() | The running executable |
The Type Mapping
| Native type | Dart type | Size |
|---|---|---|
Int8 / Uint8 | int | 1 |
Int16 / Uint16 | int | 2 |
Int32 / Uint32 | int | 4 |
Int64 / Uint64 | int | 8 |
Float | double | 4 |
Double | double | 8 |
Pointer<T> | Pointer<T> | pointer |
Void | void | — |
The Memory Functions
| Function | Purpose |
|---|---|
malloc<T>() | Allocate uninitialized |
calloc<T>() | Allocate zero-initialized |
malloc<T>(n) | Allocate an array |
malloc.free(p) | Free the memory |
p.value | Read or write the value |
p.ref | Access struct fields |
p.elementAt(i) | Pointer to element i |
p.asTypedList(n) | A typed list view |
p.address | The raw address |
The String Conversion
| Method | Direction |
|---|---|
'text'.toNativeUtf8() | Dart → native |
ptr.toDartString() | Native → Dart |
The Struct Declaration
| Annotation | Field type |
|---|---|
@Int32() | int |
@Uint8() | int |
@Double() | double |
@Array(n) | Array<T> |
@Packed(n) | Struct alignment |
The Callback
| Method | Purpose |
|---|---|
Pointer.fromFunction<Native>(dartFn, fallback) | Create a callback |
| Top-level or static function | Required |
| No closures | The native side cannot access captured state |
Best Practices
✅ Do This:
// Match the native and Dart signatures exactly.
typedef NativeAdd = Int32 Function(Int32, Int32);
typedef DartAdd = int Function(int, int); // ✅
// Use isLeaf for functions that do not call back.
lib.lookupFunction<Native, Dart>('fn', isLeaf: true); // ✅
// Free the memory when done.
malloc.free(ptr); // ✅
// Use calloc for zero-initialized memory.
final p = calloc<Int32>(); // ✅
// Convert strings with Utf8.
final s = 'text'.toNativeUtf8(); // ✅
// Use asTypedList for buffer access.
final list = ptr.asTypedList(length); // ✅
// Use ffigen for large libraries.
// Generate the bindings from the header. // ✅
// Keep the native library and the bindings in sync.
// Regenerate when the header changes. // ✅
❌ Don’t Do This:
// Don't mismatch the native and Dart types.
typedef Native = Int64 Function(Int64);
typedef Dart = int Function(int); // ⚠️ Must match
// Don't forget to free the memory.
malloc<Int32>(); // ❌ Memory leak
// Don't use a freed pointer.
malloc.free(ptr);
ptr.value = 42; // ❌ Use after free
// Don't pass a closure to fromFunction.
Pointer.fromFunction<Native>((x) => print(x)); // ❌ Must be top-level
// Don't ignore the isLeaf restriction.
// A leaf function that calls back into Dart crashes. // ❌
// Don't assume the struct layout.
// Use @Packed for packed structs. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Symbol not found | Wrong name or missing export | Check the library |
| Wrong value read | Type mismatch | Match the types |
| Memory leak | Not freed | Call malloc.free |
| Use after free | Freed then used | Free at the end |
| Crash on callback | Closure used | Top-level function |
| Struct misaligned | No @Packed | Add the annotation |
| String corrupted | Wrong encoding | Use Utf8 |
| Crash on return | Wrong signature | Match the native ABI |
Real-World Examples
1. Load a Library
final lib = DynamicLibrary.open('libexample.so');
2. Look Up a Function
final add = lib.lookupFunction<NativeAdd, DartAdd>('add');
3. Call with Integers
print(add(2, 3));
4. Call with Strings
final name = 'World'.toNativeUtf8();
final result = greet(name).toDartString();
5. Allocate Memory
final p = malloc<Int32>();
p.value = 42;
6. Free Memory
malloc.free(p);
7. Declare a Struct
final class Point extends Struct {
@Int32() external int x;
@Int32() external int y;
}
8. Access an Array
final list = ptr.asTypedList(10);
9. Create a Callback
Pointer.fromFunction<NativeCallback>(onEvent, nullptr);
10. Generate Bindings
# ffigen.yaml
headers:
entry-points:
- 'src/mylib.h'
Visual
The FFI Boundary
┌────────────────────────────────────────────────────────────┐
│ THE FFI BOUNDARY │
│ │
│ DART SIDE NATIVE SIDE │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Dart runtime │ │ C runtime │ │
│ │ Garbage collector │ │ Manual memory │ │
│ │ Managed objects │ │ Raw pointers │ │
│ │ │ │ │ │
│ │ int, double, String │ │ int32_t, double │ │
│ │ List<int> │ │ char* │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
│ │ │ │
│ │ THE FFI BOUNDARY │ │
│ │◀─────────────────────────────▶│ │
│ │ │ │
│ ┌──────────┴───────────┐ ┌──────────┴───────────┐ │
│ │ lookupFunction │ │ Exported symbols │ │
│ │ Pointer<T> │ │ Function pointers │ │
│ │ malloc / free │ │ malloc / free │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ The boundary is where the safety guarantees end. │
│ The signatures must match exactly. │
└────────────────────────────────────────────────────────────┘
The Function Call
┌────────────────────────────────────────────────────────────┐
│ THE FUNCTION CALL │
│ │
│ DART │
│ ┌──────────────────────────────────────────────────┐ │
│ │ final add = lib.lookupFunction<NativeAdd, │ │
│ │ DartAdd>('add'); │ │
│ │ │ │
│ │ final result = add(2, 3); │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ Marshalled arguments │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ NATIVE │ │
│ │ int32_t add(int32_t a, int32_t b) { │ │
│ │ return a + b; │ │
│ │ } │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ │ Return value │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DART │ │
│ │ result = 5 │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Memory
┌────────────────────────────────────────────────────────────┐
│ NATIVE MEMORY │
│ │
│ malloc<Int32>() │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Heap allocation: 4 bytes │ │
│ │ Uninitialized. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ calloc<Int32>() │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Heap allocation: 4 bytes │ │
│ │ Zero-initialized. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ p.value = 42 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Heap allocation: 4 bytes │ │
│ │ Value: 42 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ malloc.free(p) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Heap allocation released. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Struct Layout
┌────────────────────────────────────────────────────────────┐
│ STRUCT LAYOUT │
│ │
│ final class Point extends Struct { │
│ @Int32() external int x; │
│ @Int32() external int y; │
│ } │
│ │
│ MEMORY: │
│ ┌───────────────┬───────────────┐ │
│ │ x (4 bytes) │ y (4 bytes) │ │
│ └───────────────┴───────────────┘ │
│ offset 0 offset 4 │
│ │
│ total size: 8 bytes │
│ │
│ p.ref.x = 10 │
│ p.ref.y = 20 │
│ ┌───────────────┬───────────────┐ │
│ │ 10 │ 20 │ │
│ └───────────────┴───────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
dart:ffi | The SDK library for native interop |
DynamicLibrary | Loads a native library |
lookupFunction | Returns a Dart function for a native symbol |
Pointer<T> | A typed pointer to native memory |
Int32, Double, etc. | The FFI native types |
malloc / calloc | Native memory allocation |
Utf8 | Native string conversion |
Struct | The base class for native structs |
Pointer.fromFunction | Creates a native callback |
ffigen | Generates the bindings from a C header |
Key takeaways:
dart:ffiis the bridge between managed and native code. It loads a native library, looks up a function by name, and calls it with Dart values. The boundary is where the runtime’s safety guarantees end, so the declarations must match the native code exactly.- Every native type has a Dart counterpart. The
Int32maps toint, theDoublemaps todouble, and thePointer<T>maps toPointer<T>. A mismatch produces undefined behavior. - The
isLeafparameter avoids the callback setup. When the native function does not call back into Dart,isLeaf: trueskips the isolate’s state setup, which is a performance win. - Native memory is allocated and freed manually. The
mallocandcallocfunctions allocate, and themalloc.freefunction releases. The garbage collector does not manage the native heap, so a missed free is a leak and a double free is undefined behavior. - The
Utf8class converts between Dart and native strings. ThetoNativeUtf8method allocates a native string, and thetoDartStringmethod reads it back. The native string must be freed. - The
Structclass declares a native struct. The fields are annotated with the FFI type, and therefproperty accesses them. The@Packedannotation controls the alignment. - The
Pointer.fromFunctioncreates a callback. The Dart function must be top-level or static, and it cannot capture variables. The native code calls the function through the pointer. - The
ffigentool generates the bindings. Writing the FFI declarations by hand is tedious. The tool reads a C header and generates the Dart classes, which keeps the declarations in sync with the native code. - The native library must be compiled and bundled. For a Flutter plugin, the native code is compiled for each platform, and the
ffiPlugin: trueflag tells the build system to include it.
Remember: The dart:ffi library lets a Dart application call native C code directly. The DynamicLibrary loads the library, the lookupFunction returns a Dart function for the native symbol, and the FFI types map the native and Dart types. The Pointer<T> type represents native memory, the malloc and calloc functions allocate it, and the malloc.free function releases it. The Utf8 class converts strings, the Struct class declares structs, and the Pointer.fromFunction creates callbacks. The ffigen tool generates the bindings from a C header. The boundary is where the safety guarantees end, so the declarations must be exact. The result is a Dart application that reuses the C ecosystem without giving up the runtime.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!