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 мс на виклик. 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 тип | Розмір (байт) |
|---|---|---|---|
| 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 надає доступ до полів структури через гетери та сетери.
Розглянемо більш складний приклад — інтеграцію з C-бібліотекою для обчислення хешу SHA256. Це типове завдання, де FFI дає значний виграш у продуктивності порівняно з Method Channel.
Бібліотека OpenSSL надає функцію SHA256, яка обчислює хеш рядка. Через dart:ffi ми можемо викликати її безпосередньо, без написання 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 байти
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 дозволяє читати масив байт результату. Пам'ять звільняється після використання — це обов'язковий крок для запобігання витокам.
Пакет ffi надає функцію calloc для виділення пам'яті, сумісної з C. Виділена пам'ять має бути звільнена через calloc.free, інакше відбудеться витік. Для автоматичного управління пам'яттю можна використовувати клас Arena з пакету ffi, який звільняє всю виділену в ньому пам'ять при виклику arena.release(). Це особливо зручно при великій кількості тимчасових виділень.
Незважаючи на потужність FFI, у нього є обмеження, які необхідно враховувати при проектуванні архітектури Flutter-додатку. Основні обмеження пов'язані з безпекою типів, управлінням пам'яттю та сумісністю платформ.
FFI не перевіряє типи під час виконання. Якщо C-функція очікує вказівник, а передається число, додаток впаде з помилкою сегментації. Рекомендується використовувати FFIgen — інструмент, який генерує типобезпечні 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 мс) блокують 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-потік не блокується. Однак варто враховувати, що передача великих обсягів даних між ізолятами потребує копіювання пам'яті. Для великих буферів (>10 МБ) краще використовувати SharedMemory або Memory-mapped files.
Поширені запитання
FFI викликає C-функції безпосередньо, синхронно і без серіалізації — затримка 0,01–0,1 µs. Method Channel працює асинхронно через JSON-серіалізацію з затримкою 0,5–5 мс. FFI підходить для високопродуктивних операцій, Method Channel — для простих викликів платформених API.
Безпосередньо — ні, dart:ffi підтримує тільки C-функції. Для виклику C++ потрібно створити 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.