ZetCode

Dart FFI tutorial

last modified May 30, 2026

In this tutorial we show how to use the Dart Foreign Function Interface (FFI) to call native C code from Dart programs.

Introduction

The Foreign Function Interface (FFI) lets a Dart program call functions written in C and access C data structures directly. The dart:ffi library, which ships with the Dart SDK, provides the types and functions needed to describe C signatures and load native libraries at runtime.

Dart already runs on a managed runtime, so FFI bridges the gap to the native world without requiring a separate plugin layer. You describe the C types in Dart, load the shared library, and call functions as if they were normal Dart functions.

Use FFI when you need to call an existing C library, access low-level OS APIs, or run performance-critical code that is already written in C. FFI does not require Flutter or platform channels; it works in pure Dart on desktop, server, Android, and iOS.

Platform channels are a Flutter concept and require the host application (iOS, Android) to act as a bridge. FFI bypasses that entirely. For a standalone Dart program or a server application, FFI is the natural choice for native interop.

Dart FFI is supported on Linux, macOS, Windows, Android, and iOS. The same Dart code compiles and runs on all platforms; only the path to the shared library differs per platform.

Setting Up FFI

The core FFI types live in dart:ffi, which is part of the standard SDK. Higher-level utilities — memory allocators and string helpers — are in the ffi package on pub.dev.

pubspec.yaml
name: ffi_demo
environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  ffi: ^2.1.0

After adding the dependency, run dart pub get.

Import both libraries at the top of every file that uses FFI.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

Load a shared library with DynamicLibrary.open. The filename extension differs per platform.

main.dart
import 'dart:ffi';
import 'dart:io';

DynamicLibrary openLib(String name) {
  if (Platform.isLinux || Platform.isAndroid) {
    return DynamicLibrary.open('lib$name.so');
  }
  if (Platform.isMacOS || Platform.isIOS) {
    return DynamicLibrary.open('lib$name.dylib');
  }
  if (Platform.isWindows) {
    return DynamicLibrary.open('$name.dll');
  }
  throw UnsupportedError('unsupported platform');
}

void main() {
  final lib = openLib('mylib');
  // use lib...
}

DynamicLibrary.process searches the symbols already loaded by the process. Use it to reach functions from the C runtime or from a statically linked library.

main.dart
import 'dart:ffi';

void main() {
  // look up a symbol already loaded into the current process
  final lib = DynamicLibrary.process();
  // or: DynamicLibrary.executable() for the Dart VM executable itself
}

Basic Types

Every C type has a corresponding Dart FFI type. These native types are used only in typedef declarations — actual Dart variables always use Dart's native int, double, or bool.

C integer types map to Int8, Int16, Int32, Int64, and unsigned variants Uint8 through Uint64. C float maps to Float, and double maps to Double. C void maps to Void. A C pointer int* maps to Pointer<Int32>.

The sizeOf<T> function returns the number of bytes occupied by a native type — the same value that sizeof would return in C.

main.dart
import 'dart:ffi';

void main() {
  print(sizeOf<Int8>());    // 1
  print(sizeOf<Int16>());   // 2
  print(sizeOf<Int32>());   // 4
  print(sizeOf<Int64>());   // 8
  print(sizeOf<Float>());   // 4
  print(sizeOf<Double>());  // 8
  print(sizeOf<Pointer>()); // 8 on 64-bit platforms
}
$ dart main.dart
1
2
4
8
4
8
8

Calling Simple C Functions

To call a C function, you declare two Dart typedef aliases: one using native FFI types (for the C side) and one using Dart types (for the Dart side). Then you call lookupFunction on the loaded library to bind the symbol.

Start with a minimal C library. Save the file as add.c and compile it.

add.c
#include <stdint.h>

int32_t add(int32_t a, int32_t b) {
    return a + b;
}
$ gcc -shared -fPIC -o libadd.so add.c
main.dart
import 'dart:ffi';

// C signature: int32_t add(int32_t, int32_t)
typedef AddNative = Int32 Function(Int32, Int32);

// Dart signature: int add(int, int)
typedef AddDart = int Function(int, int);

void main() {
  final lib = DynamicLibrary.open('libadd.so');

  final add = lib.lookupFunction<AddNative, AddDart>('add');

  print(add(3, 7));   // 10
  print(add(100, -1)); // 99
}
$ dart main.dart
10
99

