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: 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 به char* C و بالعکس از طریق متدهای toNativeUtf8 (از بسته ffi) و fromUtf8 انجام میشود. آزادسازی رشتههای C پس از استفاده از طریق calloc.free برای جلوگیری از نشت حافظه ضروری است.
dart:ffi از اعلام ساختارهای C به عنوان کلاسهای Dart که از Struct ارث میبرند پشتیبانی میکند. فیلدهای ساختار با حاشیهنویسیهای @Int32()، @Float()، @Array() و دیگران اعلام میشوند. اندازه و offset فیلدها به طور خودکار مطابق با 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 میتوانیم آن را مستقیماً و بدون نوشتن wrapperهای 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 استفاده کنید — ابزاری که 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 اطلاع میدهد.
// 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 توابع C را مستقیماً، همزمان و بدون سریالسازی فراخوانی میکند — تأخیر 0.01–0.1 میکروثانیه. Method Channel به صورت ناهمزمان از طریق سریالسازی JSON با تأخیر 0.5–5 میلیثانیه کار میکند. FFI برای عملیاتهای با عملکرد بالا مناسب است، Method Channel برای فراخوانیهای ساده API پلتفرم.
مستقیماً — خیر، dart:ffi فقط از توابع C پشتیبانی میکند. برای فراخوانی C++ باید یک wrapper 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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.