0Pricing
Flutter Mobile Development · Pelajaran

Memanggil Library C dengan dart:ffi

Hubungkan ke library native bersama dan marshalkan struct serta pointer melalui dart:ffi.

Memanggil Library C dengan dart:ffi adalah pelajaran Flutter Mobile Development gratis di CoddyKit. Ini adalah pelajaran 1 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar Flutter Mobile Development, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus Flutter Mobile Development mencakup 4 pelajaran total.

Bagian dari pelajaran ini belum diterjemahkan dan ditampilkan dalam bahasa Inggris.

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.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Memanggil Library C dengan dart:ffi” gratis?

Ya — teks lengkap “Memanggil Library C dengan dart:ffi” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus Flutter Mobile Development, upgrade ke CoddyKit PRO. Kursus Flutter Mobile Development mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Memanggil Library C dengan dart:ffi”?

Hubungkan ke library native bersama dan marshalkan struct serta pointer melalui dart:ffi. Kamu berlatih Flutter Mobile Development dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai Flutter Mobile Development?

Tidak diperlukan pengalaman sebelumnya. Flutter Mobile Development di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 1 dari 4.

Berapa lama pelajaran “Memanggil Library C dengan dart:ffi” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran Flutter Mobile Development ini?

Ya. Setiap pelajaran Flutter Mobile Development menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Memanggil Library C dengan dart:ffi
  2. Platform Channel Aman Tipe dengan Pigeon
  3. Menulis Plugin Platform Khusus untuk iOS dan Android
  4. Isolate Latar Belakang dan Pengelolaan Memori Native
← Kembali ke Flutter Mobile Development