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 мкс, что в десятки раз быстрее, чем через 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 мкс. Разница в 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 function declaration: 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
dart:ffi поддерживает объявление C-структур как Dart-классов, наследующих Struct. Поля структуры объявляются с аннотациями @Int32(), @Float(), @Array() и другими. Размер и смещение полей вычисляются автоматически в соответствии с ABI платформы. Pointer<Point> может быть получен из C-функции, возвращающей указатель на структуру, или выделен в Dart через calloc.
// C struct: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// Calling C function that returns 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) // free memory
Класс 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'
// Signature: 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 позволяет читать массив байт результата. Память освобождается после использования — это обязательный шаг для предотвращения утечек.
Пакет 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 мс) блокируют UI-поток и вызывают пропуск кадров. Для длительных операций следует вызывать C-функцию в изоляте (Isolate) или убедиться, что C-функция запускает работу в фоновом потоке и уведомляет Dart через Port или callback.
// FFI in isolate for long operations
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // FFI call
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
Вынос FFI-вызова в изолят гарантирует, что UI-поток не блокируется. Однако стоит учитывать, что передача больших объёмов данных между изолятами требует копирования памяти. Для больших буферов (>10 МБ) предпочтительнее использовать ShareMemory или Memory-mapped files.
Часто задаваемые вопросы
FFI вызывает C-функции напрямую, синхронно и без сериализации — задержка 0.01–0.1 мкс. 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 года. Мы проконсультируем вас и предложим наилучшее решение.