FFI (Foreign Function Interface) — είναι ένας μηχανισμός της γλώσσας Dart, που παρέχεται από το πακέτο dart:ffi, ο οποίος επιτρέπει την άμεση κλήση συναρτήσεων από εγγενείς βιβλιοθήκες C, χωρίς ενδιάμεσα επίπεδα σε Kotlin, Swift ή Java. Ο προγραμματιστής φορτώνει μια δυναμική βιβλιοθήκη (.so σε Android, .dylib σε iOS, .dll σε Windows), δηλώνει τις υπογραφές των συναρτήσεων C και τις καλεί σαν συνηθισμένες συναρτήσεις Dart. Σύμφωνα με το Dart API Reference (2025), το FFI μειώνει την επιβάρυνση των διαγλωσσικών κλήσεων στα 0.1 μs, που είναι δεκάδες φορές ταχύτερο από ό,τι μέσω του Method Channel.
Κύρια σημεία
FFI (Foreign Function Interface) — είναι ένας μηχανισμός που επιτρέπει σε μια γλώσσα προγραμματισμού να καλεί συναρτήσεις γραμμένες σε άλλες γλώσσες. Στο πλαίσιο των Dart και Flutter, το FFI σημαίνει τη δυνατότητα κλήσης συναρτήσεων από βιβλιοθήκες C/C++ απευθείας από κώδικα Dart, χωρίς να χρειάζεται να γραφτεί κώδικας πλατφόρμας σε Java (Android) ή Swift/Objective-C (iOS).
Το πακέτο dart:ffi εμφανίστηκε στο Dart 2.12 (2021) και από τότε έχει γίνει ένα βασικό εργαλείο για την ενσωμάτωση του Flutter με εγγενή κώδικα. Πριν από την εμφάνιση του dart:ffi, ο μόνος τρόπος για να κληθεί μια συνάρτηση C από τη Dart ήταν το Method Channel — ένας ασύγχρονος μηχανισμός που μετέδιδε μηνύματα μέσω σειριοποίησης JSON μεταξύ Dart και εγγενούς πλευράς. Το FFI λειτουργεί διαφορετικά: ο κώδικας Dart έχει άμεση πρόσβαση στη μνήμη της βιβλιοθήκης C, καλώντας συναρτήσεις μέσω εγγενούς ABI (Application Binary Interface) χωρίς σειριοποίηση και χωρίς εναλλαγή περιβάλλοντος.
Το FFI είναι ιδιαίτερα αναζητούμενο σε σενάρια όπου η απόδοση είναι κρίσιμη: επεξεργασία εικόνας (OpenCV), ήχος (FFmpeg), κρυπτογραφία (OpenSSL), μηχανική μάθηση (TensorFlow Lite) και βάσεις δεδομένων (SQLite). Σε όλες αυτές τις περιπτώσεις, το Method Channel δημιουργεί απαράδεκτες καθυστερήσεις, ενώ το FFI παρέχει απόδοση συγκρίσιμη με τον εγγενή κώδικα C/C++. Η βιβλιοθήκη dart:ffi υποστηρίζει επίσης εργασία με μνήμη: δέσμευση, αποδέσμευση και διαχείριση δεικτών.
Το Method Channel λειτουργεί ασύγχρονα: η Dart στέλνει ένα μήνυμα στον εγγενή κώδικα, ο εγγενής κώδικας το επεξεργάζεται και στέλνει πίσω το αποτέλεσμα. Κάθε κλήση απαιτεί σειριοποίηση των ορισμάτων σε Map, μετάδοση μέσω ουράς και αποσειριοποίηση. Αυτό διαρκεί 0.5–5 ms ανά κλήση. Το FFI λειτουργεί σύγχρονα και χωρίς σειριοποίηση — η κλήση μιας συνάρτησης C διαρκεί 0.01–0.1 μs. Διαφορά 50–500 φορών, που είναι κρίσιμη για λειτουργίες υψηλής συχνότητας.
Η εργασία με το dart:ffi αποτελείται από τρία στάδια: φόρτωση βιβλιοθήκης, δήλωση υπογραφών και κλήση συναρτήσεων. Κάθε στάδιο χρησιμοποιεί την αυστηρή τυποποίηση της Dart, η οποία ελαχιστοποιεί τα σφάλματα κατά το χρόνο εκτέλεσης.
Στο πρώτο στάδιο, η δυναμική βιβλιοθήκη φορτώνεται μέσω της κλάσης DynamicLibrary. Η βιβλιοθήκη μπορεί να φορτωθεί με όνομα (libxyz.so, libxyz.dylib, xyz.dll) ή με πλήρη διαδρομή. Η Dart αναζητά αυτόματα τη βιβλιοθήκη στις τυπικές διαδρομές του συστήματος. DynamicLibrary παρέχει τη μέθοδο lookupFunction, η οποία συνδέει μια συνάρτηση Dart με μια συνάρτηση C βάσει του ονόματος συμβόλου.
Στο δεύτερο στάδιο, δηλώνεται μια συνάρτηση Dart με σχολιασμούς τύπων που αντιστοιχούν στην υπογραφή C. Για αυτό χρησιμοποιούνται ειδικοί τύποι από το dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle και άλλοι. Ο σχολιασμός lookupFunction δέχεται δύο γενικές παραμέτρους: τον τύπο της συνάρτησης Dart (πώς θα φαίνεται στη Dart) και τον τύπο της εγγενούς συνάρτησης C (πώς δηλώνεται στη C).
Στο τρίτο στάδιο, η παραγόμενη συνάρτηση Dart καλείται σαν συνηθισμένη συνάρτηση. Τα ορίσματα μεταβιβάζονται άμεσα, το αποτέλεσμα επιστρέφεται αμέσως. Εάν η συνάρτηση C τροποποιεί τη μνήμη μέσω δεικτών, η Dart μπορεί να διαβάσει αυτές τις αλλαγές μέσω της κλάσης Pointer. Η διαχείριση μνήμης στην πλευρά C παραμένει ευθύνη του προγραμματιστή — το dart:ffi δεν διαχειρίζεται μνήμη που δεσμεύτηκε από malloc στη C.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Δήλωση συνάρτησης 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
}
Σε αυτό το παράδειγμα, η add είναι μια συνάρτηση C που δέχεται δύο int και επιστρέφει int. Το typedef AddNative περιγράφει την υπογραφή C με τύπους dart:ffi, και το AddDart — πώς θα φαίνεται αυτή η συνάρτηση στη Dart. Η lookupFunction τα συνδέει και επιστρέφει μια συνάρτηση Dart που μπορεί να κληθεί σαν συνηθισμένη.
Το dart:ffi παρέχει ένα σύνολο τύπων που αντιστοιχούν σε τύπους C. Κάθε τύπος έχει σταθερό μέγεθος και κανόνες μετατροπής μεταξύ Dart και C. Η κατανόηση της αντιστοιχίας τύπων είναι κρίσιμη για τη σωστή λειτουργία του FFI — ένα λάθος στο μέγεθος ή το πρόσημο ενός τύπου μπορεί να οδηγήσει σε κατάρρευση της εφαρμογής.
| Τύπος C | Τύπος dart:ffi | Τύπος Dart | Μέγεθος (bytes) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (δείκτης) |
| void* | Pointer<Void> | Pointer | 8 (δείκτης) |
| struct | Pointer<T> (Struct) | Pointer | εξαρτάται από πεδία |
Για εργασία με συμβολοσειρές C (char*) το dart:ffi χρησιμοποιεί Pointer<Int8>. Η μετατροπή από Dart String σε C char* και αντίστροφα πραγματοποιείται μέσω των μεθόδων toNativeUtf8 (από το πακέτο ffi) και fromUtf8. Είναι σημαντικό να απελευθερώνονται οι συμβολοσειρές C μετά τη χρήση μέσω calloc.free για την αποφυγή διαρροών μνήμης.
Το dart:ffi υποστηρίζει τη δήλωση δομών C ως κλάσεις Dart που κληρονομούν από το Struct. Τα πεδία της δομής δηλώνονται με σχολιασμούς @Int32(), @Float(), @Array() και άλλους. Το μέγεθος και η μετατόπιση των πεδίων υπολογίζονται αυτόματα σύμφωνα με το ABI της πλατφόρμας. Pointer<Point> μπορεί να ληφθεί από μια συνάρτηση C που επιστρέφει δείκτη σε δομή ή να δεσμευτεί στη Dart μέσω calloc.
// Δομή C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Κλήση συνάρτησης C που επιστρέφει 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) // απελευθέρωση μνήμης
Η κλάση Point κληρονομεί από το Struct και δηλώνει τα πεδία x και y με σχολιασμούς @Int32(). Ο παραγόμενος κώδικας C θα έχει ακριβώς την ίδια διάταξη πεδίων στη μνήμη. Pointer.ref παρέχει πρόσβαση στα πεδία της δομής μέσω getters και setters.
Ας εξετάσουμε ένα πιο σύνθετο παράδειγμα — ενσωμάτωση με μια βιβλιοθήκη C για τον υπολογισμό του κατακερματισμού SHA256. Αυτή είναι μια τυπική εργασία όπου το FFI δίνει σημαντικό πλεονέκτημα απόδοσης σε σύγκριση με το Method Channel.
Η βιβλιοθήκη OpenSSL παρέχει τη συνάρτηση SHA256 που υπολογίζει τον κατακερματισμό μιας συμβολοσειράς. Μέσω του dart:ffi μπορούμε να την καλέσουμε άμεσα, χωρίς να γράψουμε wrappers Java ή Swift. Αυτό είναι ένα παράδειγμα του πώς το FFI επιτρέπει την επαναχρησιμοποίηση υπαρχουσών βιβλιοθηκών C στο Flutter.
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// Υπογραφή: 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 bytes
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
}
Σε αυτό το παράδειγμα, η συνάρτηση sha256 φορτώνει τη βιβλιοθήκη libcrypto.so, βρίσκει το σύμβολο SHA256 και το καλεί με δείκτες στα δεδομένα εισόδου και εξόδου. toNativeUtf8 μετατρέπει το Dart String σε C συμβολοσειρά (δεσμεύει μνήμη), και η asTypedList επιτρέπει την ανάγνωση του πίνακα byte του αποτελέσματος. Η μνήμη απελευθερώνεται μετά τη χρήση — αυτό είναι υποχρεωτικό βήμα για την πρόληψη διαρροών.
Το πακέτο ffi παρέχει τη συνάρτηση calloc για δέσμευση μνήμης συμβατής με C. Η δεσμευμένη μνήμη πρέπει να απελευθερωθεί μέσω calloc.free, διαφορετικά θα προκύψει διαρροή. Για αυτόματη διαχείριση μνήμης μπορεί να χρησιμοποιηθεί η κλάση Arena από το πακέτο ffi, η οποία απελευθερώνει όλη τη μνήμη που δεσμεύτηκε σε αυτήν κατά την κλήση arena.release(). Αυτό είναι ιδιαίτερα βολικό όταν υπάρχει μεγάλος αριθμός προσωρινών δεσμεύσεων.
Παρά τη δύναμη του FFI, έχει περιορισμούς που πρέπει να ληφθούν υπόψη κατά τον σχεδιασμό της αρχιτεκτονικής μιας εφαρμογής Flutter. Οι κύριοι περιορισμοί σχετίζονται με την ασφάλεια τύπων, τη διαχείριση μνήμης και τη συμβατότητα πλατφόρμας.
Το FFI δεν ελέγχει τους τύπους κατά το χρόνο εκτέλεσης. Εάν μια συνάρτηση C αναμένει δείκτη αλλά μεταβιβάζεται ένας αριθμός, η εφαρμογή θα καταρρεύσει με segmentation fault. Συνιστάται η χρήση του FFIgen — ενός εργαλείου που παράγει τύπο-ασφαλή wrappers Dart βάσει κεφαλίδων C (αρχεία .h). FFIgen αναλύει τις δηλώσεις συναρτήσεων C και δημιουργεί κώδικα Dart με σωστούς τύπους, εξαλείφοντας σφάλματα στο στάδιο γραφής κώδικα.
Τα ονόματα και οι διαδρομές προς δυναμικές βιβλιοθήκες διαφέρουν σε διαφορετικές πλατφόρμες: libxyz.so σε Android/Linux, libxyz.dylib σε iOS/macOS, xyz.dll σε Windows. Για διασταυρούμενες πλατφόρμες βιβλιοθήκες χρησιμοποιείται υπό όρους μεταγλώττιση μέσω dart:io (Platform.isAndroid, Platform.isIOS) ή αφαιρέσεις όπως το package:ffi. Συνιστάται η δημιουργία μιας μεθόδου εργοστασίου που επιστρέφει τη σωστή βιβλιοθήκη για την τρέχουσα πλατφόρμα.
Το FFI δεν διαχειρίζεται μνήμη στην πλευρά C. Εάν μια συνάρτηση C δεσμεύει μνήμη μέσω malloc, πρέπει να απελευθερωθεί μέσω free, διαφορετικά θα προκύψει διαρροή. Στη Dart δεν υπάρχει συλλέκτης σκουπιδιών για μνήμη C. Σύσταση: να απελευθερώνετε πάντα τη μνήμη στην ίδια μέθοδο όπου δεσμεύτηκε ή να χρησιμοποιείτε Arena για ομαδική απελευθέρωση.
Οι κλήσεις FFI εκτελούνται στο ίδιο νήμα με τον κώδικα Dart. Οι μεγάλες σύγχρονες λειτουργίες (πάνω από 10 ms) μπλοκάρουν το νήμα UI και προκαλούν πτώση καρέ. Για μεγάλες λειτουργίες, η συνάρτηση C πρέπει να κληθεί σε ένα απομονωτή (Isolate) ή να διασφαλιστεί ότι η συνάρτηση C ξεκινά εργασία σε νήμα παρασκηνίου και ειδοποιεί τη Dart μέσω Port ή callback.
// FFI σε απομονωτή για μεγάλες λειτουργίες
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // Κλήση FFI
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Η μεταφορά μιας κλήσης FFI σε ένα απομονωτή εγγυάται ότι το νήμα UI δεν μπλοκάρεται. Ωστόσο, η μεταφορά μεγάλων όγκων δεδομένων μεταξύ απομονωτών απαιτεί αντιγραφή μνήμης. Για μεγάλα buffer (>10 MB) είναι προτιμότερη η χρήση ShareMemory ή αρχείων αντιστοιχισμένων στη μνήμη.
Συχνές ερωτήσεις
Το FFI καλεί συναρτήσεις C άμεσα, σύγχρονα και χωρίς σειριοποίηση — καθυστέρηση 0.01–0.1 μs. Το Method Channel λειτουργεί ασύγχρονα μέσω σειριοποίησης JSON με καθυστέρηση 0.5–5 ms. Το FFI είναι κατάλληλο για λειτουργίες υψηλής απόδοσης, το Method Channel για απλές κλήσεις API πλατφόρμας.
Άμεσα — όχι, το dart:ffi υποστηρίζει μόνο συναρτήσεις C. Για να κληθεί C++ πρέπει να δημιουργηθεί ένα wrapper C με extern "C" (σημεία εισόδου που εξάγονται ως σύμβολα C). Οι κλάσεις C++ απαιτούν ένα επιπλέον επίπεδο που μετατρέπει τις κλήσεις μεθόδων σε συναρτήσεις C.
Το FFI δεν υποστηρίζει εξαιρέσεις — εάν μια συνάρτηση C επιστρέφει κωδικό σφάλματος, πρέπει να ελεγχθεί χειροκίνητα. Συνιστάται να τυλίγετε τις κλήσεις FFI σε try-catch στη Dart και να ελέγχετε τους κωδικούς επιστροφής των συναρτήσεων C. Κρίσιμα σφάλματα (segfault) δεν μπορούν να πιαστούν.
Το FFI δεν λειτουργεί με βιβλιοθήκες που απαιτούν σύνθετη αρχικοποίηση Java (JNI) ή Objective-C (Message Dispatch). Για παράδειγμα, τα UIKit και Android Views δεν είναι προσβάσιμα μέσω FFI. Ο περιορισμός σχετίζεται με το γεγονός ότι το FFI λειτουργεί σε επίπεδο C ABI, ενώ αυτά τα API απαιτούν συγκεκριμένα περιβάλλοντα εκτέλεσης.
Ναι, οι βιβλιοθήκες C μεταγλωττίζονται ξεχωριστά για κάθε πλατφόρμα-στόχο. Για Android, το .so χτίζεται για διαφορετικά ABI (armeabi-v7a, arm64-v8a, x86_64). Για iOS — ένα καθολικό .dylib (arm64). Για Windows — .dll. Flutter συσκευάζει αυτόματα τη σωστή έκδοση της βιβλιοθήκης κατά τη μεταγλώττιση.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης