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 50–100 برابر بیشتر از Method Channel است
  • نوع‌بندی 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 در مقابل 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: 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 به char* C و بالعکس از طریق متدهای toNativeUtf8 (از بسته ffi) و fromUtf8 انجام می‌شود. آزادسازی رشته‌های C پس از استفاده از طریق calloc.free برای جلوگیری از نشت حافظه ضروری است.

ساختارها (Struct)

dart:ffi از اعلام ساختارهای C به عنوان کلاس‌های Dart که از Struct ارث می‌برند پشتیبانی می‌کند. فیلدهای ساختار با حاشیه‌نویسی‌های @Int32()، @Float()، @Array() و دیگران اعلام می‌شوند. اندازه و offset فیلدها به طور خودکار مطابق با 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 می‌توانیم آن را مستقیماً و بدون نوشتن wrapperهای 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 استفاده کنید — ابزاری که wrapperهای 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 کار را در نخ پس‌زمینه اجرا کرده و از طریق Port یا callback به Dart اطلاع می‌دهد.

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 مگابایت) استفاده از ShareMemory یا فایل‌های نگاشت شده در حافظه ترجیح داده می‌شود.

سوالات متداول

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++ باید یک wrapper 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 برای یکپارچه‌سازی با عملکرد بالا با کتابخانه‌های بومی در Flutter استفاده کنید

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید