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

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

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

FFI срещу Method Channel: принципна разлика

Method Channel работи асинхронно: Dart изпраща съобщение до native кода, native кодът го обработва и изпраща резултата обратно. Всяко извикване изисква сериализация на аргументите в Map, предаване през опашка и десериализация. Това отнема 0.5–5 ms на извикване. FFI работи синхронно и без сериализация — извикването на C функция отнема 0.01–0.1 μs. Разлика от 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) и типът на native C функцията (как е декларирана в C).

На третия етап генерираната Dart функция се извиква като обикновена функция. Аргументите се предават директно, резултатът се връща незабавно. Ако C функцията променя паметта чрез указатели, Dart може да чете тези промени чрез класа Pointer. Управлението на паметта от страна на C остава отговорност на разработчика — dart:ffi не управлява памет, заделена чрез malloc в C.

Основен пример: извикване на C функция от Dart

dart
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 функция, която може да бъде извиквана като обикновена.

Типове данни в 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<Int8>. Конвертирането от 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 структура: 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.

Практически примери за 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'

// Сигнатура: 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.

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

FFI извиква C функции директно, синхронно и без сериализация — закъснение 0.01–0.1 μs. Method Channel работи асинхронно чрез JSON сериализация със закъснение 0.5–5 ms. 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: няма проверка на типове по време на изпълнение, няма директна поддръжка на C++, блокира UI нишка
  • Използвайте FFI за високопроизводителна интеграция с native библиотеки във Flutter

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също