FFI (واجهة الوظائف الخارجية) هي آلية من لغة Dart، توفرها الحزمة dart:ffi، التي تسمح باستدعاء الدوال من مكتبات C الأصلية مباشرة، دون طبقات وسيطة في Kotlin أو Swift أو Java. يقوم المطور بتحميل مكتبة ديناميكية (.so على Android، .dylib على iOS، .dll على Windows)، ويُصرّح بتوقيعات دوال C ويستدعيها كدوال Dart عادية. وفقًا لمرجع Dart API (2025)، FFI يقلل الحمل الزائد للاستدعاءات بين اللغات إلى 0.1 ميكروثانية، وهو أسرع بعشرات المرات من Method Channel.
الرئيسية
FFI (واجهة الوظائف الخارجية) هي آلية تسمح للغة البرمجة باستدعاء دوال مكتوبة بلغات أخرى. في سياق 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 الأصلي (واجهة ثنائية التطبيق) دون تسلسل أو تبديل سياق.
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 تأخذ عددين صحيحين وتعيد عدداً صحيحاً. 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 إلى C char* والعكس عبر toNativeUtf8 (من حزمة ffi) و fromUtf8. من المهم تحرير سلاسل C بعد الاستخدام عبر calloc.free لتجنب تسرب الذاكرة.
يدعم dart:ffi إعلان هياكل C كفئات Dart ترث من Struct. تُعلن حقول الهيكل بتعليقات @Int32() و@Float() و@Array() وغيرها. يتم حساب حجم وإزاحة الحقول تلقائياً وفقاً لـ 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 يوفر الوصول إلى حقول الهيكل عبر getters و setters.
لننظر إلى مثال أكثر تعقيداً — التكامل مع مكتبة C لحساب تجزئة SHA256. هذه مهمة نموذجية حيث يوفر FFI مكسباً كبيراً في الأداء مقارنة بـ Method Channel.
مكتبة OpenSSL توفر دالة SHA256، التي تحسب تجزئة سلسلة نصية. عبر dart:ffi، يمكننا استدعاؤها مباشرة، دون كتابة أغلفة 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 تتوقع مؤشراً ولكنها تستقبل رقماً، سيتعطل التطبيق مع خطأ تجزئة. يُوصى باستخدام 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 مللي ثانية) تحظر خيط واجهة المستخدم وتسبب فقدان الإطارات. للعمليات الطويلة، يجب استدعاء دالة C في Isolate أو التأكد من أن دالة C تشغل العمل في خيط خلفية وتُعلم Dart عبر Port أو callback.
// FFI في isolate للعمليات الطويلة
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 إلى isolate يضمن عدم حظر خيط واجهة المستخدم. ومع ذلك، يجب مراعاة أن نقل كميات كبيرة من البيانات بين isolates يتطلب نسخ الذاكرة. للمخازن المؤقتة الكبيرة (>10 MB)، يُفضل استخدام SharedMemory أو الملفات المعينة في الذاكرة.
الأسئلة الشائعة
FFI يستدعي دوال C مباشرة، بشكل متزامن ودون تسلسل — زمن الوصول 0.01–0.1 ميكروثانية. Method Channel يعمل بشكل غير متزامن عبر تسلسل JSON بزمن وصول 0.5–5 مللي ثانية. FFI مناسب للعمليات عالية الأداء، Method Channel — للاستدعاءات البسيطة لواجهات برمجة المنصة.
مباشرة — لا، 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 يعمل على مستوى ABI لـ C، بينما تتطلب هذه الواجهات بيئات تشغيل محددة.
نعم، تُجمع مكتبات C بشكل منفصل لكل منصة مستهدفة. لنظام Android، يُبنى .so لـ ABI مختلفة (armeabi-v7a، arm64-v8a، x86_64). لنظام iOS — .dylib عالمي (arm64). لنظام Windows — .dll. Flutter يحزم تلقائياً الإصدار الصحيح من المكتبة أثناء البناء.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.