Flutter FFI: wanneer Dart native code nodig heeft

Gepubliceerd op 21 augustus 2026

8 min lezen
Flutter-app die native C-code aanroept via dart:ffi
Jonas Ockerman
Jonas OckermanCo-Founder

De meeste Flutter apps hebben nooit native code nodig. Dart regelt networking, state management, UI rendering en zelfs cryptografie zonder dat je naar C moet zakken. Maar soms loop je tegen een muur: de OS API die je nodig hebt heeft geen Dart binding, de bibliotheek die je nodig hebt is in C geschreven, of je performance budget wordt in microseconden gemeten.

Wij liepen tegen die muur aan bij het bouwen van een examenmonitoringtool voor Karel de Grote Hogeschool. De app moest netwerkpakketten in realtime opvangen, virtuele machines detecteren en hardware-adapters oplijsten, en dat op Windows, macOS en Linux. Niets daarvan kan in pure Dart.

Zo gebruikten we Flutter’s Foreign Function Interface (FFI) om die kloof te overbruggen.

Wat Flutter FFI precies is

FFI staat voor Foreign Function Interface. Het laat Dart toe om rechtstreeks functies aan te roepen in gecompileerde native bibliotheken (.so op Linux, .dylib op macOS, .dll op Windows), zonder platform channels of message serialization.

Het cruciale verschil met platform channels: FFI is synchroon en draait in dezelfde geheugenruimte. Geen message encoding, geen async bridge overhead, geen wachten op de platform thread. Je roept een C-functie aan vanuit Dart op dezelfde manier als C dat doet.

// Laad de gecompileerde bibliotheek
final dylib = DynamicLibrary.open('capture_wrapper.so');

// Zoek een functie op via haar C-naam
final findAllDevs = dylib.lookupFunction<
  Pointer<Utf8> Function(),  // C-signatuur
  Pointer<Utf8> Function()   // Dart-signatuur
>('find_all_devices');

// Roep ze aan
final result = findAllDevs();

Dat is alles. Geen platform channel boilerplate, geen MethodChannel, geen handlers registreren.

Waarom we native code nodig hadden

De examenmonitor van KDG houdt studentenmachines in de gaten tijdens examens. Docenten moeten weten of een student verbinding maakt met een onverwacht netwerk, Bluetooth aanzet, een VM opent of een verboden applicatie start. Alle activiteit wordt versleuteld en lokaal gelogd voor nazicht achteraf.

Drie mogelijkheden vroegen om native code:

1. Realtime packet capture. Dart heeft geen API voor packet capture. Verkeer van een interface lezen vraagt toegang op kernelniveau die Dart’s networking-laag niet blootstelt. We wikkelden een volwassen C-bibliotheek voor capture in achter een FFI-grens.

2. Detectie van virtuele machines. Weten of de app in VMware, VirtualBox of Hyper-V draait, steunt op signalen die de Dart runtime niet kan zien. We wikkelden een C++-bibliotheek voor detectie in en stelden één getypeerde aanroep beschikbaar.

3. Netwerkadapters oplijsten. Fysieke en virtuele netwerkadapters met hun hardwaredetails oplijsten vraagt OS-API’s die Dart niet aanbiedt.

De architectuur

Elke native mogelijkheid zit in haar eigen Dart package, met een propere grens:

exam_monitor (Flutter app)
  ├── capture_wrapper (Dart FFI package)
  │     ├── src/capture_wrapper.c      ← wikkelt de C-bibliotheek voor capture in
  │     ├── src/capture_wrapper.h      ← header voor ffigen
  │     └── lib/capture_wrapper.dart   ← automatisch gegenereerde bindings

  └── vm_wrapper (Dart FFI package)
        ├── src/vm_wrapper.cpp         ← wikkelt de C++-bibliotheek voor detectie in
        ├── src/vm_wrapper.h
        └── lib/vm_wrapper.dart

De Flutter app raakt nooit ruwe pointers aan. Elk wrapper package stelt getypeerde Dart API’s beschikbaar. De native code wordt per platform gecompileerd met CMake (Linux/Windows) en CocoaPods (macOS).

Bindings automatisch genereren met ffigen

FFI-bindings met de hand schrijven is vervelend en foutgevoelig. Wij gebruikten package:ffigen om ze uit C-headerbestanden te genereren.

Je definieert je C-functies in een header:

// capture_wrapper.h
const char* find_all_devices(void);
intptr_t init_dart_api(void* data);
void run_capture(int64_t send_port, const char* device);
int get_packet_count(void);

Voeg een ffigen-config toe aan je package:

# ffigen.yaml
name: NetworkActivityBindings
output: lib/src/capture_wrapper_bindings_generated.dart
headers:
  entry-points:
    - src/capture_wrapper.h

Draai dart run ffigen en je krijgt type-veilige Dart bindings met de juiste pointertypes, struct-layouts en functiesignaturen. Verandert de C-API, dan genereer je opnieuw.

Het isolate-probleem

Packet capture is een blokkerende operatie. De leeslus van de capture-bibliotheek zit in een strakke cyclus die uit de kernelbuffer haalt en voor elk pakket je handler aanroept. Roep je dat aan vanuit de hoofdisolate van Dart, dan bevriest de UI.

De oplossing: draai de capture in een aparte Dart isolate en gebruik de Dart Native API om resultaten terug te sturen.

// In C: stuur elk opgevangen pakket terug naar Dart
static Dart_Port_DL g_send_port;

void run_capture(int64_t send_port, const char* device) {
    g_send_port = (Dart_Port_DL) send_port;
    // Open het device en loop, roep packet_handler aan per pakket
}

static void packet_handler(unsigned char *args,
    const struct capture_pkthdr *header,
    const unsigned char *packet) {

    // Parse het pakket (haal IP's, poorten, protocol eruit)
    // ...

    // Stuur naar Dart via de native port
    Dart_CObject obj;
    obj.type = Dart_CObject_kString;
    obj.value.as_string = packet_info;
    Dart_PostCObject_DL(g_send_port, &obj);
}
// Initialiseer de Dart native API één keer, voor het spawnen
initDartApi(NativeApi.initializeApiDLData);

final receivePort = ReceivePort();
// Geef de native kant het int64-id van de port, niet het SendPort-object
final nativePort = receivePort.sendPort.nativePort;

await Isolate.spawn((int port) {
  // Blokkerende capture, draait tot ze gestopt wordt
  runCapture(port, 'eth0');
}, nativePort);

// Pakketten komen binnen als berichten op de receive port
receivePort.listen((packet) {
  // Werk de UI bij, schrijf naar het versleutelde log
});

Dit patroon, een blokkerende native aanroep in een isolate met message-passing terug naar de hoofdisolate, is herbruikbaar voor elke langlopende native operatie.

Hulp nodig met FFI?

Platformspecifieke build setup

Elk platform compileert de native code anders.

Linux (CMake):

add_library(capture_wrapper SHARED "src/capture_wrapper.c")
find_library(CAPTURE_LIB NAMES capture)
target_link_libraries(capture_wrapper PRIVATE ${CAPTURE_LIB})

macOS (CocoaPods):

Pod::Spec.new do |s|
  s.source_files = 'src/**/*.{c,h}'
  s.frameworks = 'SystemConfiguration'
  s.libraries = 'capture'
end

Windows (CMake + SDK van de leverancier):

add_library(capture_wrapper SHARED "src/capture_wrapper.c")
target_include_directories(capture_wrapper PRIVATE "${CAPTURE_SDK}/Include")
target_link_libraries(capture_wrapper PRIVATE "${CAPTURE_SDK}/Lib/x64/capture.lib")

Het plugin-systeem van Flutter zorgt ervoor dat de gecompileerde bibliotheken mee in de app verpakt worden. Je geeft FFI-ondersteuning aan in pubspec.yaml:

flutter:
  plugin:
    platforms:
      linux:
        ffiPlugin: true
      macos:
        ffiPlugin: true
      windows:
        ffiPlugin: true

Build hooks: de nieuwere aanpak

Sinds Dart 3.10 en Flutter 3.38 kan het systeem van package:hooks en package:code_assets (build hooks en code assets) het compileren van native bibliotheken automatiseren. In plaats van per platform handmatig CMake te configureren en ffiPlugin: true op te geven, schrijf je een hook/build.dart die beschrijft hoe je native code gecompileerd moet worden. Het buildsysteem van Dart regelt dan zelf het compileren, linken en bundelen op alle platformen.

Voor het KDG-project kozen we de handmatige CMake/CocoaPods-aanpak, omdat build hooks nog experimenteel waren toen we begonnen. Voor nieuwe projecten zijn ze de aangewezen weg, als je buildvereisten eenvoudig zijn.

En Rust dan?

Rust is een sterk alternatief voor C bij FFI-werk. Het package flutter_rust_bridge genereert bindings automatisch, regelt geheugenbeheer en geeft je de veiligheidsgaranties van Rust.

Wij kozen C voor het KDG-project omdat de bibliotheken die we inwikkelden al C en C++ zijn. Een laag Rust tussen Dart en die bibliotheken zou onnodige omweg geweest zijn.

Gebruik Rust wanneer je de native code van nul schrijft. De geheugenveiligheid, foutafhandeling en tooling zijn de extra buildcomplexiteit waard. Gebruik C wanneer je een bestaande C-bibliotheek inwikkelt.

Hoe dan ook zorgen ffigen (voor C) en flutter_rust_bridge (voor Rust) ervoor dat je zelden nog met de hand bindingcode schrijft.

Wanneer FFI en wanneer platform channels

FFIPlatform channels
SnelheidSynchroon, zelfde procesAsynchroon, message-passing
ToepassingC/C++/Rust-bibliotheken, performance-kritischPlatform SDK API’s (Swift, Kotlin)
GeheugenManueel (of beheerd door Rust)Automatisch
DesktopEersteklas ondersteuningWerkt, maar minder gangbaar
ComplexiteitHoger (pointers, geheugen)Lager (gecodeerde berichten)

Voor het KDG-project zouden platform channels onwerkbaar geweest zijn. Ruwe pakketdata door de standaard binaire codec serialiseren, over een message channel sturen en aan de andere kant weer decoderen zou latency toegevoegd hebben die we ons niet konden veroorloven, en complexiteit die we niet nodig hadden.

Wat je moet onthouden

Grijp niet standaard naar native code. Dart is snel genoeg voor de meeste dingen. FFI voegt buildcomplexiteit, platformspecifieke code en zorgen rond geheugenbeheer toe.

Heb je het toch nodig, isoleer het dan. Wikkel elke native mogelijkheid in haar eigen Dart package. Houd de FFI-grens klein en getypeerd. Laat ffigen of flutter_rust_bridge de boilerplate genereren.

Gebruik isolates voor blokkerende aanroepen. Roep nooit een blokkerende native functie aan vanuit de hoofdisolate. Het patroon van isolate plus Dart Native API houdt de UI responsief.

Test vroeg op alle doelplatformen. Buildsystemen verschillen tussen Linux, macOS en Windows. Hoe vroeger je verifieert dat je CMake/CocoaPods-setup op alle drie compileert, hoe minder verrassingen bij release.

We hebben productie-Flutter apps opgeleverd met native integraties voor packet capture, realtime veilingsystemen en BLE-hardwareprotocollen. Moet jouw project verder gaan dan wat Dart standaard biedt, dan zijn we daar al geweest.

Verder gaan dan wat Dart standaard biedt?