لمحة عن هذا الدرس التطبيقي حول الترميز
1. مقدمة
تسمح واجهة برمجة التطبيقات الأجنبية (FFI) في Dart لتطبيقات Flutter باستخدام المكتبات الأصلية الحالية التي توفّر واجهة برمجة تطبيقات C. تتوافق Dart مع واجهة برمجة التطبيقات الأجنبية (FFI) على Android وiOS وWindows وmacOS وLinux. بالنسبة إلى الويب، تتيح Dart إمكانية التشغيل التفاعلي لـ JavaScript، ولكن لم يتم تناول هذا الموضوع في هذا الدليل التعليمي.
ما ستُنشئه
في هذا الدرس البرمجي، يمكنك إنشاء مكوّن إضافي للأجهزة الجوّالة وأجهزة الكمبيوتر المكتبي يستخدم مكتبة C. باستخدام واجهة برمجة التطبيقات هذه، يمكنك كتابة مثال على تطبيق يستخدِم المكوّن الإضافي. سيؤدي المكوّن الإضافي والتطبيق إلى ما يلي:
- استيراد رمز مصدر مكتبة C إلى المكوّن الإضافي الجديد من Flutter
- تخصيص المكوّن الإضافي للسماح بإنشاءه على أنظمة التشغيل Windows وmacOS وLinux وAndroid وiOS
- إنشاء تطبيق يستخدم المكوّن الإضافي REPL (حلقة الطباعة التلقائية) في JavaScript
المُعطيات
في هذا الدليل التعليمي حول الرموز البرمجية، ستتعرّف على المعرفة العملية المطلوبة لإنشاء مكوّن إضافي لتطبيق Flutter يستند إلى واجهة برمجة التطبيقات (FFI) على كلٍّ من منصّات أجهزة الكمبيوتر المكتبي والأجهزة الجوّالة، بما في ذلك:
- إنشاء نموذج مكوّن إضافي لتطبيق Flutter يستند إلى Dart FFI
- استخدام حزمة
ffigen
لإنشاء رمز ربط لمكتبة C - استخدام CMake لإنشاء مكوّن إضافي لواجهة برمجة التطبيقات Flutter FFI لنظام التشغيل Android وWindows وLinux
- استخدام CocoaPods لإنشاء مكوّن إضافي لواجهة برمجة التطبيقات Flutter FFI لنظامَي التشغيل iOS وmacOS
المتطلبات
- Android Studio 4.1 أو إصدار أحدث لتطوير تطبيقات Android
- الإصدار 13 من Xcode أو إصدار أحدث لتطوير التطبيقات على نظامَي التشغيل iOS وmacOS
- Visual Studio 2022 أو Visual Studio Build Tools 2022 مع "تطوير برامج سطح المكتب باستخدام C++" لتطوير برامج سطح المكتب في نظام التشغيل Windows
- حزمة تطوير البرامج (SDK) من Flutter
- أي أدوات إنشاء مطلوبة للأنظمة الأساسية التي سيتم تطويرها عليها (على سبيل المثال، CMake وCocoaPods وما إلى ذلك)
- LLVM للأنظمة الأساسية التي ستُجري عليها عمليات التطوير يستخدم
ffigen
مجموعة أدوات المُجمِّع LLVM لتحليل ملف الرأس C من أجل إنشاء ربط واجهة برمجة التطبيقات (FFI) المعروض في Dart. - محرِّر رموز برمجية، مثل Visual Studio Code
2. الخطوات الأولى
تمّت إضافة أدوات ffigen
مؤخرًا إلى Flutter. يمكنك التأكّد من أنّ عملية تثبيت Flutter تستخدم الإصدار الثابت الحالي من خلال تنفيذ الأمر التالي.
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-AU) [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 16.4) [✓] Chrome - develop for the web [✓] Android Studio (version 2024.2) [✓] IntelliJ IDEA Community Edition (version 2024.3.1.1) [✓] VS Code (version 1.101.0) [✓] Connected device (3 available) [✓] Network resources • No issues found!
تأكَّد من أنّ flutter doctor
يشير إلى أنّك تستخدم القناة الثابتة، وأنّه ما مِن إصدارات أحدث من Flutter ثابتة متاحة. إذا لم تكن تستخدم الإصدار الثابت أو إذا كانت هناك إصدارات أحدث متاحة، يمكنك تنفيذ الأمرَين التاليَين لتحديث أدوات Flutter.
flutter channel stable flutter upgrade
يمكنك تشغيل الرمز البرمجي في هذا الدليل التعليمي باستخدام أيّ من الأجهزة التالية:
- جهاز الكمبيوتر المخصّص للتطوير (لإنشاء إصدارات مخصّصة لأجهزة الكمبيوتر المكتبي من المكوّن الإضافي ومثال التطبيق)
- جهاز Android أو iOS متصل بالكمبيوتر ومفعَّل فيه وضع المطوّر
- محاكي iOS (يتطلب تثبيت أدوات Xcode)
- محاكي Android (يتطلب الإعداد في "استوديو Android")
3. إنشاء نموذج المكوّن الإضافي
بدء تطوير المكوّنات الإضافية في Flutter
تأتي Flutter مزوّدة بنماذج للمكونات الإضافية تسهّل عليك البدء. عند إنشاء نموذج المكوّن الإضافي، يمكنك تحديد اللغة التي تريد استخدامها.
نفِّذ الأمر التالي في دليل العمل لإنشاء مشروعك باستخدام نموذج المكوّن الإضافي:
flutter create --template=plugin_ffi --platforms=android,ios,linux,macos,windows ffigen_app
تحدّد المَعلمة --platforms
الأنظمة الأساسية التي سيتوافق معها المكوّن الإضافي.
يمكنك فحص تنسيق المشروع الذي تم إنشاؤه باستخدام الأمر tree
أو مستكشف الملفات في نظام التشغيل.
$ tree -L 2 ffigen_app ffigen_app ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android │ ├── build.gradle │ ├── ffigen_app_android.iml │ ├── local.properties │ ├── settings.gradle │ └── src ├── example │ ├── README.md │ ├── analysis_options.yaml │ ├── android │ ├── ffigen_app_example.iml │ ├── ios │ ├── lib │ ├── linux │ ├── macos │ ├── pubspec.lock │ ├── pubspec.yaml │ └── windows ├── ffigen.yaml ├── ffigen_app.iml ├── ios │ ├── Classes │ └── ffigen_app.podspec ├── lib │ ├── ffigen_app.dart │ └── ffigen_app_bindings_generated.dart ├── linux │ └── CMakeLists.txt ├── macos │ ├── Classes │ └── ffigen_app.podspec ├── pubspec.lock ├── pubspec.yaml ├── src │ ├── CMakeLists.txt │ ├── ffigen_app.c │ └── ffigen_app.h └── windows └── CMakeLists.txt 17 directories, 26 files
ننصحك بإلقاء نظرة على بنية الدليل لمعرفة ما تم إنشاؤه ومكانه. يضع نموذج plugin_ffi
رمز Dart للمكوّن الإضافي ضمن lib
، وهي أدلة خاصة بالمنصة باسم android
وios
وlinux
وmacos
وwindows
، والأهم من ذلك، دليل example
.
بالنسبة إلى المطوّر المعتاد على تطوير التطبيقات العادية باستخدام Flutter، قد تبدو هذه البنية غريبة، لأنّه لا يتوفّر ملف قابل للتنفيذ محدّد على المستوى الأعلى. يُفترض أن يتم تضمين المكوّن الإضافي في مشاريع Flutter أخرى، ولكن عليك تفصيل الرمز البرمجي في الدليل example
للتأكّد من أنّ رمز المكوّن الإضافي يعمل.
حان وقت البدء.
4. إنشاء المثال وتشغيله
للتأكّد من تثبيت نظام الإنشاء والمتطلبات الأساسية بشكل صحيح وعملهما على كل نظام أساسي متوافق، يمكنك إنشاء نموذج التطبيق الذي تم إنشاؤه وتشغيله لكل وجهة.
Windows
تأكَّد من استخدام إصدار متوافق من نظام التشغيل Windows. يعمل رمز Lab هذا على نظامَي التشغيل Windows 10 وWindows 11.
يمكنك إنشاء التطبيق من داخل محرِّر الرموز البرمجية أو على سطر الأوامر.
PS C:\Users\brett\Documents> cd .\ffigen_app\example\ PS C:\Users\brett\Documents\ffigen_app\example> flutter run -d windows Launching lib\main.dart on Windows in debug mode...Building Windows application... Syncing files to device Windows... 160ms Flutter run key commands. r Hot reload. R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). Running with sound null safety An Observatory debugger and profiler on Windows is available at: http://127.0.0.1:53317/OiKWpyHXxHI=/ The Flutter DevTools debugger and profiler on Windows is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:53317/OiKWpyHXxHI=/
من المفترض أن تظهر لك نافذة تطبيق قيد التشغيل على النحو التالي:
Linux
تأكَّد من استخدام إصدار متوافق من نظام التشغيل Linux. يستخدم هذا الدرس التطبيقي حول الترميز Ubuntu 22.04.1
.
بعد تثبيت جميع المتطلبات الأساسية المُدرَجة في الخطوة 2، نفِّذ الأوامر التالية في وحدة طرفية:
$ cd ffigen_app/example $ flutter run -d linux Launching lib/main.dart on Linux in debug mode... Building Linux application... Syncing files to device Linux... 504ms Flutter run key commands. r Hot reload. 🔥🔥🔥 R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). 💪 Running with sound null safety 💪 An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:36653/Wgek1JGag48=/ The Flutter DevTools debugger and profiler on Linux is available at: http://127.0.0.1:9103?uri=http://127.0.0.1:36653/Wgek1JGag48=/
من المفترض أن تظهر لك نافذة تطبيق قيد التشغيل على النحو التالي:
Android
بالنسبة إلى Android، يمكنك استخدام Windows أو macOS أو Linux للترجمة.
عليك إجراء تغيير على example/android/app/build.gradle.kts
لاستخدام إصدار NDK المناسب.
example/android/app/build.gradle.kts)
android {
// Modify the next line from `flutter.ndkVersion` to the following:
ndkVersion = "27.0.12077973"
// ...
}
تأكَّد من أنّ لديك جهاز Android متصلاً بجهاز الكمبيوتر المخصّص للتطوير أو أنّك تستخدم نسخة من "محاكي Android" (AVD). تأكَّد من أنّ Flutter يمكنه الاتصال بجهاز Android أو المحاكي من خلال تنفيذ ما يلي:
$ flutter devices 3 connected devices: sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 12 (API 32) (emulator) macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
بعد الحصول على جهاز Android يعمل، سواء كان جهازًا فعليًا أو جهاز محاكاة، نفِّذ الأمر التالي:
cd ffigen_app/example flutter run
سيطلب منك Flutter تحديد الجهاز الذي تريد تشغيله عليه. اختَر الجهاز المناسب من بين الأجهزة المدرَجة.
نظاما التشغيل macOS وiOS
لتطوير تطبيقات Flutter على نظامَي التشغيل macOS وiOS، يجب استخدام جهاز كمبيوتر يعمل بنظام التشغيل macOS.
ابدأ بتشغيل مثال التطبيق على نظام التشغيل macOS. أكِّد مرة أخرى الأجهزة التي ترصدها Flutter:
$ flutter devices 2 connected devices: macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
شغِّل مثال التطبيق باستخدام مشروع المكوّن الإضافي الذي تم إنشاؤه:
cd ffigen_app/example flutter run -d macos
من المفترض أن تظهر لك نافذة تطبيق قيد التشغيل على النحو التالي:
بالنسبة إلى أجهزة iOS، يمكنك استخدام المحاكي أو جهاز أجهزة فعلي. إذا كنت تستخدم المحاكي، ابدأ تشغيله أولاً. يُدرج الأمر flutter devices
الآن المحاكي كأحد الأجهزة المتاحة.
$ flutter devices 3 connected devices: iPhone SE (3rd generation) (mobile) • 1BCBE334-7EC4-433A-90FD-1BC14F3BA41F • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-1 (simulator) macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
بعد الحصول على جهاز iOS يعمل، سواء كان جهازًا فعليًا أو جهاز محاكاة، نفِّذ الأمر التالي:
cd ffigen_app/example flutter run
سيطلب منك Flutter تحديد الجهاز الذي تريد تشغيله عليه. اختَر الجهاز المناسب من بين الأجهزة المدرَجة.
يُمنَح محاكي iOS الأولوية على استهداف نظام التشغيل macOS، لذا يمكنك تخطّي تحديد جهاز باستخدام المَعلمة -d
.
تهانينا، لقد نجحت في إنشاء تطبيق وتشغيله على خمسة أنظمة تشغيل مختلفة. بعد ذلك، عليك إنشاء المكوّن الإضافي الأصلي والتفاعل معه من Dart باستخدام واجهة برمجة التطبيقات الأجنبية (FFI).
5. استخدام Duktape على نظام التشغيل Windows وLinux وAndroid
مكتبة C التي ستستخدمها في هذا الدليل التعليمي هي Duktape. Duktape هو محرك JavaScript قابل للتضمين، مع التركيز على قابلية النقل والمساحة الصغيرة التي يشغلها. في هذه الخطوة، عليك ضبط المكوّن الإضافي لتجميع مكتبة Duktape وربطها بالمكوّن الإضافي، ثم الوصول إليها باستخدام واجهة برمجة التطبيقات لنظام التشغيل Dart.
تعمل هذه الخطوة على ضبط عملية الدمج للعمل على نظام التشغيل Windows وLinux وAndroid. يتطلب دمج iOS وmacOS عملية ضبط إضافية (بخلاف ما هو موضّح بالتفصيل في هذه الخطوة) لتضمين المكتبة المجمّعة في ملف Flutter التنفيذي النهائي. يتم تناول الإعدادات الإضافية المطلوبة في الخطوة التالية.
استرداد Duktape
أولاً، احصل على نسخة من رمز المصدر duktape
من خلال تنزيله من الموقع الإلكتروني duktape.org.
بالنسبة إلى نظام التشغيل Windows، يمكنك استخدام PowerShell مع Invoke-WebRequest
:
PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz
بالنسبة إلى نظام التشغيل Linux، يُعدّ wget
خيارًا جيدًا.
$ wget https://duktape.org/duktape-2.7.0.tar.xz --2022-12-22 16:21:39-- https://duktape.org/duktape-2.7.0.tar.xz Resolving duktape.org (duktape.org)... 104.198.14.52 Connecting to duktape.org (duktape.org)|104.198.14.52|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1026524 (1002K) [application/x-xz] Saving to: 'duktape-2.7.0.tar.xz' duktape-2.7.0.tar.x 100%[===================>] 1002K 1.01MB/s in 1.0s 2022-12-22 16:21:41 (1.01 MB/s) - 'duktape-2.7.0.tar.xz' saved [1026524/1026524]
الملف هو أرشيف tar.xz
. على نظام التشغيل Windows، يمكنك تنزيل أدوات 7Zip واستخدامها على النحو التالي.
PS> 7z x .\duktape-2.7.0.tar.xz 7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15 Scanning the drive for archives: 1 file, 1026524 bytes (1003 KiB) Extracting archive: .\duktape-2.7.0.tar.xz -- Path = .\duktape-2.7.0.tar.xz Type = xz Physical Size = 1026524 Method = LZMA2:26 CRC64 Streams = 1 Blocks = 1 Everything is Ok Size: 19087360 Compressed: 1026524
عليك تشغيل 7z مرّتين، المرّة الأولى لاستخراج ضغط xz، والمرة الثانية لتوسيع أرشيف tar.
PS> 7z x .\duktape-2.7.0.tar 7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15 Scanning the drive for archives: 1 file, 19087360 bytes (19 MiB) Extracting archive: .\duktape-2.7.0.tar -- Path = .\duktape-2.7.0.tar Type = tar Physical Size = 19087360 Headers Size = 543232 Code Page = UTF-8 Characteristics = GNU ASCII Everything is Ok Folders: 46 Files: 1004 Size: 18281564 Compressed: 19087360
في بيئات Linux الحديثة، يستخرج tar
المحتوى في خطوة واحدة على النحو التالي.
$ tar xvf duktape-2.7.0.tar.xz x duktape-2.7.0/ x duktape-2.7.0/README.rst x duktape-2.7.0/Makefile.sharedlibrary x duktape-2.7.0/Makefile.coffee x duktape-2.7.0/extras/ x duktape-2.7.0/extras/README.rst x duktape-2.7.0/extras/module-node/ x duktape-2.7.0/extras/module-node/README.rst x duktape-2.7.0/extras/module-node/duk_module_node.h x duktape-2.7.0/extras/module-node/Makefile [... and many more files]
تثبيت LLVM
لاستخدام ffigen
، عليك تثبيت LLVM الذي يستخدمه ffigen
لتحليل رؤوس C. على نظام التشغيل Windows، نفِّذ الأمر التالي.
PS> winget install -e --id LLVM.LLVM Found LLVM [LLVM.LLVM] Version 15.0.5 This application is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages. Downloading https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.5/LLVM-15.0.5-win64.exe ██████████████████████████████ 277 MB / 277 MB Successfully verified installer hash Starting package install... Successfully installed
عليك ضبط مسارات النظام لإضافة C:\Program Files\LLVM\bin
إلى مسار البحث عن الثنائيات لإكمال تثبيت LLVM على جهاز Windows. يمكنك اختبار ما إذا تم تثبيته بشكل صحيح على النحو التالي.
PS> clang --version clang version 15.0.5 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: C:\Program Files\LLVM\bin
بالنسبة إلى نظام التشغيل Ubuntu، يمكن تثبيت التبعية LLVM على النحو التالي. تحتوي توزيعات Linux الأخرى على مهام تابعة مشابهة لـ LLVM وClang.
$ sudo apt install libclang-dev [sudo] password for brett: Reading package lists... Done Building dependency tree... Done Reading state information... Done The following additional packages will be installed: libclang-15-dev The following NEW packages will be installed: libclang-15-dev libclang-dev 0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded. Need to get 26.1 MB of archives. After this operation, 260 MB of additional disk space will be used. Do you want to continue? [Y/n] y Get:1 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-15-dev amd64 1:15.0.2-1 [26.1 MB] Get:2 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-dev amd64 1:15.0-55.1ubuntu1 [2962 B] Fetched 26.1 MB in 7s (3748 kB/s) Selecting previously unselected package libclang-15-dev. (Reading database ... 85898 files and directories currently installed.) Preparing to unpack .../libclang-15-dev_1%3a15.0.2-1_amd64.deb ... Unpacking libclang-15-dev (1:15.0.2-1) ... Selecting previously unselected package libclang-dev. Preparing to unpack .../libclang-dev_1%3a15.0-55.1ubuntu1_amd64.deb ... Unpacking libclang-dev (1:15.0-55.1ubuntu1) ... Setting up libclang-15-dev (1:15.0.2-1) ... Setting up libclang-dev (1:15.0-55.1ubuntu1) ...
كما هو موضّح أعلاه، يمكنك اختبار عملية تثبيت LLVM على Linux باتّباع الخطوات التالية.
$ clang --version Ubuntu clang version 15.0.2-1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
ضبط ffigen
قد يتضمّن النموذج الذي أنشأ pubpsec.yaml
من المستوى الأعلى إصدارات قديمة من حزمة ffigen
. نفِّذ الأمر التالي لتعديل التبعيات في Dart في مشروع المكوّن الإضافي:
flutter pub upgrade --major-versions
بعد تحديث حزمة ffigen
، عليك ضبط الملفات التي ستستخدمها حزمة ffigen
لإنشاء ملفات الربط. عدِّل محتوى ملف ffigen.yaml
في مشروعك ليطابق ما يلي.
ffigen.yaml
# Run with `dart run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
Bindings for `src/duktape.h`.
Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
output: 'lib/duktape_bindings_generated.dart'
headers:
entry-points:
- 'src/duktape.h'
include-directives:
- 'src/duktape.h'
preamble: |
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
comments:
style: any
length: full
ignore-source-errors: true
يتضمّن هذا الإعداد ملف الرأس بتنسيق C الذي يتم تمريره إلى LLVM وملف الإخراج الذي يتم إنشاؤه والوصف الذي يتم وضعه في أعلى الملف وقسم تمهيدي يُستخدَم لإضافة تحذير lint.
هناك عنصر إعداد واحد في نهاية الملف يستحق المزيد من الشرح. اعتبارًا من الإصدار 11.0.0 من ffigen
، لن ينشئ أداة إنشاء عمليات الربط عمليات الربط تلقائيًا في حال حدوث تحذيرات أو أخطاء من إنشاء clang
عند تحليل ملفات الرأس.
تؤدي ملفات عناوين Duktape، كما هي مكتوبة، إلى تنشيط clang
على نظام التشغيل macOS لإنشاء تحذيرات بسبب عدم توفّر محدّدات نوع قابلية العدم في مؤشرات Duktape. لكي يكون Duktape متوافقًا تمامًا مع نظامَي التشغيل macOS وiOS، يجب إضافة محددات الأنواع هذه إلى قاعدة بيانات Duktape. في الوقت الحالي، قرّرنا تجاهل هذه التحذيرات من خلال ضبط العلامة ignore-source-errors
على true
.
في تطبيق الإصدار العلني، يجب إزالة جميع تحذيرات المُجمِّع قبل شحن تطبيقك. ومع ذلك، لا يشمل هذا الدليل التعليمي كيفية إجراء ذلك في Duktape.
اطّلِع على مستندات ffigen
للحصول على مزيد من التفاصيل عن المفاتيح والقِيم الأخرى.
عليك نسخ ملفات Duktape محدّدة من توزيع Duktape إلى الموقع الذي تم ضبط ffigen
للعثور عليها فيه.
cp duktape-2.7.0/src/duktape.c src/ cp duktape-2.7.0/src/duktape.h src/ cp duktape-2.7.0/src/duk_config.h src/
من الناحية الفنية، ما عليك سوى نسخ duktape.h
إلى ffigen
، ولكنك على وشك ضبط CMake لإنشاء المكتبة التي تحتاج إلى الثلاثة. شغِّل ffigen
لإنشاء عملية الربط الجديدة:
$ dart run ffigen --config ffigen.yaml Building package executable... (1.5s) Built ffigen:ffigen. [INFO] : Running in Directory: '/Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05' [INFO] : Input Headers: [file:///Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/src/duktape.h] [WARNING]: No definition found for declaration -(Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread [WARNING]: No definition found for declaration -(Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread [WARNING]: Generated declaration '__builtin_va_list' starts with '_' and therefore will be private. [INFO] : Finished, Bindings generated in /Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/lib/duktape_bindings_generated.dart
ستظهر لك تحذيرات مختلفة على كل نظام تشغيل. يمكنك تجاهل هذه الأخطاء في الوقت الحالي، لأنّه من المعروف أنّه يتم تجميع Duktape 2.7.0 باستخدام clang
على نظام التشغيل Windows وLinux وmacOS.
ضبط CMake
CMake هو نظام لإنشاء نظام الإنشاء. يستخدم هذا المكوّن الإضافي CMake لإنشاء نظام الإنشاء لنظام التشغيل Android وWindows وLinux لتضمين Duktape في ملف Flutter الثنائي الذي تم إنشاؤه. عليك تعديل ملف إعدادات CMake الذي تم إنشاؤه من النموذج على النحو التالي.
src/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(ffigen_app_library VERSION 0.0.1 LANGUAGES C)
add_library(ffigen_app SHARED
duktape.c # Modify
)
set_target_properties(ffigen_app PROPERTIES
PUBLIC_HEADER duktape.h # Modify
PRIVATE_HEADER duk_config.h # Add
OUTPUT_NAME "ffigen_app" # Add
)
# Add from here...
if (WIN32)
set_target_properties(ffigen_app PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif (WIN32)
# ... to here.
target_compile_definitions(ffigen_app PUBLIC DART_SHARED_LIB)
if (ANDROID)
# Support Android 15 16k page size
target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()
تضيف إعدادات CMake ملفات المصدر، والأهم من ذلك، تعدّل السلوك التلقائي لملف المكتبة الذي تم إنشاؤه على نظام التشغيل Windows لتصدير جميع رموز C تلقائيًا. هذه حلاً بديلاً من CMake للمساعدة في نقل المكتبات بأسلوب Unix، مثل Duktape، إلى عالم Windows.
استبدِل محتوى lib/ffigen_app.dart
بما يلي.
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;
import 'duktape_bindings_generated.dart';
const String _libName = 'ffigen_app';
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
return DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
final DuktapeBindings _bindings = DuktapeBindings(_dylib);
class Duktape {
Duktape() {
ctx = _bindings.duk_create_heap(
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
}
void evalString(String jsCode) {
var nativeUtf8 = jsCode.toNativeUtf8();
_bindings.duk_eval_raw(
ctx,
nativeUtf8.cast<Char>(),
0,
0 |
DUK_COMPILE_EVAL |
DUK_COMPILE_SAFE |
DUK_COMPILE_NOSOURCE |
DUK_COMPILE_STRLEN |
DUK_COMPILE_NOFILENAME,
);
ffi.malloc.free(nativeUtf8);
}
int getInt(int index) {
return _bindings.duk_get_int(ctx, index);
}
void dispose() {
_bindings.duk_destroy_heap(ctx);
ctx = nullptr;
}
late Pointer<duk_hthread> ctx;
}
هذا الملف مسؤول عن تحميل ملف مكتبة الروابط الديناميكية (.so
لنظامَي التشغيل Linux وAndroid و.dll
لنظام التشغيل Windows) وتوفير حزمة برمجية تعرض واجهة أكثر ملاءمةً للغة Dart لرمز C الأساسي.
بما أنّ هذا الملف يستورد حزمة ffi
مباشرةً، عليك نقل الحزمة من dev_dependencies
إلى dependencies
. ويمكنك تنفيذ ذلك بسرعة من خلال كتابة الأمر التالي:
dart pub add ffi
استبدِل محتوى main.dart
في المثال بما يلي.
example/lib/main.dart
import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
const String jsCode = '1+2';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Duktape duktape;
String output = '';
@override
void initState() {
super.initState();
duktape = Duktape();
setState(() {
output = 'Initialized Duktape';
});
}
@override
void dispose() {
duktape.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 25);
const spacerSmall = SizedBox(height: 10);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Duktape Test')),
body: Center(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(output, style: textStyle, textAlign: TextAlign.center),
spacerSmall,
ElevatedButton(
child: const Text('Run JavaScript'),
onPressed: () {
duktape.evalString(jsCode);
setState(() {
output = '$jsCode => ${duktape.getInt(-1)}';
});
},
),
],
),
),
),
),
);
}
}
يمكنك الآن تشغيل مثال التطبيق مرة أخرى باستخدام:
cd example flutter run
من المفترض أن يظهر لك التطبيق قيد التشغيل على النحو التالي:
تعرض لقطات الشاشة هذه حالتَي الشاشة قبل الضغط على الزر تشغيل JavaScript وبعده. يوضّح هذا الإجراء تنفيذ رمز JavaScript من Dart وعرض النتيجة على الشاشة.
Android
Android هو نظام تشغيل يستند إلى نواة Linux، وهو يشبه إلى حدٍ ما إصدارات Linux المخصّصة لأجهزة الكمبيوتر المكتبي. يمكن لنظام إنشاء CMake إخفاء معظم الاختلافات بين النظامَين الأساسيَين. لإنشاء التطبيق وتشغيله على Android، تأكَّد من تشغيل محاكي Android (أو اتصال جهاز Android). شغِّل التطبيق، على سبيل المثال:
cd example flutter run -d emulator-5554
من المفترض أن يظهر لك الآن مثال التطبيق قيد التشغيل على Android:
6. استخدام Duktape على نظامَي التشغيل macOS وiOS
لقد حان الوقت الآن لتشغيل المكوّن الإضافي على نظامَي التشغيل macOS وiOS، وهما نظامَان تشغيل مرتبطان ببعضهما. ابدأ باستخدام نظام التشغيل macOS. على الرغم من أنّ CMake متوافق مع نظامَي التشغيل macOS وiOS، لن تتمكّن من إعادة استخدام العمل الذي أجريته على نظامَي التشغيل Linux وAndroid، لأنّ Flutter على نظامَي التشغيل macOS وiOS يستخدم CocoaPods لاستيراد المكتبات.
تنظيف
في الخطوة السابقة، أنشأت تطبيقًا يعمل على Android وWindows وLinux. ومع ذلك، هناك ملفّان متبقّيان من النموذج الأصلي عليك الآن تنظيفهما. يمكنك إزالتها الآن باتّباع الخطوات التالية:
rm src/ffigen_app.c rm src/ffigen_app.h rm ios/Classes/ffigen_app.c rm macos/Classes/ffigen_app.c
نظام التشغيل Mac
يستخدم Flutter على نظام التشغيل macOS حزمة CocoaPods لاستيراد رمز C وC++. وهذا يعني أنّه يجب دمج هذه الحزمة في البنية الأساسية لإنشاء CocoaPods. لتفعيل إعادة استخدام رمز C الذي سبق لك إعداده للإنشاء باستخدام CMake في الخطوة السابقة، ستحتاج إلى إضافة ملف توجيه واحد في أداة تشغيل نظام التشغيل macOS.
macos/Classes/duktape.c
#include "../../src/duktape.c"
يستخدم هذا الملف إمكانات المعالج المُسبَق لـ C لتضمين رمز المصدر من رمز المصدر الأصلي الذي أعددته في الخطوة السابقة. اطّلِع على macos/ffigen_app.podspec لمعرفة مزيد من التفاصيل حول آلية عمل هذا الإجراء.
يتّبع تشغيل هذا التطبيق الآن النمط نفسه الذي رأيته على نظامَي التشغيل Windows وLinux.
cd example flutter run -d macos
iOS
على غرار عملية إعداد نظام التشغيل macOS، يتطلب نظام التشغيل iOS إضافة ملف C واحد للتوجيه أيضًا.
ios/Classes/duktape.c
#include "../../src/duktape.c"
باستخدام هذا الملف الفردي، تم الآن ضبط المكوّن الإضافي أيضًا ليعمل على نظام التشغيل iOS. شغِّل التطبيق كالمعتاد.
flutter run -d iPhone
تهانينا! لقد دمجت بنجاح رمزًا أصليًا على خمس منصات. هذه مناسبة للاحتفال. وربما واجهة مستخدم أكثر وظيفية، والتي ستنشئها في الخطوة التالية.
7. تنفيذ حلقة القراءة والتقييم والطباعة
يكون التفاعل مع لغة برمجة أكثر متعة في بيئة تفاعلية سريعة. كان التنفيذ الأصلي لهذه البيئة هو حلقة Read Eval Print (REPL) في LISP. ستنفِّذ شيئًا مشابهًا باستخدام Duktape في هذه الخطوة.
تجهيز المحتوى للنشر
يفترض الرمز البرمجي الحالي الذي يتفاعل مع مكتبة Duktape C أنّه لا يمكن حدوث أي خطأ. ولا تحمِّل هذه الطريقة مكتبات الروابط الديناميكية Duktape أثناء الاختبار. لجعل عملية الدمج هذه جاهزة للنشر، عليك إجراء بعض التغييرات على lib/ffigen_app.dart
.
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;
import 'package:path/path.dart' as p; // Add this import
import 'duktape_bindings_generated.dart';
const String _libName = 'ffigen_app';
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open(
'build/macos/Build/Products/Debug/$_libName/$_libName.framework/$_libName',
);
}
// To here.
return DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return DynamicLibrary.open(
'build/linux/x64/debug/bundle/lib/lib$_libName.so',
);
}
// To here.
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
// Add from here...
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return switch (Abi.current()) {
Abi.windowsArm64 => DynamicLibrary.open(
p.canonicalize(
p.join(r'build\windows\arm64\runner\Debug', '$_libName.dll'),
),
),
Abi.windowsX64 => DynamicLibrary.open(
p.canonicalize(
p.join(r'build\windows\x64\runner\Debug', '$_libName.dll'),
),
),
_ => throw 'Unsupported platform',
};
}
// To here.
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
final DuktapeBindings _bindings = DuktapeBindings(_dylib);
class Duktape {
Duktape() {
ctx = _bindings.duk_create_heap(
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
}
// Modify this function
String evalString(String jsCode) {
var nativeUtf8 = jsCode.toNativeUtf8();
final evalResult = _bindings.duk_eval_raw(
ctx,
nativeUtf8.cast<Char>(),
0,
0 |
DUK_COMPILE_EVAL |
DUK_COMPILE_SAFE |
DUK_COMPILE_NOSOURCE |
DUK_COMPILE_STRLEN |
DUK_COMPILE_NOFILENAME,
);
ffi.malloc.free(nativeUtf8);
if (evalResult != 0) {
throw _retrieveTopOfStackAsString();
}
return _retrieveTopOfStackAsString();
}
// Add this function
String _retrieveTopOfStackAsString() {
Pointer<Size> outLengthPtr = ffi.calloc<Size>();
final errorStrPtr = _bindings.duk_safe_to_lstring(ctx, -1, outLengthPtr);
final returnVal = errorStrPtr.cast<ffi.Utf8>().toDartString(
length: outLengthPtr.value,
);
ffi.calloc.free(outLengthPtr);
return returnVal;
}
void dispose() {
_bindings.duk_destroy_heap(ctx);
ctx = nullptr;
}
late Pointer<duk_hthread> ctx;
}
يتطلّب ذلك إضافة حزمة path
.
flutter pub add path
تم توسيع الرمز البرمجي لتحميل مكتبة الربط الديناميكي للتعامل مع الحالة التي يتم فيها استخدام المكوّن الإضافي في أداة تشغيل الاختبارات. يتيح ذلك كتابة اختبار دمج يُجري اختبارات على واجهة برمجة التطبيقات هذه كاختبار Flutter. تم توسيع نطاق الرمز لتقييم سلسلة من رموز JavaScript من أجل معالجة حالات الخطأ بشكل صحيح، مثل الرموز غير المكتملة أو غير الصحيحة. توضِّح هذه التعليمات البرمجية الإضافية كيفية التعامل مع الحالات التي يتم فيها عرض السلاسل كصفائف بايت ويجب تحويلها إلى سلاسل Dart.
إضافة حِزم
عند إنشاء بيئة REPL، ستُظهر تفاعلًا بين المستخدم ومحرك JavaScript في Duktape. يُدخِل المستخدم سطور رمز، ويستجيب Duktape إما بنتيجة الحساب أو باستثناء. ستستخدم freezed
لتقليل كمية الرموز البرمجية المتكررة التي تحتاج إلى كتابتها. ستستخدم أيضًا google_fonts
لجعل المحتوى المعروض أكثر ملاءمةً للموضوع، وflutter_riverpod
لإدارة الحالة.
أضِف العناصر التابعة المطلوبة إلى مثال التطبيق:
cd example flutter pub add flutter_riverpod freezed_annotation google_fonts flutter pub add -d build_runner freezed
بعد ذلك، أنشِئ ملفًا لتسجيل تفاعل REPL:
example/lib/duktape_message.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'duktape_message.freezed.dart';
@freezed
class DuktapeMessage with _$DuktapeMessage {
factory DuktapeMessage.evaluate(String code) = DuktapeMessageCode;
factory DuktapeMessage.response(String result) = DuktapeMessageResponse;
factory DuktapeMessage.error(String log) = DuktapeMessageError;
}
تستخدِم هذه الفئة ميزة نوع الائتلاف في freezed
لتفعيل التعبير الآمن من النوع لشكل كل سطر معروض في بيئة REPL كأحد الأنواع الثلاثة. في هذه المرحلة، من المحتمل أن يعرض الرمز بعض أشكال الخطأ، لأنّ هناك رمزًا إضافيًا يجب إنشاؤه. يمكنك إجراء ذلك الآن على النحو التالي.
flutter pub run build_runner build
يؤدي ذلك إلى إنشاء ملف example/lib/duktape_message.freezed.dart
الذي يعتمد عليه الرمز الذي كتبته للتو.
بعد ذلك، عليك إجراء تعديلَين على ملفات ضبط نظام التشغيل macOS لتفعيل google_fonts
لإجراء طلبات على الشبكة للحصول على بيانات الخطوط.
example/macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- ...to here -->
</dict>
</plist>
example/macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- ...to here -->
</dict>
</plist>
إنشاء بيئة REPL
بعد أن عدّلت طبقة الدمج للتعامل مع الأخطاء، وأنشئت تمثيلاً للبيانات للتفاعل، حان الوقت لإنشاء واجهة مستخدم مثالية للتطبيق.
example/lib/main.dart
import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'duktape_message.dart';
void main() {
runApp(const ProviderScope(child: DuktapeApp()));
}
final duktapeMessagesProvider =
StateNotifierProvider<DuktapeMessageNotifier, List<DuktapeMessage>>((ref) {
return DuktapeMessageNotifier(messages: <DuktapeMessage>[]);
});
class DuktapeMessageNotifier extends StateNotifier<List<DuktapeMessage>> {
DuktapeMessageNotifier({required List<DuktapeMessage> messages})
: duktape = Duktape(),
super(messages);
final Duktape duktape;
void eval(String code) {
state = [DuktapeMessage.evaluate(code), ...state];
try {
final response = duktape.evalString(code);
state = [DuktapeMessage.response(response), ...state];
} catch (e) {
state = [DuktapeMessage.error('$e'), ...state];
}
}
}
class DuktapeApp extends StatelessWidget {
const DuktapeApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Duktape App', home: DuktapeRepl());
}
}
class DuktapeRepl extends ConsumerStatefulWidget {
const DuktapeRepl({super.key});
@override
ConsumerState<DuktapeRepl> createState() => _DuktapeReplState();
}
class _DuktapeReplState extends ConsumerState<DuktapeRepl> {
final _controller = TextEditingController();
final _focusNode = FocusNode();
var _isComposing = false;
void _handleSubmitted(String text) {
_controller.clear();
setState(() {
_isComposing = false;
});
setState(() {
ref.read(duktapeMessagesProvider.notifier).eval(text);
});
_focusNode.requestFocus();
}
@override
Widget build(BuildContext context) {
final messages = ref.watch(duktapeMessagesProvider);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Duktape REPL'),
elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0,
),
body: Column(
children: [
Flexible(
child: Ink(
color: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
bottom: false,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (context, idx) {
return switch (messages[idx]) {
DuktapeMessageCode code => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'> ${code.code}',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleMedium,
),
),
),
DuktapeMessageResponse response => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'= ${response.result}',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleMedium,
color: Colors.blue[800],
),
),
),
DuktapeMessageError error => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
error.log,
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleSmall,
color: Colors.red[800],
fontWeight: FontWeight.bold,
),
),
),
DuktapeMessage message => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'Unhandled message $message',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleSmall,
color: Colors.red[800],
fontWeight: FontWeight.bold,
),
),
),
};
},
itemCount: messages.length,
),
),
),
),
const Divider(height: 1.0),
SafeArea(
top: false,
child: Container(
decoration: BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
),
),
],
),
);
}
Widget _buildTextComposer() {
return IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Text('>', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(width: 4),
Flexible(
child: TextField(
controller: _controller,
decoration: const InputDecoration(border: InputBorder.none),
onChanged: (text) {
setState(() {
_isComposing = text.isNotEmpty;
});
},
onSubmitted: _isComposing ? _handleSubmitted : null,
focusNode: _focusNode,
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
icon: const Icon(Icons.send),
onPressed: _isComposing
? () => _handleSubmitted(_controller.text)
: null,
),
),
],
),
),
);
}
}
هناك الكثير من الإجراءات التي يتم تنفيذها في هذه التعليمات البرمجية، ولكنّ شرحها بالكامل لا يندرج ضمن نطاق هذا الدليل التعليمي. نقترح عليك تشغيل الرمز البرمجي، ثم إجراء تعديلات عليه، بعد مراجعة المستندات المناسبة.
cd example flutter run
8. تهانينا
تهانينا! لقد أنشأت بنجاح مكوّنًا إضافيًا مستندًا إلى واجهة برمجة التطبيقات Flutter FFI لأنظمة التشغيل Windows وmacOS وLinux وAndroid وiOS.
بعد إنشاء مكوّن إضافي، قد تحتاج إلى مشاركته على الإنترنت كي يتمكّن الآخرون من استخدامه. يمكنك العثور على المستندات الكاملة حول نشر المكوّن الإضافي على pub.dev في مقالة تطوير حِزم المكوّنات الإضافية.