قنوات المنصة والتكامل الأصلي

Flutter FFI: استدعاء مكتبات C/C++ مباشرة من Dart

16 دقيقة الدرس 10 من 11

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 الخاص بك.