lookupFunction takes two type arguments: the native C signature first, the Dart signature second. The method looks up the symbol by name, verifies that the signatures are compatible, and returns a callable Dart function.

Working With Pointers

A Pointer<T> holds the memory address of a value of type T. The ffi package provides two allocators: calloc (zeroes memory) and malloc (leaves memory uninitialised). Always free allocations when they are no longer needed.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void main() {
  // Allocate a single Int32
  final ptr = calloc<Int32>();

  // Write a value
  ptr.value = 42;

  // Read it back
  print(ptr.value); // 42

  // Free when done
  calloc.free(ptr);
}
$ dart main.dart
42

Pass a count to allocate a contiguous block. Index the pointer like an array.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void main() {
  const n = 5;
  final buf = calloc<Int32>(n);

  for (int i = 0; i < n; i++) {
    buf[i] = i * 10;
  }

  for (int i = 0; i < n; i++) {
    print(buf[i]);
  }

  calloc.free(buf);
}
$ dart main.dart
0
10
20
30
40

ptr.elementAt(n) returns a new pointer advanced by n elements — the equivalent of C pointer arithmetic. The original pointer address is unchanged.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void main() {
  final buf = calloc<Int32>(4);
  buf[0] = 10;
  buf[1] = 20;
  buf[2] = 30;
  buf[3] = 40;

  // Pointer advanced by 2 elements
  final mid = buf.elementAt(2);
  print(mid.value);      // 30
  print(mid[1]);         // 40

  calloc.free(buf);
}
$ dart main.dart
30
40

Structs

A C struct is mapped to a Dart class that extends Struct. Each field is declared external and annotated with its native type. Access struct fields through .ref on a Pointer<T>.

point.c
#include <stdint.h>

typedef struct {
    int32_t x;
    int32_t y;
} Point;

int32_t distance_squared(const Point *p) {
    return p->x * p->x + p->y * p->y;
}
$ gcc -shared -fPIC -o libpoint.so point.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

// Mirror of the C struct
final class Point extends Struct {
  @Int32()
  external int x;

  @Int32()
  external int y;
}

typedef DistSqNative = Int32 Function(Pointer<Point>);
typedef DistSqDart = int Function(Pointer<Point>);

void main() {
  final lib = DynamicLibrary.open('libpoint.so');
  final distSq = lib.lookupFunction<DistSqNative, DistSqDart>('distance_squared');

  final p = calloc<Point>();
  p.ref.x = 3;
  p.ref.y = 4;

  print(distSq(p)); // 25

  calloc.free(p);
}
$ dart main.dart
25

The Dart struct class is used only as a type parameter — you never instantiate it directly. Memory is always managed through a Pointer, and fields are accessed via .ref.

The following example shows a struct with multiple field types.

reading.c
#include <stdint.h>

typedef struct {
    int32_t id;
    double  value;
    uint8_t flags;
} Reading;

void fill_reading(Reading *r, int32_t id, double value, uint8_t flags) {
    r->id    = id;
    r->value = value;
    r->flags = flags;
}
$ gcc -shared -fPIC -o libreading.so reading.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

final class Reading extends Struct {
  @Int32()
  external int id;

  @Double()
  external double value;

  @Uint8()
  external int flags;
}

typedef FillNative = Void Function(Pointer<Reading>, Int32, Double, Uint8);
typedef FillDart   = void Function(Pointer<Reading>, int,   double, int);

void main() {
  final lib = DynamicLibrary.open('libreading.so');
  final fill = lib.lookupFunction<FillNative, FillDart>('fill_reading');

  final r = calloc<Reading>();
  fill(r, 42, 3.14, 0xFF);

  print(r.ref.id);     // 42
  print(r.ref.value);  // 3.14
  print(r.ref.flags);  // 255

  calloc.free(r);
}
$ dart main.dart
42
3.14
255

Arrays and Buffers

C structs often contain fixed-size inline arrays. In Dart FFI, model these with the Array<T> type and the @Array(n) annotation.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

final class RGB extends Struct {
  @Array(3)
  external Array<Uint8> channels;
}

void main() {
  final color = calloc<RGB>();

  color.ref.channels[0] = 255; // R
  color.ref.channels[1] = 128; // G
  color.ref.channels[2] = 0;   // B

  final r = color.ref.channels[0];
  final g = color.ref.channels[1];
  final b = color.ref.channels[2];
  print('R=$r, G=$g, B=$b'); // R=255, G=128, B=0

  calloc.free(color);
}
$ dart main.dart
R=255, G=128, B=0

For large buffers, asTypedList provides a zero-copy TypedData view over native memory. This avoids per-element copies when exchanging data with C code that fills or reads a buffer.

main.dart
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';

void main() {
  const n = 6;
  final ptr = calloc<Double>(n);

  // Zero-copy view as a Dart Float64List
  final view = ptr.asTypedList(n);

  for (int i = 0; i < n; i++) {
    view[i] = i * 0.5;
  }

  // Reads through the pointer confirm the same memory
  for (int i = 0; i < n; i++) {
    print(ptr[i]);
  }

  calloc.free(ptr);
}
$ dart main.dart
0.0
0.5
1.0
1.5
2.0
2.5

Converting a Dart list to a native array requires copying the values manually unless you allocate the memory first and fill it in place.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

Pointer<Int32> listToNative(List<int> values) {
  final ptr = calloc<Int32>(values.length);
  for (int i = 0; i < values.length; i++) {
    ptr[i] = values[i];
  }
  return ptr;
}

List<int> nativeToList(Pointer<Int32> ptr, int length) {
  return List.generate(length, (i) => ptr[i]);
}

void main() {
  final values = [10, 20, 30, 40, 50];
  final ptr = listToNative(values);

  final back = nativeToList(ptr, values.length);
  print(back); // [10, 20, 30, 40, 50]

  calloc.free(ptr);
}
$ dart main.dart
[10, 20, 30, 40, 50]

Strings

C uses null-terminated UTF-8 strings. The ffi package provides a toNativeUtf8 extension on String that allocates a C string from a Dart string, and toDartString on Pointer<Utf8> that converts back. Free the pointer when done.

main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void main() {
  final dart = 'Hello, FFI!';

  // Dart string → native UTF-8 string
  final native = dart.toNativeUtf8();
  print(native.runtimeType); // Pointer<Utf8>

  // Native UTF-8 string → Dart string
  final back = native.toDartString();
  print(back); // Hello, FFI!

  calloc.free(native);
}
$ dart main.dart
Pointer<Utf8>
Hello, FFI!

The next example passes a Dart string to a C function and reads back a C string that was filled by C into an output buffer.

greet.c
#include <stdio.h>

void greet(const char *name, char *out, int out_size) {
    snprintf(out, out_size, "Hello, %s!", name);
}
$ gcc -shared -fPIC -o libgreet.so greet.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

typedef GreetNative = Void Function(Pointer<Utf8>, Pointer<Utf8>, Int32);
typedef GreetDart   = void Function(Pointer<Utf8>, Pointer<Utf8>, int);

void main() {
  final lib = DynamicLibrary.open('libgreet.so');
  final greet = lib.lookupFunction<GreetNative, GreetDart>('greet');

  final name   = 'Dart'.toNativeUtf8();
  final outBuf = calloc<Uint8>(256);

  greet(name, outBuf.cast<Utf8>(), 256);

  print(outBuf.cast<Utf8>().toDartString()); // Hello, Dart!

  calloc.free(name);
  calloc.free(outBuf);
}
$ dart main.dart
Hello, Dart!

When a C function returns a const char* that points to static memory (i.e. memory the C library owns), do not free the pointer — just call toDartString on it.

version.c
const char *lib_version(void) {
    return "1.0.0";
}
$ gcc -shared -fPIC -o libversion.so version.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

typedef VersionNative = Pointer<Utf8> Function();
typedef VersionDart   = Pointer<Utf8> Function();

void main() {
  final lib = DynamicLibrary.open('libversion.so');
  final version = lib.lookupFunction<VersionNative, VersionDart>('lib_version');

  final ptr = version();
  // do not free: the pointer is owned by the C library
  print(ptr.toDartString()); // 1.0.0
}
$ dart main.dart
1.0.0

Callbacks

A C function that accepts a function pointer can be given a Dart function. Wrap the Dart function with NativeCallable.isolateLocal to produce a Pointer<NativeFunction<T>> that C can call. Close the callable when it is no longer needed.

foreach.c
#include <stdint.h>

typedef void (*IntCallback)(int32_t);

