استخدام FFI في مكوّن Flutter الإضافي

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)

تطبيق Duktape REPL على نظام التشغيل macOS

المعلومات التي ستطّلع عليها

في هذا الدرس التطبيقي حول الترميز، ستتعلّم المعرفة العملية المطلوبة لإنشاء مكوّن إضافي على 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=/

من المفترض أن تظهر نافذة تطبيق قيد التشغيل على النحو التالي:

تطبيق مؤسسة مالية أجنبية تم إنشاؤه باستخدام نموذج ويتم تشغيله كتطبيق Windows

نظام التشغيل 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=/

من المفترض أن تظهر نافذة تطبيق قيد التشغيل على النحو التالي:

تطبيق FFI تم إنشاؤه بواسطة نموذج ويتم تشغيله كتطبيق Linux

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

تطبيق مالي تم إنشاؤه باستخدام نموذج ويتم تشغيله في محاكي Android

نظام التشغيل 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

من المفترض أن تظهر نافذة تطبيق قيد التشغيل على النحو التالي:

تطبيق FFI تم إنشاؤه بواسطة نموذج ويتم تشغيله كتطبيق Linux

بالنسبة إلى 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

يكون لمحاكي 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

من المفترض أن يعمل التطبيق على النحو التالي:

عرض إعداد Duktape في تطبيق Windows

عرض مخرجات JavaScript من Duktape في تطبيق على نظام التشغيل Windows

تعرض هاتان لقطتا الشاشة قبل الضغط على الزر تشغيل JavaScript وبعده. يوضح هذا تنفيذ رمز JavaScript من Dart وعرض النتيجة على الشاشة.

Android

Android هو نظام تشغيل Linux قائم على النواة ويشبه إلى حد ما توزيعات Linux على أجهزة سطح المكتب. يمكن لنظام إنشاء CMake إخفاء معظم الاختلافات بين المنصّتين. لإنشاء إصدار وتشغيله على نظام التشغيل Android، تأكَّد من أنّ محاكي Android يعمل (أو تم توصيل جهاز Android). شغِّل التطبيق. مثل:

$ cd example
$ flutter run -d emulator-5554

من المفترض أن يظهر لك الآن نموذج التطبيق الذي يعمل على نظام التشغيل Android:

عرض إعدادات Duktape في محاكي Android

عرض ناتج JavaScript من Duktape في محاكي 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

عرض إعداد Duktape في تطبيق macOS

عرض ناتج JavaScript من Duktape في تطبيق على نظام التشغيل macOS

iOS

على غرار إعداد نظام التشغيل macOS، يتطلب iOS إضافة ملف C واحد لإعادة التوجيه أيضًا.

ios/Classes/duktape.c

#include "../../src/duktape.c"

من خلال هذا الملف الفردي، يتم الآن إعداد المكوّن الإضافي للتشغيل على نظام التشغيل iOS. شغِّل التطبيق كالمعتاد.

$ flutter run -d iPhone

عرض إعداد Duktape في محاكي iOS

عرض مخرجات JavaScript من Duktape في محاكي iOS

تهانينا! لقد دمجت الرمز الأصلي بنجاح في خمسة أنظمة أساسية. هذا سبب للاحتفال! ربما حتى واجهة مستخدم أكثر فعالية، والتي ستنشئها في الخطوة التالية.

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

تشغيل فيديو Duktape REPL على أحد تطبيقات Linux

تشغيل Duktape REPL على أحد تطبيقات Windows

تشغيل لعبة Duktape REPL على محاكي iOS

تشغيل لعبة Duktape REPL على محاكي Android

8. تهانينا

تهانينا! لقد أنشأت بنجاح مكوّنًا إضافيًا مستندًا إلى Flutter FFI على أنظمة التشغيل Windows وmacOS وLinux وAndroid وiOS.

بعد إنشاء مكوّن إضافي، قد ترغب في مشاركته على الإنترنت حتى يتمكن الآخرون من استخدامه. يمكنك العثور على الوثائق الكاملة حول نشر المكوّن الإضافي على pub.dev في قسم تطوير حزم المكوّنات الإضافية.