FFI: dasar-dasar, Foreign Function Interface dan integrasi dengan C

Penulis: IT Sectr Diterbitkan: 2026-06-05 Waktu membaca: 10 mnt

FFI (Foreign Function Interface) — adalah mekanisme bahasa Dart, yang disediakan oleh paket dart:ffi, yang memungkinkan pemanggilan langsung fungsi dari pustaka C native tanpa lapisan perantara di Kotlin, Swift atau Java. Pengembang memuat pustaka dinamis (.so di Android, .dylib di iOS, .dll di Windows), mendeklarasikan tanda tangan fungsi C dan memanggilnya seperti fungsi Dart biasa. Menurut Dart API Reference (2025), FFI mengurangi overhead panggilan antar-bahasa hingga 0.1 μs, yang puluhan kali lebih cepat daripada melalui Method Channel.

Poin utama

  • FFI (Foreign Function Interface) — mekanisme pemanggilan langsung fungsi C dari Dart
  • dart:ffi menyediakan API untuk memuat pustaka dan mendeklarasikan tanda tangan
  • Kinerja panggilan melalui FFI 50–100 kali lebih tinggi daripada melalui Method Channel
  • Tipifikasi FFI mendukung tipe primitif, struktur dan pointer C
  • Flutter menggunakan FFI untuk integrasi dengan pustaka native: OpenCV, SQLite, FFmpeg

Apa itu FFI?

FFI (Foreign Function Interface) — adalah mekanisme yang memungkinkan bahasa pemrograman memanggil fungsi yang ditulis dalam bahasa lain. Dalam konteks Dart dan Flutter, FFI berarti kemampuan untuk memanggil fungsi dari pustaka C/C++ langsung dari kode Dart, tanpa perlu menulis kode platform di Java (Android) atau Swift/Objective-C (iOS).

Paket dart:ffi muncul di Dart 2.12 (2021) dan sejak itu menjadi alat kunci untuk integrasi Flutter dengan kode native. Sebelum munculnya dart:ffi, satu-satunya cara untuk memanggil fungsi C dari Dart adalah Method Channel — mekanisme asinkron yang mengirimkan pesan melalui serialisasi JSON antara Dart dan sisi native. FFI bekerja berbeda: kode Dart mengakses langsung memori pustaka C, memanggil fungsi melalui ABI native (Application Binary Interface) tanpa serialisasi dan tanpa pergantian konteks.

FFI sangat dibutuhkan dalam skenario di mana kinerja sangat penting: pemrosesan gambar (OpenCV), audio (FFmpeg), kriptografi (OpenSSL), pembelajaran mesin (TensorFlow Lite) dan basis data (SQLite). Dalam semua kasus ini, Method Channel menciptakan latensi yang tidak dapat diterima, sementara FFI memberikan kinerja yang sebanding dengan kode C/C++ native. Pustaka dart:ffi juga mendukung bekerja dengan memori: alokasi, pembebasan dan pengelolaan pointer.

FFI vs Method Channel: perbedaan mendasar

Method Channel bekerja secara asinkron: Dart mengirimkan pesan ke kode native, kode native memprosesnya dan mengirimkan hasilnya kembali. Setiap panggilan memerlukan serialisasi argumen ke Map, transmisi melalui antrian dan deserialisasi. Ini membutuhkan waktu 0.5–5 ms per panggilan. FFI bekerja secara sinkron dan tanpa serialisasi — panggilan fungsi C membutuhkan waktu 0.01–0.1 μs. Perbedaan 50–500 kali, yang sangat penting untuk operasi frekuensi tinggi.

Bagaimana cara kerja dart:ffi?

Bekerja dengan dart:ffi terdiri dari tiga tahap: memuat pustaka, mendeklarasikan tanda tangan dan memanggil fungsi. Setiap tahap menggunakan tipifikasi ketat Dart, yang meminimalkan kesalahan saat runtime.

Pada tahap pertama, pustaka dinamis dimuat melalui kelas DynamicLibrary. Pustaka dapat dimuat berdasarkan nama (libxyz.so, libxyz.dylib, xyz.dll) atau berdasarkan jalur lengkap. Dart secara otomatis mencari pustaka di jalur standar sistem. DynamicLibrary menyediakan metode lookupFunction, yang menghubungkan fungsi Dart dengan fungsi C berdasarkan nama simbol.

Pada tahap kedua, fungsi Dart dideklarasikan dengan anotasi tipe yang sesuai dengan tanda tangan C. Untuk ini digunakan tipe khusus dari dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle dan lainnya. Anotasi lookupFunction menerima dua parameter generik: tipe fungsi Dart (bagaimana akan terlihat di Dart) dan tipe fungsi C native (bagaimana dideklarasikan di C).

Pada tahap ketiga, fungsi Dart yang dihasilkan dipanggil seperti fungsi biasa. Argumen dilewatkan langsung, hasil dikembalikan segera. Jika fungsi C mengubah memori melalui pointer, Dart dapat membaca perubahan ini melalui kelas Pointer. Pengelolaan memori di sisi C tetap menjadi tanggung jawab pengembang — dart:ffi tidak mengelola memori yang dialokasikan oleh malloc di C.

Contoh dasar: memanggil fungsi C dari Dart

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

// Deklarasi fungsi C: int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)

void main() {
    final lib = DynamicLibrary.open('libcalculator.so')
    final AddDart add = lib
        .lookupFunction<AddNative, AddDart>('add')

    print(add(5, 3)) // 8
}

Dalam contoh ini, add adalah fungsi C yang menerima dua int dan mengembalikan int. typedef AddNative menggambarkan tanda tangan C dengan tipe dart:ffi, dan AddDart — bagaimana fungsi ini akan terlihat di Dart. lookupFunction menghubungkannya dan mengembalikan fungsi Dart yang dapat dipanggil seperti biasa.

Tipe data di FFI

dart:ffi menyediakan satu set tipe yang sesuai dengan tipe C. Setiap tipe memiliki ukuran tetap dan aturan konversi antara Dart dan C. Memahami korespondensi tipe sangat penting untuk kerja FFI yang benar — kesalahan dalam ukuran atau tanda tipe dapat menyebabkan aplikasi crash.

Tipe CTipe dart:ffiTipe DartUkuran (byte)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (pointer)
void*Pointer<Void>Pointer8 (pointer)
structPointer<T> (Struct)Pointertergantung pada field

Untuk bekerja dengan string C (char*) dart:ffi menggunakan Pointer<Int8>. Konversi dari Dart String ke char* C dan sebaliknya dilakukan melalui metode toNativeUtf8 (dari paket ffi) dan fromUtf8. Penting untuk membebaskan string C setelah digunakan melalui calloc.free untuk menghindari kebocoran memori.

Struktur (Struct)

dart:ffi mendukung pendeklarasian struktur C sebagai kelas Dart yang mewarisi Struct. Field struktur dideklarasikan dengan anotasi @Int32(), @Float(), @Array() dan lainnya. Ukuran dan offset field dihitung secara otomatis sesuai dengan ABI platform. Pointer<Point> dapat diperoleh dari fungsi C yang mengembalikan pointer ke struktur atau dialokasikan di Dart melalui calloc.

dart
// Struktur C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
    @Int32()
    external int x

    @Int32()
    external int y
}

// Memanggil fungsi C yang mengembalikan Point*
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)

final Pointer<Point> p = createPoint(10, 20)
print('x: ${p.ref.x}, y: ${p.ref.y}')
calloc.free(p) // bebaskan memori

Kelas Point mewarisi Struct dan mendeklarasikan field x dan y dengan anotasi @Int32(). Kode C yang dihasilkan akan memiliki tata letak field yang persis sama di memori. Pointer.ref menyediakan akses ke field struktur melalui getter dan setter.

Contoh praktis FFI

