0Pricing
Flutter Mobile Development · 课时

使用 dart:ffi 调用 C 库

绑定原生共享库,并通过 dart:ffi 传递结构体和指针。

使用 dart:ffi 调用 C 库 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why dart:ffi?

dart:ffi is Dart's Foreign Function Interface. It lets your Flutter app call functions in native C shared libraries (.so, .dylib, .dll, or the iOS process image) directly, with no platform-channel round-trip.

  • Synchronous by default and very low overhead, unlike MethodChannel which serializes messages across an async boundary.
  • Ideal for CPU-heavy code, existing C/C++/Rust libraries, and OS-level APIs (sqlite, libsodium, image codecs).
  • You bind a C signature to a Dart signature, then call it like an ordinary function.

The cost: you manage memory and types yourself. Get a pointer or a struct layout wrong and you crash the whole process.

Opening a DynamicLibrary

Everything starts with a DynamicLibrary. It is the handle to the loaded native code from which you look up symbols.

  • DynamicLibrary.open(path) loads a shared library by file name. On Android use 'libfoo.so'; on iOS/macOS code is usually statically linked, so use DynamicLibrary.process() or DynamicLibrary.executable().
  • Pick the right name per platform with Platform.isAndroid / Platform.isIOS.
import 'dart:ffi';
import 'dart:io' show Platform;

DynamicLibrary openNativeLib() {
  if (Platform.isAndroid) {
    return DynamicLibrary.open('libnative_math.so');
  }
  if (Platform.isIOS || Platform.isMacOS) {
    // Symbols are linked into the app process on iOS.
    return DynamicLibrary.process();
  }
  if (Platform.isWindows) {
    return DynamicLibrary.open('native_math.dll');
  }
  return DynamicLibrary.open('libnative_math.so');
}

Native types vs Dart types

FFI uses two type universes. The native type describes the C ABI; the Dart type is what your Dart code actually sees.

  • Int32, Int64, Uint8, Double, Float are native marker types — you never instantiate them, they map to Dart int/double.
  • Pointer<T> is a native address. Void marks no value.
  • The C function type is written with Function using native types; the Dart-facing type uses plain Dart types.

Example: C int32_t add(int32_t, int32_t) becomes native Int32 Function(Int32, Int32) and Dart int Function(int, int).

Looking up and calling a function

Use lookupFunction to bind a C symbol to a Dart function in one call. It takes two generic parameters: the native signature and the Dart signature.

  • The first type argument must use native types (Int32, Double, …).
  • The second is the callable Dart type returned to you.

Below, a pure-Dart simulation shows the call shape that FFI mirrors at runtime.

// Conceptually, FFI does this:
//   typedef NativeAdd = Int32 Function(Int32, Int32);
//   typedef DartAdd   = int Function(int, int);
//   final add = lib.lookupFunction<NativeAdd, DartAdd>('add');

// Pure-Dart stand-in so the call site is identical in shape:
int Function(int, int) bindAdd() {
  return (int a, int b) => a + b; // native impl returns a + b
}

void main() {
  final add = bindAdd();
  print('add(20, 22) = ${add(20, 22)}');
}

typedef for clean bindings

Real bindings declare the two signatures as typedefs. This keeps lookupFunction readable and lets you reuse signatures.

  • Native typedef uses native marker types and the suffix convention ...Native.
  • Dart typedef uses Dart types.
  • The string passed to lookupFunction is the exact exported C symbol name.
import 'dart:ffi';

// C: double native_pow(double base, int32_t exp);
typedef NativePowNative = Double Function(Double, Int32);
typedef NativePow = double Function(double, int);

class MathBindings {
  final DynamicLibrary lib;
  late final NativePow pow;

  MathBindings(this.lib) {
    pow = lib.lookupFunction<NativePowNative, NativePow>('native_pow');
  }
}

Allocating native memory

To pass pointers you must allocate native (off-heap) memory. The package:ffi library provides malloc (a calloc variant also exists) plus extensions for strings.

  • malloc<Int32>() returns a Pointer<Int32>; use .value to read/write.
  • malloc<Int32>(n) allocates an array of n elements; index with ptr[i] or ptr.elementAt(i).
  • You must free what you allocate with malloc.free(ptr) — the GC does not track native memory.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void usePointer() {
  final ptr = malloc<Int32>(3); // array of 3 int32
  try {
    ptr[0] = 10;
    ptr[1] = 20;
    ptr[2] = 12;
    var sum = 0;
    for (var i = 0; i < 3; i++) {
      sum += ptr[i];
    }
    print('sum = $sum');
  } finally {
    malloc.free(ptr); // always free
  }
}

Marshalling strings

C strings are null-terminated char*, represented as Pointer<Utf8> (from package:ffi). Conversion goes both ways:

  • Dart → C: myString.toNativeUtf8() allocates a native buffer (free it later).
  • C → Dart: ptr.toDartString() copies the bytes into a Dart String.

If the native function returns a pointer it allocated, you typically must call its matching free export — never malloc.free memory you did not allocate with malloc.

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

