FFI (Foreign Function Interface) — е механизъм на езика Dart, предоставен от пакета dart:ffi, който позволява директно извикване на функции от native 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 с native код. Преди появата на dart:ffi, единственият начин за извикване на C функция от Dart беше Method Channel — асинхронен механизъм, който предаваше съобщения чрез JSON сериализация между Dart и native страната. FFI работи различно: Dart кодът директно достъпва паметта на C библиотеката, извиквайки функции чрез native ABI (Application Binary Interface) без сериализация и без превключване на контекст.
FFI е особено търсен в сценарии, където производителността е критична: обработка на изображения (OpenCV), аудио (FFmpeg), криптография (OpenSSL), машинно обучение (TensorFlow Lite) и бази данни (SQLite). Във всички тези случаи Method Channel създава неприемливи закъснения, докато FFI осигурява производителност, сравнима с native C/C++ код. Библиотеката dart:ffi също поддържа работа с памет: заделяне, освобождаване и управление на указатели.
Method Channel работи асинхронно: Dart изпраща съобщение до native кода, native кодът го обработва и изпраща резултата обратно. Всяко извикване изисква сериализация на аргументите в 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) и типът на native 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 предоставя достъп до полетата на структурата чрез getter и setter.
Нека разгледаме по-сложен пример — интеграция с 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 функция очаква указател, а се подаде число, приложението ще се срине със segmentation fault. Препоръчва се използването на 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 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 нишката не се блокира. Въпреки това, прехвърлянето на големи обеми данни между изолати изисква копиране на памет. За големи буфери (>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++ трябва да се създаде 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също