Flutter FFI: استدعاء مكتبات C/C++ مباشرة من Dart
Flutter FFI: استدعاء مكتبات C/C++ مباشرة من Dart
dart:ffi (واجهة الدوال الأجنبية - Foreign Function Interface) تتيح لكود Dart استدعاء دوال C وC++ الأصيلة دون المرور عبر قناة المنصة. بينما تضيف قناة المنصة زمن استجابة غير متزامن وتستلزم كتابة كود مضيف بـ Kotlin/Swift لكل منصة، فإن FFI متزامنة، وذات أداء شبه فوري، وتعمل على Android وiOS وLinux وmacOS وWindows من خلال ربط Dart واحد.
تُعدّ FFI الأداة المثلى عندما تحتاج إلى أداء خام (معالجة صوت DSP، برمجيات ترميز الصور، التشفير)، أو عند توافر مكتبة C موثوقة (OpenSSL، SQLite، zlib)، أو حين تريد مشاركة تنفيذ C/C++ واحد عبر جميع المنصات دون كتابة خمسة أغلفة إضافية.
dart:ffi مكتبة أساسية ضمن Dart SDK — لا حاجة لأي حزمة إضافية من pub.dev. وهي متاحة في Flutter وبرامج Dart المستقلة على حدٍّ سواء.آلية عمل FFI على المستوى العالي
تتكون عملية العمل من ثلاث خطوات:
- التحويل البرمجي: حوّل كود C/C++ إلى مكتبة مشتركة (
.soعلى Android/Linux،.dylibعلى macOS/iOS،.dllعلى Windows). - التحميل: حمّل المكتبة أثناء التشغيل باستخدام
DynamicLibrary.open()أوDynamicLibrary.process(). - الربط: اربط كل دالة C بزوج من تعريفات النوع (typedef) في Dart: أحدهما لتوقيع النوع الأصيل بـ C، والآخر لتوقيع الدالة القابلة للاستدعاء في Dart.
الأنواع الأصيلة ومقابلاتها في Dart
يُعيَّن كل نوع C إلى نوع أصيل مناظر في dart:ffi. أشهر التعيينات:
int(C 32-بت) ←Int32/int(Dart)long(C 64-بت) ←Int64/int(Dart)double(C) ←Double/double(Dart)float(C) ←Float/double(Dart)void*/ المؤشرات ←Pointer<T>char*للنصوص ←Pointer<Utf8>(منpackage:ffi)struct← فئة فرعية تمتد منStruct
package:ffi (المنشورة من فريق Dart) تضيف مساعدات مثل Pointer<Utf8>.toDartString()، ومخصصات malloc/calloc، ودالة using() لإدارة الذاكرة تلقائياً. أضفها بالأمر flutter pub add ffi.مثال 1 — استدعاء دالة رياضية بسيطة بلغة C
افترض أن لديك مكتبة C صغيرة بدالة تُعيد مربع عدد صحيح:
ترويسة C الأصيلة (native_math.h) والتنفيذ
// native_math.h
int32_t square(int32_t value);
// native_math.c
#include "native_math.h"
int32_t square(int32_t value) { return value * value; }
قم بتحويله إلى مكتبة مشتركة (مثلاً libnative_math.so)، ضعها في مشروع Flutter، ثم اربطها في Dart:
ربط FFI في Dart لدالة square()
import 'dart:ffi';
import 'dart:io' show Platform;
// الخطوة 1 — typedef لتوقيع C الأصيل
typedef SquareNative = Int32 Function(Int32 value);
// الخطوة 2 — typedef لتوقيع Dart القابل للاستدعاء
typedef SquareDart = int Function(int value);
// الخطوة 3 — تحميل المكتبة المشتركة
final DynamicLibrary _nativeLib = Platform.isAndroid
? DynamicLibrary.open('libnative_math.so')
: DynamicLibrary.process(); // macOS/iOS: مرتبطة بالعملية
// الخطوة 4 — البحث عن الرمز وتحويله
final SquareDart square = _nativeLib
.lookup<NativeFunction<SquareNative>>('square')
.asFunction<SquareDart>();
void main() {
print(square(7)); // يطبع 49 — متزامن، بدون تأخير
}
مثال 2 — التعامل مع البنيات والمؤشرات في C
للبيانات الأكثر تعقيداً، تُعيَّن البنيات C إلى فئات Dart تمتد من Struct. تُعلَن الحقول بالتوضيحات @Int32() و@Double() وغيرها.
تعيين بنية Dart واستخدام المؤشرات
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// تعيين: typedef struct { int32_t x; int32_t y; } Point2D;
final class Point2D extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
// دالة C: int32_t distance_squared(Point2D* a, Point2D* b);
typedef DistanceNative =
Int32 Function(Pointer<Point2D> a, Pointer<Point2D> b);
typedef DistanceDart =
int Function(Pointer<Point2D> a, Pointer<Point2D> b);
final distanceSquared = _nativeLib
.lookup<NativeFunction<DistanceNative>>('distance_squared')
.asFunction<DistanceDart>();
void computeDistance() {
// تخصيص بنيتين Point2D على الكومة الأصيلة
final a = calloc<Point2D>();
final b = calloc<Point2D>();
a.ref.x = 0; a.ref.y = 0;
b.ref.x = 3; b.ref.y = 4;
final d2 = distanceSquared(a, b); // يُعيد 25
print('تربيع المسافة: $d2');
// تحرير الذاكرة الأصيلة دائماً
calloc.free(a);
calloc.free(b);
}
calloc أو malloc لا يُديرها مُجمِّع القمامة في Dart. يجب استدعاء calloc.free(ptr) عند الانتهاء. إهمال تحرير الذاكرة الأصيلة يُسبّب تسرباً لا يستطيع محلل VM في Dart اكتشافه. استخدم مساعد using() من package:ffi لتأطير التخصيصات تلقائياً.الـ Isolates وسلامة الخيوط مع FFI
استدعاءات FFI المتزامنة تحجب الـ isolate المُستدعي. إذا كانت دالة C بطيئة (مثل معالجة صور ثقيلة)، استدعِها من isolate خلفية باستخدام Isolate.run() أو compute() لإبقاء خيط واجهة المستخدم مستجيباً. للعمل الأصيل الذي يستغرق وقتاً طويلاً، يمكن لكود C إنشاء خيوط نظام تشغيل خاصة به — شريطة ألا تستدعي Dart من تلك الخيوط مباشرةً دون استخدام Native API (Dart_PostCObject_DL).
تضمين المكتبة المشتركة في Flutter
يجب تضمين المكتبة المشتركة مع التطبيق:
- Android: ضع ملفات
.soفيandroid/app/src/main/jniLibs/<ABI>/(مثلarm64-v8a،x86_64). تلتقطها أداة Gradle في Flutter تلقائياً. - iOS/macOS: أضف
.dylibأو.xcframeworkإلى "Frameworks, Libraries, and Embedded Content" في هدف Xcode. - Linux/Windows/سطح المكتب: ضع
.so/.dllبجانب الملف التنفيذي أو في مسار يُعيدهDynamicLibrary.open().
FFI مقابل قنوات المنصة: متى تختار FFI
استخدم FFI عندما: تمتلك مكتبة C/C++ خالصة، تحتاج استدعاءات متزامنة، أو تريد ربطاً واحداً بـ Dart لجميع المنصات.
استخدم قنوات المنصة عندما: تحتاج استدعاء واجهات برمجية على مستوى النظام (الكاميرا، البلوتوث، الإشعارات) التي لا تُكشَف إلا عبر حزم تطوير المنصة، أو حين تحتاج تشغيل كود Kotlin/Swift.
dart:ffi استدعاءات مباشرة ومتزامنة لكود C/C++ المُصرَّف من Dart. تحمّل المكتبة بـ DynamicLibrary، تُعلن typedef للأنواع الأصيلة، تبحث عن الرموز، وتستدعيها كدوال Dart اعتيادية. أدِر ذاكرة الكومة الأصيلة يدوياً، احتفظ بالاستدعاءات الطويلة خارج isolate واجهة المستخدم، وادمج .so/.dylib/.dll المُصرَّف مع تطبيق Flutter الخاص بك.