| |

Dart 57 🎯 Foreign Function Interface (FFI): Calling C/C++ Libraries from Dart (dart:ffi)

⏱️ Estimated read: 19 min | |

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 typeDart typeSize
Int8int1 byte
Uint8int1 byte
Int16int2 bytes
Uint16int2 bytes
Int32int4 bytes
Uint32int4 bytes
Int64int8 bytes
Uint64int8 bytes
Floatdouble4 bytes
Doubledouble8 bytes
Pointer<T>Pointer<T>pointer
Voidvoid—
Boolbool1 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

MethodUse Case
DynamicLibrary.open(path)Load by path
DynamicLibrary.process()The running process
DynamicLibrary.executable()The running executable

The Type Mapping

Native typeDart typeSize
Int8 / Uint8int1
Int16 / Uint16int2
Int32 / Uint32int4
Int64 / Uint64int8
Floatdouble4
Doubledouble8
Pointer<T>Pointer<T>pointer
Voidvoid—

The Memory Functions

FunctionPurpose
malloc<T>()Allocate uninitialized
calloc<T>()Allocate zero-initialized
malloc<T>(n)Allocate an array
malloc.free(p)Free the memory
p.valueRead or write the value
p.refAccess struct fields
p.elementAt(i)Pointer to element i
p.asTypedList(n)A typed list view
p.addressThe raw address

The String Conversion

MethodDirection
'text'.toNativeUtf8()Dart → native
ptr.toDartString()Native → Dart

The Struct Declaration

AnnotationField type
@Int32()int
@Uint8()int
@Double()double
@Array(n)Array<T>
@Packed(n)Struct alignment

The Callback

MethodPurpose
Pointer.fromFunction<Native>(dartFn, fallback)Create a callback
Top-level or static functionRequired
No closuresThe 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

PitfallWhy It HappensFix
Symbol not foundWrong name or missing exportCheck the library
Wrong value readType mismatchMatch the types
Memory leakNot freedCall malloc.free
Use after freeFreed then usedFree at the end
Crash on callbackClosure usedTop-level function
Struct misalignedNo @PackedAdd the annotation
String corruptedWrong encodingUse Utf8
Crash on returnWrong signatureMatch 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

ItemValue
dart:ffiThe SDK library for native interop
DynamicLibraryLoads a native library
lookupFunctionReturns a Dart function for a native symbol
Pointer<T>A typed pointer to native memory
Int32, Double, etc.The FFI native types
malloc / callocNative memory allocation
Utf8Native string conversion
StructThe base class for native structs
Pointer.fromFunctionCreates a native callback
ffigenGenerates the bindings from a C header

Key takeaways:

  • dart:ffi is 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 Int32 maps to int, the Double maps to double, and the Pointer<T> maps to Pointer<T>. A mismatch produces undefined behavior.
  • The isLeaf parameter avoids the callback setup. When the native function does not call back into Dart, isLeaf: true skips the isolate’s state setup, which is a performance win.
  • Native memory is allocated and freed manually. The malloc and calloc functions allocate, and the malloc.free function 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 Utf8 class converts between Dart and native strings. The toNativeUtf8 method allocates a native string, and the toDartString method reads it back. The native string must be freed.
  • The Struct class declares a native struct. The fields are annotated with the FFI type, and the ref property accesses them. The @Packed annotation controls the alignment.
  • The Pointer.fromFunction creates 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 ffigen tool 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: true flag 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.


Was this helpful?


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!