void apply_to_range(int32_t start, int32_t end, IntCallback cb) {
    for (int32_t i = start; i < end; i++) {
        cb(i);
    }
}
$ gcc -shared -fPIC -o libforeach.so foreach.c
main.dart
import 'dart:ffi';

typedef CallbackNative    = Void Function(Int32);
typedef ApplyRangeNative  = Void Function(Int32, Int32, Pointer<NativeFunction<CallbackNative>>);
typedef ApplyRangeDart    = void Function(int,   int,   Pointer<NativeFunction<CallbackNative>>);

void onValue(int v) {
  print('received: $v');
}

void main() {
  final lib = DynamicLibrary.open('libforeach.so');
  final applyRange = lib.lookupFunction<ApplyRangeNative, ApplyRangeDart>('apply_to_range');

  final callable = NativeCallable<CallbackNative>.isolateLocal(onValue);
  applyRange(1, 6, callable.nativeFunction);
  callable.close();
}
$ dart main.dart
received: 1
received: 2
received: 3
received: 4
received: 5

NativeCallable.isolateLocal creates a callback that must be called from the same Dart isolate that created it. If the C library calls the callback from another thread, use NativeCallable.listener instead, which posts the call to the isolate's event loop.

The older Pointer.fromFunction is still available and works well for callbacks that do not need to be closed explicitly. The function must be a top-level or static function.

main.dart
import 'dart:ffi';

typedef CallbackNative   = Void Function(Int32);
typedef ApplyRangeNative = Void Function(Int32, Int32, Pointer<NativeFunction<CallbackNative>>);
typedef ApplyRangeDart   = void Function(int,   int,   Pointer<NativeFunction<CallbackNative>>);

void onValue(int v) {
  print('value: $v');
}

void main() {
  final lib = DynamicLibrary.open('libforeach.so');
  final applyRange = lib.lookupFunction<ApplyRangeNative, ApplyRangeDart>('apply_to_range');

  final cbPtr = Pointer.fromFunction<CallbackNative>(onValue);
  applyRange(10, 13, cbPtr);
}
$ dart main.dart
value: 10
value: 11
value: 12

Error Handling

C functions signal errors with return codes. A common convention is returning 0 on success and a negative value on failure, sometimes alongside an output parameter. Check the return value before using any output.

divide.c
#include <stdint.h>

int32_t safe_divide(int32_t a, int32_t b, int32_t *result) {
    if (b == 0) {
        return -1; // error: division by zero
    }
    *result = a / b;
    return 0; // success
}
$ gcc -shared -fPIC -o libdivide.so divide.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

typedef SafeDivideNative = Int32 Function(Int32, Int32, Pointer<Int32>);
typedef SafeDivideDart   = int  Function(int,   int,   Pointer<Int32>);

void main() {
  final lib  = DynamicLibrary.open('libdivide.so');
  final div  = lib.lookupFunction<SafeDivideNative, SafeDivideDart>('safe_divide');
  final result = calloc<Int32>();

  var status = div(10, 2, result);
  if (status == 0) {
    print('10 / 2 = \${result.value}'); // 5
  }

  status = div(10, 0, result);
  if (status != 0) {
    print('error: division by zero');
  }

  calloc.free(result);
}
$ dart main.dart
10 / 2 = 5
error: division by zero

Some C APIs use errno. Retrieve it by calling strerror or by reading the thread-local errno variable through a small C helper.

errdemo.c
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>

int32_t open_port(int32_t port) {
    if (port < 1 || port > 65535) {
        errno = 22; // EINVAL
        return -1;
    }
    return 0;
}

const char *last_error(void) {
    return strerror(errno);
}
$ gcc -shared -fPIC -o liberrdemo.so errdemo.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

typedef OpenPortNative  = Int32 Function(Int32);
typedef OpenPortDart    = int Function(int);
typedef LastErrNative   = Pointer<Utf8> Function();
typedef LastErrDart     = Pointer<Utf8> Function();

void main() {
  final lib       = DynamicLibrary.open('liberrdemo.so');
  final openPort  = lib.lookupFunction<OpenPortNative, OpenPortDart>('open_port');
  final lastError = lib.lookupFunction<LastErrNative,  LastErrDart>('last_error');

  var status = openPort(8080);
  print(status == 0 ? 'port 8080 ok' : lastError().toDartString());

  status = openPort(99999);
  print(status == 0 ? 'port 99999 ok' : lastError().toDartString());
}
$ dart main.dart
port 8080 ok
Invalid argument

Real-World Examples

Calling a Math Library

The system math library is already loaded in any C process. On Linux the library file is libm.so.6. The functions sin, cos, and sqrt are standard and available everywhere.

main.dart
import 'dart:ffi';
import 'dart:math' as math;

typedef MathFnNative = Double Function(Double);
typedef MathFnDart   = double Function(double);

void main() {
  // On macOS use 'libm.dylib', on Windows 'msvcrt.dll'
  final libm = DynamicLibrary.open('libm.so.6');

  final sin  = libm.lookupFunction<MathFnNative, MathFnDart>('sin');
  final cos  = libm.lookupFunction<MathFnNative, MathFnDart>('cos');
  final sqrt = libm.lookupFunction<MathFnNative, MathFnDart>('sqrt');

  print(sin(math.pi / 6));   // ~0.5
  print(cos(0.0));            // 1.0
  print(sqrt(144.0));         // 12.0
}
$ dart main.dart
0.49999999999999994
1.0
12.0

Wrapping a C API

A clean Dart wrapper hides the FFI details behind a plain Dart class. The example wraps a simple C counter API.

counter.c
#include <stdlib.h>
#include <stdint.h>

typedef struct {
    int32_t value;
    int32_t step;
} Counter;

Counter *counter_new(int32_t initial, int32_t step) {
    Counter *c = malloc(sizeof(Counter));
    c->value = initial;
    c->step  = step;
    return c;
}

int32_t counter_next(Counter *c) {
    c->value += c->step;
    return c->value;
}

void counter_reset(Counter *c) {
    c->value = 0;
}

void counter_free(Counter *c) {
    free(c);
}
$ gcc -shared -fPIC -o libcounter.so counter.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

// ── Native struct ────────────────────────────────────────────────────────────

final class NativeCounter extends Struct {
  @Int32()
  external int value;

  @Int32()
  external int step;
}

// ── Native typedefs ──────────────────────────────────────────────────────────

typedef _NewN   = Pointer<NativeCounter> Function(Int32, Int32);
typedef _NextN  = Int32 Function(Pointer<NativeCounter>);
typedef _ResetN = Void  Function(Pointer<NativeCounter>);
typedef _FreeN  = Void  Function(Pointer<NativeCounter>);

typedef _NewD   = Pointer<NativeCounter> Function(int, int);
typedef _NextD  = int  Function(Pointer<NativeCounter>);
typedef _ResetD = void Function(Pointer<NativeCounter>);
typedef _FreeD  = void Function(Pointer<NativeCounter>);

// ── Dart wrapper ─────────────────────────────────────────────────────────────

class Counter {
  static late final _NewD   _new;
  static late final _NextD  _next;
  static late final _ResetD _reset;
  static late final _FreeD  _free;

  static void _init() {
    final lib = DynamicLibrary.open('libcounter.so');
    _new   = lib.lookupFunction<_NewN,   _NewD>('counter_new');
    _next  = lib.lookupFunction<_NextN,  _NextD>('counter_next');
    _reset = lib.lookupFunction<_ResetN, _ResetD>('counter_reset');
    _free  = lib.lookupFunction<_FreeN,  _FreeD>('counter_free');
  }

  static bool _initialized = false;

  final Pointer<NativeCounter> _ptr;

  Counter(int initial, int step) : _ptr = (() {
    if (!_initialized) { _init(); _initialized = true; }
    return _new(initial, step);
  })();

  int next()       => _next(_ptr);
  void reset()     => _reset(_ptr);
  void dispose()   => _free(_ptr);
}

// ── Usage ────────────────────────────────────────────────────────────────────

void main() {
  final c = Counter(0, 5);

  for (int i = 0; i < 5; i++) {
    print(c.next());
  }

  c.reset();
  print(c.next()); // 5

  c.dispose();
}
$ dart main.dart
5
10
15
20
25
5

Reading System Information

System calls accessible via libc can be reached directly with FFI. The following example calls getpid and getuid without writing any C glue code.

main.dart
import 'dart:ffi';

typedef PidFnNative = Int32 Function();
typedef PidFnDart   = int  Function();

void main() {
  // libc is always loaded in the process; DynamicLibrary.process finds it
  final libc = DynamicLibrary.process();

  final getpid = libc.lookupFunction<PidFnNative, PidFnDart>('getpid');
  final getuid = libc.lookupFunction<PidFnNative, PidFnDart>('getuid');

  print('PID: \${getpid()}');
  print('UID: \${getuid()}');
}
$ dart main.dart
PID: 18423
UID: 1000

Struct-Based C API

Many C libraries pass configuration or state as structs. The example below shows reading a C struct that is filled by the library, then extracting individual fields in Dart.

sensor.c
#include <stdint.h>
#include <string.h>

typedef struct {
    int32_t  id;
    double   temperature;
    double   humidity;
    uint8_t  status;    // 0 = ok, 1 = warning, 2 = error
} SensorData;

void sensor_read(int32_t sensor_id, SensorData *out) {
    out->id          = sensor_id;
    out->temperature = 21.5 + sensor_id * 0.3;
    out->humidity    = 55.0 - sensor_id * 1.2;
    out->status      = 0;
}
$ gcc -shared -fPIC -o libsensor.so sensor.c
main.dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';

final class SensorData extends Struct {
  @Int32()
  external int id;

  @Double()
  external double temperature;

  @Double()
  external double humidity;

  @Uint8()
  external int status;
}

typedef SensorReadNative = Void Function(Int32, Pointer<SensorData>);
typedef SensorReadDart   = void Function(int,   Pointer<SensorData>);

void main() {
  final lib    = DynamicLibrary.open('libsensor.so');
  final read   = lib.lookupFunction<SensorReadNative, SensorReadDart>('sensor_read');
  final data   = calloc<SensorData>();

  for (int id = 0; id < 3; id++) {
    read(id, data);
    final d = data.ref;
    print('Sensor \${d.id}: temp=\${d.temperature.toStringAsFixed(1)} '          'hum=\${d.humidity.toStringAsFixed(1)} status=\${d.status}');
  }

  calloc.free(data);
}
$ dart main.dart
Sensor 0: temp=21.5 hum=55.0 status=0
Sensor 1: temp=21.8 hum=53.8 status=0
Sensor 2: temp=22.1 hum=52.6 status=0

Performance Notes

A direct FFI call into compiled C code is fast. The overhead is small compared to platform channels or subprocess calls. For tight numerical loops, calling a C function that processes a large buffer in one shot is much faster than calling a Dart function for each element.

Memory allocation adds latency. Each calloc or malloc call crosses into native memory and may trigger a system allocation. Allocate once, reuse the buffer, and free when the work is done.

asTypedList gives a direct view into native memory with no copy. Use it when passing large arrays between Dart and C instead of looping over individual elements.

main.dart
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';

// Preferred: copy the whole list into native memory at once
Pointer<Double> fromFloatList(Float64List src) {
  final ptr  = calloc<Double>(src.length);
  final view = ptr.asTypedList(src.length);
  view.setAll(0, src);          // single memcpy-like operation
  return ptr;
}

void main() {
  final data = Float64List.fromList([1.0, 2.0, 3.0, 4.0, 5.0]);
  final ptr  = fromFloatList(data);

  for (int i = 0; i < data.length; i++) {
    print(ptr[i]);
  }

  calloc.free(ptr);
}
$ dart main.dart
1.0
2.0
3.0
4.0
5.0

Avoid allocating inside a loop. Allocating and freeing a buffer on every iteration dominates the total cost.

Type conversion between Dart and C is cheap for scalars. Strings require a heap allocation for the null-terminated copy — convert once and reuse the pointer if the same string is passed repeatedly.

Summary

In this tutorial we covered the Dart FFI. We loaded shared libraries, declared C types in Dart, called C functions, worked with pointers, mapped C structs, exchanged strings, and passed Dart functions to C as callbacks.

Use FFI when you need to call an existing C or system library, when you are writing a Dart package that wraps a native SDK, or when a performance-critical algorithm is already available as compiled C code.

Avoid FFI when a pure Dart solution is sufficient, when the complexity of managing native memory outweighs the benefit, or when your application only targets Flutter and a well-maintained plugin already exists for the same purpose.

The key rules are: match C types exactly, free every allocation, and keep FFI boundaries thin by wrapping them in plain Dart classes.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.