Mari kita lihat contoh yang lebih kompleks — integrasi dengan pustaka C untuk menghitung hash SHA256. Ini adalah tugas tipikal di mana FFI memberikan keunggulan kinerja yang signifikan dibandingkan Method Channel.

Integrasi dengan OpenSSL melalui FFI

Pustaka OpenSSL menyediakan fungsi SHA256 yang menghitung hash dari sebuah string. Melalui dart:ffi kita dapat memanggilnya langsung, tanpa menulis wrapper Java atau Swift. Ini adalah contoh bagaimana FFI memungkinkan penggunaan kembali pustaka C yang ada di Flutter.

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

// Tanda tangan: unsigned char* SHA256(
//   const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
    Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
    Pointer<Uint8>, int, Pointer<Uint8>)

String sha256(String input) {
    final lib = DynamicLibrary.open('libcrypto.so')
    final Sha256Dart sha256Fn = lib
        .lookupFunction<Sha256Native, Sha256Dart>('SHA256')

    final inputPtr = input.toNativeUtf8()
    final outputPtr = calloc(Uint8)(32) // SHA256 = 32 byte

    sha256Fn(inputPtr, input.length, outputPtr)

    final digest = outputPtr.asTypedList(32)
    final hex = digest.map((b) => b.toRadixString(16)
        .padLeft(2, '0')).join()

    calloc.free(inputPtr)
    calloc.free(outputPtr)

    return hex
}

Dalam contoh ini, fungsi sha256 memuat pustaka libcrypto.so, menemukan simbol SHA256 dan memanggilnya dengan pointer ke data input dan output. toNativeUtf8 mengonversi Dart String ke string C (mengalokasikan memori), dan asTypedList memungkinkan membaca array byte hasil. Memori dibebaskan setelah digunakan — ini adalah langkah wajib untuk mencegah kebocoran.

Alokasi dan pembebasan memori

Paket ffi menyediakan fungsi calloc untuk mengalokasikan memori yang kompatibel dengan C. Memori yang dialokasikan harus dibebaskan melalui calloc.free, jika tidak akan terjadi kebocoran. Untuk manajemen memori otomatis, dapat digunakan kelas Arena dari paket ffi, yang membebaskan semua memori yang dialokasikan di dalamnya saat arena.release() dipanggil. Ini sangat nyaman ketika ada banyak alokasi sementara.

Keterbatasan dan praktik terbaik

Meskipun FFI sangat kuat, ia memiliki keterbatasan yang harus dipertimbangkan saat merancang arsitektur aplikasi Flutter. Keterbatasan utama terkait dengan keamanan tipe, manajemen memori dan kompatibilitas platform.

Keamanan

FFI tidak memeriksa tipe saat runtime. Jika fungsi C mengharapkan pointer tetapi angka yang diberikan, aplikasi akan crash dengan segmentation fault. Disarankan menggunakan FFIgen — alat yang menghasilkan wrapper Dart yang aman tipe berdasarkan header C (file .h). FFIgen menganalisis deklarasi fungsi C dan membuat kode Dart dengan tipe yang benar, yang menghilangkan kesalahan pada tahap penulisan kode.

Kompatibilitas platform

Nama dan jalur ke pustaka dinamis berbeda di berbagai platform: libxyz.so di Android/Linux, libxyz.dylib di iOS/macOS, xyz.dll di Windows. Untuk pustaka lintas platform digunakan kompilasi bersyarat melalui dart:io (Platform.isAndroid, Platform.isIOS) atau abstraksi seperti package:ffi. Disarankan untuk membuat metode factory yang mengembalikan pustaka yang benar untuk platform saat ini.

Manajemen memori

FFI tidak mengelola memori di sisi C. Jika fungsi C mengalokasikan memori melalui malloc, harus dibebaskan melalui free, jika tidak akan terjadi kebocoran. Di Dart tidak ada pengumpul sampah untuk memori C. Rekomendasi: selalu bebaskan memori di metode yang sama di mana ia dialokasikan atau gunakan Arena untuk pembebasan grup.