// C: int32_t count_chars(const char* text);
typedef CountNative = Int32 Function(Pointer<Utf8>);
typedef Count = int Function(Pointer<Utf8>);

int countChars(Count nativeCount, String text) {
  final cStr = text.toNativeUtf8();
  try {
    return nativeCount(cStr);
  } finally {
    malloc.free(cStr);
  }
}

Defining a Struct

To marshal C structs, declare a Dart class extending Struct. Each field is annotated with its native type so the FFI runtime computes the exact memory layout/offsets.

  • Scalar fields get annotations like @Int32(), @Double().
  • Field order and types must match the C struct exactly, including padding/alignment rules.
  • You never construct a Struct with new; you obtain one via a Pointer<T>.ref backed by native memory.
import 'dart:ffi';

// C:
// typedef struct { double x; double y; } Point;
final class Point extends Struct {
  @Double()
  external double x;

  @Double()
  external double y;
}

Passing structs by pointer

Most C APIs take a Point*. Allocate the struct, fill it through .ref, pass the pointer, then read results back.

  • malloc<Point>() gives a Pointer<Point> sized correctly for the layout.
  • ptr.ref is a view onto that native memory; writing ptr.ref.x = 3.0 mutates the C struct in place.
  • The native function reads/writes the same memory — this is how you get values out by reference.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

// C: void translate(Point* p, double dx, double dy);
typedef TranslateNative = Void Function(Pointer<Point>, Double, Double);
typedef Translate = void Function(Pointer<Point>, double, double);

final class Point extends Struct {
  @Double()
  external double x;
  @Double()
  external double y;
}

void moveOrigin(Translate translate) {
  final p = malloc<Point>();
  try {
    p.ref.x = 0;
    p.ref.y = 0;
    translate(p, 4.0, 5.0);
    print('moved to (${p.ref.x}, ${p.ref.y})');
  } finally {
    malloc.free(p);
  }
}

Don't block the UI thread

FFI calls are synchronous: they run on the calling isolate's thread. A long native computation called from the main isolate freezes Flutter's UI.

  • For heavy work, run the FFI call inside an Isolate (e.g. Isolate.run on modern Dart) or a worker isolate.
  • Note: a DynamicLibrary handle and native pointers can be passed between isolates as addresses, but each isolate must re-open or share carefully — treat pointers as plain integers across boundaries.
  • Native code that calls back into Dart must use NativeCallable / send ports, not arbitrary threads.
import 'dart:isolate';

// Simulates offloading a heavy native FFI computation off the UI thread.
int _heavyNativeWork(int n) {
  var acc = 0;
  for (var i = 0; i < n; i++) {
    acc = (acc + i) % 1000003;
  }
  return acc;
}

Future<void> main() async {
  final result = await Isolate.run(() => _heavyNativeWork(5000000));
  print('result = $result');
}

Memory safety and ownership

FFI bugs are process crashes, not exceptions. Discipline matters:

  • Ownership: whoever allocates must free. Memory from malloc → malloc.free. Memory from a C library → that library's destructor export.
  • Wrap allocate/use/free in try/finally so you free even on error.
  • For long-lived native objects, attach a NativeFinalizer so the destructor runs when the Dart wrapper is GC'd.
  • Never read .ref/.value on a pointer after it is freed — that is a use-after-free.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

class SafeBuffer {
  final Pointer<Uint8> ptr;
  final int length;
  SafeBuffer(this.length) : ptr = malloc<Uint8>(length);

  void dispose() => malloc.free(ptr);
}

void main() {
  final buf = SafeBuffer(16);
  try {
    buf.ptr[0] = 255;
    print('first byte = ${buf.ptr[0]}');
  } finally {
    buf.dispose();
  }
}

Quick Check

Answer based on dart:ffi struct and memory rules.

Recap

You can now bind to native C libraries from Flutter with dart:ffi:

  • Load code with DynamicLibrary.open / .process(), choosing the path per platform.
  • Bind symbols with lookupFunction<Native, Dart>, declaring native vs Dart typedefs.
  • Allocate off-heap memory with malloc, marshal strings via toNativeUtf8 / toDartString, and pass arrays as pointers.
  • Define structs by extending Struct with native-type annotations; pass them by pointer and read results through .ref.
  • Keep heavy calls off the UI isolate, and enforce strict ownership: free what you allocate, use try/finally and NativeFinalizer, and never touch freed pointers.

FFI trades safety for speed and reach — correct types, layout, and lifetimes are entirely your responsibility.

常见问题解答

「使用 dart:ffi 调用 C 库」课时是免费的吗?

是的 — 「使用 dart:ffi 调用 C 库」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。

「使用 dart:ffi 调用 C 库」这节课中我会学到什么?

绑定原生共享库,并通过 dart:ffi 传递结构体和指针。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Flutter Mobile Development 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「使用 dart:ffi 调用 C 库」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Flutter Mobile Development 课中编写并运行代码吗?

能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 dart:ffi 调用 C 库
  2. 类型安全的平台通道与 Pigeon
  3. 为 iOS 和 Android 编写自定义平台插件
  4. 后台隔离区与原生内存管理
← 返回 Flutter Mobile Development