FFI: основы, Foreign Function Interface и интеграция с C

Автор: IT Sectr Опубликовано: 2026-06-05 Время чтения: 10 мин

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) — механизм вызова C-функций из Dart напрямую
  • dart:ffi предоставляет API для загрузки библиотек и объявления сигнатур
  • Производительность вызовов через FFI выше, чем через Method Channel в 50–100 раз
  • Типизация FFI поддерживает примитивные типы, структуры и указатели C
  • Flutter использует FFI для интеграции с нативными библиотеками: OpenCV, SQLite, FFmpeg

Что такое FFI?

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 также поддерживает работу с памятью: выделение, освобождение и управление указателями.

FFI vs Method Channel: принципиальная разница

Method Channel работает асинхронно: Dart отправляет сообщение в нативный код, нативный код обрабатывает его и отправляет результат обратно. Каждый вызов требует сериализации аргументов в Map, передачи через очередь и десериализации. Это занимает 0.5–5 мс на вызов. FFI работает синхронно и без сериализации — вызов C-функции занимает 0.01–0.1 мкс. Разница в 50–500 раз, что критично для высокочастотных операций.

Как работает dart:ffi?

Работа с 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.

Базовый пример: вызов C-функции из Dart

dart
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-функцию, которую можно вызывать как обычную.

Типы данных в FFI

dart:ffi предоставляет набор типов, соответствующих типам C. Каждый тип имеет фиксированный размер и правила преобразования между Dart и C. Понимание соответствия типов критически важно для корректной работы FFI — ошибка в размере или знаке типа может привести к падению приложения.

C типdart:ffi типDart типРазмер (байт)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (указатель)
void*Pointer<Void>Pointer8 (указатель)
structPointer<T> (Struct)Pointerзависит от полей

Для работы со строками C (char*) dart:ffi использует Pointer. Преобразование из Dart String в C char* и обратно выполняется через методы toNativeUtf8 (из пакета ffi) и fromUtf8. Важно освобождать строки C после использования через calloc.free, чтобы избежать утечек памяти.

Структуры (Struct)

dart:ffi поддерживает объявление C-структур как Dart-классов, наследующих Struct. Поля структуры объявляются с аннотациями @Int32(), @Float(), @Array() и другими. Размер и смещение полей вычисляются автоматически в соответствии с ABI платформы. Pointer<Point> может быть получен из C-функции, возвращающей указатель на структуру, или выделен в Dart через calloc.

dart
// 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 предоставляет доступ к полям структуры через геттеры и сеттеры.

Практические примеры FFI

Рассмотрим более сложный пример — интеграцию с C-библиотекой для вычисления хеша SHA256. Это типичная задача, где FFI даёт значительный выигрыш в производительности по сравнению с Method Channel.

Интеграция с OpenSSL через FFI

Библиотека OpenSSL предоставляет функцию SHA256, которая вычисляет хеш строки. Через dart:ffi мы можем вызвать её напрямую, без написания Java или Swift обёрток. Это пример того, как FFI позволяет переиспользовать существующие C-библиотеки в Flutter.

dart
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(). Это особенно удобно при большом количестве временных выделений.

Ограничения и best practices

Несмотря на мощность 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.

dart
// 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 отличается от Method Channel?

FFI вызывает C-функции напрямую, синхронно и без сериализации — задержка 0.01–0.1 мкс. Method Channel работает асинхронно через JSON-сериализацию с задержкой 0.5–5 мс. FFI подходит для высокопроизводительных операций, Method Channel — для простых вызовов платформенных API.

Можно ли вызывать C++ функции через FFI?

Напрямую — нет, dart:ffi поддерживает только C-функции. Для вызова C++ нужно создать C-обёртку с extern "C" (точки входа, которые экспортируются как C-символы). C++ классы требуют дополнительной прослойки, которая преобразует вызовы методов в C-функции.

Как обрабатывать ошибки в C-функциях?

FFI не поддерживает исключения — если C-функция возвращает код ошибки, его нужно проверять вручную. Рекомендуется оборачивать FFI-вызовы в try-catch в Dart и проверять возвращаемые коды C-функций. Критические ошибки (segfault) поймать невозможно.

Какие библиотеки нельзя использовать через FFI?

FFI не работает с библиотеками, требующими сложной инициализации Java (JNI) или Objective-C (Message Dispatch). Например, UIKit и Android Views недоступны через FFI. Ограничение связано с тем, что FFI работает на уровне C ABI, а эти API требуют специфических рантаймов.

Нужно ли компилировать C-библиотеки для каждой платформы?

Да, C-библиотеки компилируются для каждой целевой платформы отдельно. Для Android собирается .so под разные ABI (armeabi-v7a, arm64-v8a, x86_64). Для iOS — универсальный .dylib (arm64). Для Windows — .dll. Flutter автоматически упаковывает правильную версию библиотеки при сборке.

Итоги

  • FFI (Foreign Function Interface) — механизм прямого вызова C-функций из Dart через dart:ffi
  • Производительность вызовов FFI в 50–500 раз выше, чем через Method Channel
  • Архитектура включает загрузку библиотеки, объявление сигнатур и вызов функций
  • Типы данных dart:ffi поддерживают Int32, Float, Double, Pointer, Struct и другие C-типы
  • Память C-стороны управляется вручную через calloc/free или Arena
  • Ограничения FFI: нет проверки типов runtime, нет поддержки C++ напрямую, блокирует UI-поток
  • Используйте FFI для высокопроизводительной интеграции с нативными библиотеками в Flutter

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также