Kinerja dan thread

Panggilan FFI dieksekusi di thread yang sama dengan kode Dart. Operasi sinkron yang lama (lebih dari 10 ms) memblokir thread UI dan menyebabkan frame drop. Untuk operasi yang lama, fungsi C harus dipanggil di isolat (Isolate) atau memastikan bahwa fungsi C memulai pekerjaan di thread latar belakang dan memberi tahu Dart melalui Port atau callback.

dart
// FFI di isolat untuk operasi panjang
import 'dart:isolate'

Future<String> computeHash(String input) async {
    final port = ReceivePort()
    await Isolate.spawn((SendPort sendPort) {
        final result = sha256(input) // Panggilan FFI
        sendPort.send(result)
    }, port.sendPort)

    return await port.first as String
}

Memindahkan panggilan FFI ke isolat memastikan bahwa thread UI tidak diblokir. Namun, mentransfer data dalam jumlah besar antar isolat memerlukan penyalinan memori. Untuk buffer besar (>10 MB) lebih baik menggunakan ShareMemory atau file yang dipetakan memori.

Pertanyaan yang sering diajukan

Apa perbedaan FFI dengan Method Channel?

FFI memanggil fungsi C secara langsung, sinkron dan tanpa serialisasi — latensi 0.01–0.1 μs. Method Channel bekerja secara asinkron melalui serialisasi JSON dengan latensi 0.5–5 ms. FFI cocok untuk operasi berkinerja tinggi, Method Channel untuk panggilan API platform sederhana.

Bisakah fungsi C++ dipanggil melalui FFI?

Secara langsung — tidak, dart:ffi hanya mendukung fungsi C. Untuk memanggil C++ perlu membuat wrapper C dengan extern "C" (titik masuk yang diekspor sebagai simbol C). Kelas C++ memerlukan lapisan tambahan yang mengonversi panggilan metode menjadi fungsi C.

Bagaimana menangani kesalahan di fungsi C?

FFI tidak mendukung pengecualian — jika fungsi C mengembalikan kode kesalahan, harus diperiksa secara manual. Disarankan untuk membungkus panggilan FFI dalam try-catch di Dart dan memeriksa kode yang dikembalikan oleh fungsi C. Kesalahan kritis (segfault) tidak dapat ditangkap.

Pustaka apa yang tidak bisa digunakan melalui FFI?

FFI tidak bekerja dengan pustaka yang memerlukan inisialisasi kompleks Java (JNI) atau Objective-C (Message Dispatch). Misalnya, UIKit dan Android Views tidak tersedia melalui FFI. Keterbatasan terkait dengan fakta bahwa FFI bekerja pada level C ABI, sementara API ini memerlukan runtime spesifik.

Apakah perlu mengompilasi pustaka C untuk setiap platform secara terpisah?

Ya, pustaka C dikompilasi secara terpisah untuk setiap platform target. Untuk Android, .so dibangun untuk berbagai ABI (armeabi-v7a, arm64-v8a, x86_64). Untuk iOS — .dylib universal (arm64). Untuk Windows — .dll. Flutter secara otomatis mengemas versi pustaka yang benar saat membangun.

Kesimpulan

  • FFI (Foreign Function Interface) — mekanisme pemanggilan langsung fungsi C dari Dart melalui dart:ffi
  • Kinerja panggilan FFI 50–500 kali lebih tinggi daripada melalui Method Channel
  • Arsitektur mencakup memuat pustaka, mendeklarasikan tanda tangan dan memanggil fungsi
  • Tipe data dart:ffi mendukung Int32, Float, Double, Pointer, Struct dan tipe C lainnya
  • Memori di sisi C dikelola secara manual melalui calloc/free atau Arena
  • Keterbatasan FFI: tidak ada pemeriksaan tipe runtime, tidak ada dukungan C++ langsung, memblokir thread UI
  • Gunakan FFI untuk integrasi kinerja tinggi dengan pustaka native di Flutter

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga