שימוש ב-FFI בפלאגין של Flutter

1. מבוא

FFI (ממשק פונקציות זרות) של Drt מאפשר לאפליקציות ל-Flutter להשתמש בספריות מקוריות קיימות שחושפות ממשק C API. Dat תומך ב-FFI ב-Android, ב-iOS, ב-Windows, ב-macOS וב-Linux. אפליקציית Dart תומכת ב-JavaScript יכולת פעולה הדדית באינטרנט, אבל הנושא הזה לא נכלל ב-Codelab הזה.

מה תפַתחו

ב-Codelab הזה, אתם מפתחים פלאגין לנייד ולמחשב שמשתמש בספריית C. באמצעות ה-API הזה אפשר לכתוב אפליקציה פשוטה לדוגמה שמשתמשת בפלאגין. הפלאגין והאפליקציה:

  • מייבאים את קוד המקור של ספריית C לפלאגין החדש של Flutter
  • ניתן להתאים אישית את הפלאגין כדי לאפשר לו לבנות אותו ב-Windows, ב-macOS, ב-Linux, ב-Android וב-iOS
  • פיתוח אפליקציה שמשתמשת בפלאגין ליצירת REPL של JavaScript (קריאה ללולאת הדפסה

Duktape REPL פועלת כאפליקציית macOS

מה תלמדו

ב-Codelab הזה תלמדו את הידע המעשי שנדרש כדי לפתח פלאגין Flutter מבוסס FFI גם בפלטפורמות למחשבים וגם במכשירים ניידים, כולל:

  • יצירת תבנית פלאגין של Flutter מבוססת Dart FFI
  • שימוש בחבילת ffigen ליצירת קוד קישור לספריית C
  • שימוש ב-CMake כדי לבנות פלאגין של Flutter FFI ל-Android, ל-Windows ול-Linux
  • שימוש ב-CocoaPods לפיתוח פלאגין של Flutter FFI ל-iOS ול-macOS

מה צריך להכין

  • Android Studio 4.1 ואילך לפיתוח של Android
  • Xcode 13 ואילך לפיתוח ב-iOS ו-macOS
  • Visual Studio 2022 או Visual Studio Build Tools 2022 עם 'פיתוח למחשב עם C++ '. עומס עבודה (workload) לפיתוח שולחן עבודה של Windows
  • Flutter SDK
  • כלי ה-build שנדרשים לפלטפורמות שבהן תתפתחו (לדוגמה, CMake, CocoaPods וכו').
  • LLVM לפלטפורמות שאותן אתם מפתחים. חבילת הכלים של המהדר (compiler) LLVM משמשת את ffigen לניתוח קובץ הכותרת של 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

אפשר להריץ את הקוד ב-Codelab הזה באמצעות כל אחד מהמכשירים הבאים:

  • מחשב הפיתוח (לגרסאות build של הפלאגין ואפליקציה לדוגמה)
  • מכשיר Android או iOS פיזי שמחובר למחשב ומוגדר למצב פיתוח
  • הסימולטור של iOS (צריך להתקין כלי Xcode)
  • אמולטור Android (נדרשת הגדרה ב-Android Studio)

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 ממקמת את קוד Dat של הפלאגין ב-lib, בספריות ספציפיות לפלטפורמה בשם android, ios, linux, macos ו-windows, והכי חשוב, ספרייה example.

עבור מפתח שמשמש להתפתחות רגילה של Flutter, המבנה הזה עשוי להיראות מוזר, כי לא הוגדר קובץ הפעלה ברמה העליונה. פלאגין אמור להיכלל בפרויקטים אחרים של Flutter, אבל עליך להזין את הקוד בספרייה example כדי לוודא שקוד הפלאגין שלך פועל.

הגיע הזמן להתחיל!

4. בנייה והרצה של הדוגמה

כדי לוודא שמערכת ה-build והדרישות המוקדמות מותקנות כראוי ועובדות בכל פלטפורמה נתמכת, צריך ליצור ולהפעיל את האפליקציה לדוגמה שנוצרה עבור כל יעד.

Windows

חשוב לוודא שאתם משתמשים בגרסה נתמכת של Windows. ה-Codelab הזה ידוע שהוא פועל ב-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=/

אמור להופיע חלון של אפליקציה פועלת כמו:

אפליקציית FFI שנוצרה באמצעות תבנית ופועלת כאפליקציה ל-Windows

Linux

מוודאים שמשתמשים בגרסה נתמכת של Linux. ב-Codelab הזה נעשה שימוש ב-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 Emulator (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

אפליקציית FFI שנוצרה באמצעות תבנית ופועלת באמולטור 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

אפליקציית FFI שנוצרה באמצעות תבנית פועלת בסימולטור iOS

הסימולטור של iOS מקבל עדיפות על פני היעד ב-macOS, כך שאפשר לדלג על ציון מכשיר עם הפרמטר -d.

ברכותינו, יצרת בהצלחה אפליקציה והפעלה שלה בחמש מערכות הפעלה שונות. בשלב הבא, פיתוח הפלאגין המקורי והתממשקות איתו מ-Dart באמצעות FFI.

5. שימוש ב-Duktape ב-Windows, ב-Linux וב-Android

ספריית C שבה תשתמשו ב-Codelab הזה היא Duktape. Duktape הוא מנוע JavaScript שניתן להטמעה, שמתמקד בניידות ובטביעת רגל קומפקטית. בשלב הזה מגדירים את הפלאגין כדי להדר את ספריית Duktape, לקשר אותו לפלאגין, ואז לגשת אליו באמצעות ה-FFI של Drt.

השלב הזה מגדיר את השילוב כך שיפעל ב-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. מריצים את הפקודה הבאה כדי לעדכן את יחסי התלות של Drt בפרויקט הפלאגין:

$ 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, את קובץ הפלט שצריך ליצור, את התיאור שצריך להוסיף בראש הקובץ ואת קטע ההקדמה שמשמש להוספת אזהרה לגבי שגיאות בקוד.

יש פריט תצורה אחד בסוף הקובץ שדורש הסבר נוסף. החל מגרסה 11.0.0 של ffigen, מחולל הקישורים לא ייצור קישורים אם יש אזהרות או שגיאות שנוצרות על ידי clang במהלך ניתוח קובצי הכותרת.

כפי שכתבתם את קובצי הכותרות של Duktape, הם מפעילים את הפקודה clang ב-macOS כדי ליצור אזהרות כי אין בסמנים של Duktape מציינים את סוג ה-null. כדי לתמוך באופן מלא ב-macOS וב-iOS Duktape, צריך להוסיף את המפרטים האלה ל-codebase של Duktape. בינתיים, אנחנו מחליטים להתעלם מהאזהרות האלה. לשם כך, מגדירים את הדגל ignore-source-errors לערך true.

באפליקציית ייצור עליכם להסיר את כל אזהרות המהדר לפני שליחת האפליקציה. אבל האפשרות הזו בשביל Duktape לא נכללת במסגרת ה-Codelab הזה.

פרטים נוספים על המפתחות והערכים האחרים זמינים במסמכי התיעוד של 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 היא מערכת יצירה של מערכת build. הפלאגין הזה משתמש ב-CMake כדי ליצור את מערכת ה-build ל-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 כברירת מחדל. זוהי סביבת 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) ובאספקת wrapper שחושף ממשק אידיומטי יותר של Drt לקוד 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 במחשב. מערכת ה-build של CMake יכולה להסתיר את רוב ההבדלים בין שתי הפלטפורמות. כדי ליצור ולהפעיל מערכת Android, צריך לוודא שאמולטור Android פועל (או שמכשיר Android מחובר). מפעילים את האפליקציה. מוצרים לדוגמה:

$ cd example
$ flutter run -d emulator-5554

עכשיו אמורה להופיע האפליקציה לדוגמה שפועלת ב-Android:

הצגת Duktape שאותחל באמולטור Android

הצגת הפלט של Duktape JavaScript באמולטור 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++. המשמעות היא שצריך לשלב את החבילה הזו בתשתית ה-build של 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 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;
}

הקוד לטעינה של ספריית הקישורים הדינמיים הורחב כדי לטפל במקרה שבו נעשה שימוש בפלאגין בהרצת בדיקה. כך ניתן לכתוב בדיקת אינטגרציה שמבצעת את ה-API הזה כבדיקת Flutter. הקוד המשמש להערכת מחרוזת של קוד JavaScript הוארך כדי לטפל כראוי בתנאי שגיאות, כגון קוד חלקי או שגוי. הקוד הנוסף הזה מראה איך לטפל במצבים שבהם מחרוזות מוחזרות כמערכי בייטים וצריך להמיר אותן למחרוזות Dart.

הוספת חבילות

כשיוצרים REPL, מציגים אינטראקציה בין המשתמש לבין מנוע Duktape JavaScript. המשתמש מזין שורות קוד, ו-Diktape משיב על התוצאה של החישוב או על חריג. צריך להשתמש ב-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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

בקוד הזה קורים הרבה דברים, אבל הוא חורג מההיקף של ה-Codelab הזה כדי להסביר את הכול. מומלץ להריץ את הקוד, ולאחר מכן לבצע שינויים בקוד, לאחר עיון בתיעוד המתאים.

$ cd example
$ flutter run

Duktape REPL פועלת באפליקציית Linux

Duktape REPL פועלת באפליקציה ל-Windows

Duktape REPL פועלת בסימולטור של iOS

Duktape REPL פועלת באמולטור Android

8. מזל טוב

מעולה! יצרת בהצלחה פלאגין מבוסס-FFI של Flutter ל-Windows, ל-macOS, ל-Linux, ל-Android ול-iOS!

אחרי שיוצרים פלאגין, כדאי לשתף אותו באינטרנט כדי שאחרים יוכלו להשתמש בו. אפשר למצוא את התיעוד המלא על פרסום הפלאגין ב-pub.dev בקטע פיתוח חבילות פלאגין.