1. مقدمة
تسمح FFI (واجهة الدوال الخارجية) في Dart لتطبيقات Flutter بالاستفادة من المكتبات الأصلية الحالية التي تعرض واجهة C API. يتوافق Dart مع FFI على أنظمة التشغيل Android وiOS وWindows وmacOS وLinux. بالنسبة إلى الويب، يدعم Dart إمكانية التشغيل التفاعلي لـ JavaScript، ولكن هذا الموضوع لا يتناوله هذا الدرس التطبيقي حول الترميز.
ما الذي ستنشئه
في هذا الدرس التطبيقي، يمكنك إنشاء مكوّن إضافي للأجهزة الجوّالة وأجهزة الكمبيوتر المكتبي يستخدم مكتبة C. باستخدام واجهة برمجة التطبيقات هذه، ستكتب كمثال بسيط لتطبيق يستخدم المكون الإضافي. سينفّذ المكوّن الإضافي والتطبيق ما يلي:
- استيراد رمز مصدر مكتبة C إلى المكوّن الإضافي الجديد Flutter
- تخصيص المكوِّن الإضافي للسماح بتصميمه على أنظمة التشغيل Windows وmacOS وLinux وAndroid وiOS
- إنشاء تطبيق يستخدم المكوّن الإضافي لـ JavaScript REPL (read reveal print Loop)
المعلومات التي ستطّلع عليها
في هذا الدرس التطبيقي حول الترميز، ستتعلّم المعرفة العملية المطلوبة لإنشاء مكوّن إضافي على Flutter مستندًا إلى FFI على منصات الكمبيوتر المكتبي والأجهزة الجوّالة، بما في ذلك:
- إنشاء نموذج مكوّن Flutter الإضافي المستند إلى Dart FFI
- استخدام الحزمة
ffigen
لإنشاء رمز ربط لمكتبة C - استخدام أداة CMake لإنشاء مكوّن Flutter FFI الإضافي على أجهزة Android وWindows وLinux.
- استخدام CocoaPods لإنشاء مكوّن إضافي Flutter FFI على نظامَي التشغيل iOS وmacOS
المتطلبات
- الإصدار 4.1 من "استوديو Android" أو إصدار أحدث لتطوير نظام Android
- إصدار Xcode 13 أو إصدار أحدث لتطوير نظامَي التشغيل iOS وmacOS
- Visual Studio 2022 أو Visual Studio Build Tools 2022 مع "تطوير برامج الكمبيوتر باستخدام C++ " عبء العمل لتطوير أجهزة سطح المكتب على نظام التشغيل Windows
- Flutter SDK
- يشير هذا المصطلح إلى أي أدوات تصميم مطلوبة للمنصات التي سيتم تطويرها عليها (مثل 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.3.9, on macOS 13.1 22C65 darwin-arm, locale en) [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 14.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2021.2) [✓] IntelliJ IDEA Community Edition (version 2022.2.2) [✓] VS Code (version 1.74.0) [✓] Connected device (2 available) [✓] HTTP Host Availability • 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. من المعروف أنّ هذا الدرس التطبيقي حول الترميز يعمل على نظامَي التشغيل 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 للتجميع. أولاً، تأكّد من أنّ لديك جهاز 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
نظام التشغيل macOS وiOS
بالنسبة إلى تطوير البرامج التي تعمل بنظامَي التشغيل macOS وiOS Flutter، يجب استخدام جهاز كمبيوتر يعمل بنظام التشغيل 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
بعد بدء المحاكي، شغِّل: flutter run
.
$ cd ffigen_app/example $ flutter run -d iphone
يكون لمحاكي iOS الأولوية على استهداف نظام التشغيل macOS، لذا يمكنك تخطّي تحديد جهاز باستخدام المَعلمة -d
.
تهانينا، لقد أنشأت بنجاح تطبيقًا وقمت بتشغيله على خمسة أنظمة تشغيل مختلفة. بعد ذلك، إنشاء المكوّن الإضافي الأصلي والتفاعل معه من Dart باستخدام FFI.
5- استخدام Duktape على أنظمة التشغيل Windows وLinux وAndroid
مكتبة C التي ستستخدمها في هذا الدرس التطبيقي حول الترميز هي Duktape. Duktape هو محرّك JavaScript قابل للتضمين يركّز على إمكانية النقل وبصمته الصغيرة. في هذه الخطوة، يجب إعداد المكوّن الإضافي لتجميع مكتبة Duktape وربطها بالمكوّن الإضافي، ثم الوصول إليها باستخدام القيمة المالية الأساسية Dart's FFI.
تضبط هذه الخطوة عملية الدمج للعمل على أنظمة التشغيل 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 `flutter pub run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
Bindings for `src/duktape.h`.
Regenerate bindings with `flutter pub 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. للتوافق بشكل كامل مع نظامَي التشغيل macOS وiOS Duktape، يجب إضافة محدِّدات الأنواع هذه إلى قاعدة رموز 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
لإنشاء الربط الجديد:
$ flutter pub run ffigen --config ffigen.yaml Running in Directory: '/home/brett/GitHub/codelabs/ffigen_codelab/step_05' Input Headers: [./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 '__va_list_tag' start's with '_' and therefore will be private. Finished, Bindings generated in /home/brett/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)
تضيف إعدادات CMake الملفات المصدر، والأهم من ذلك، تعدّل السلوك التلقائي لملف المكتبة الذي تم إنشاؤه على Windows لتصدير جميع رموز C بشكل تلقائي. تهدف هذه الأداة إلى تطوير مكتبات بتصميم يونكس (Unix) بهدف مساعدتها في تطوير نظام التشغيل Windows في عالم نظام التشغيل 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
نظام التشغيل macOS
يستخدم 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
يعد التفاعل مع لغة برمجة أكثر متعة في بيئة تفاعلية سريعة. وقد تم التنفيذ الأصلي لمثل هذه البيئة من خلال تقرير Read Eval Print Loop (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 DynamicLibrary.open(p.canonicalize(
p.join(r'build\windows\runner\Debug', '$_libName.dll')));
}
// ...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;
}
تم توسيع التعليمات البرمجية لتحميل مكتبة الروابط الديناميكية للتعامل مع الحالة التي يتم فيها استخدام المكوّن الإضافي في برنامج تشغيل اختبار. ويتيح ذلك كتابة اختبار دمج يمارس واجهة برمجة التطبيقات هذه كاختبار Flutter. تم توسيع الرمز المخصّص لتقييم سلسلة من رمز JavaScript بحيث يعالج حالات الخطأ بشكل صحيح، على سبيل المثال الرمز غير المكتمل أو غير الصحيح. يوضح هذا الرمز الإضافي كيفية التعامل مع المواقف التي يتم فيها عرض السلاسل كصفائف بايت وتحتاج إلى تحويلها إلى سلاسل Dart.
إضافة الحِزم
عند إنشاء REPL، ستعرِض تفاعلاً بين المستخدم ومحرّك JavaScript Duktape. يدخل المستخدم سطورًا من التعليمات البرمجية، ويستجيب Duktape إما بنتيجة العملية الحسابية أو استثناء. ستستخدم freezed
لتقليل مقدار الرمز النموذجي الذي تحتاج إلى كتابته. ويمكنك أيضًا استخدام السمة google_fonts
لجعل المحتوى المعروض مرتبطًا بشكل أكبر بالمظهر، وflutter_riverpod
لإدارة الحالة.
أضف التبعيات المطلوبة إلى التطبيق كمثال:
$ cd example $ dart pub add flutter_riverpod freezed_annotation google_fonts $ dart 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) => messages[idx].when(
evaluate: (str) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'> $str',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleMedium,
),
),
),
response: (str) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
'= $str',
style: GoogleFonts.firaCode(
textStyle: Theme.of(context).textTheme.titleMedium,
color: Colors.blue[800],
),
),
),
error: (str) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
str,
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 في قسم تطوير حزم المكوّنات الإضافية.