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

1. מבוא

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

מה תפַתחו

ב-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++‎' לפיתוח למחשב עם Windows
  • Flutter SDK
  • כל כלי ה-build הנדרשים לפלטפורמות שבהן יתבצע הפיתוח (לדוגמה, CMake,‏ CocoaPods וכו').
  • LLVM לפלטפורמות שבהן יתבצע הפיתוח. ffigen משתמש ב-LLVM compiler tool suite כדי לנתח את קובץ הכותרת של C ולבנות את קישור ה-FFI שחשוף ב-Dart.
  • עורך קוד, כמו Visual Studio Code.

2. תחילת העבודה

כלי ffigen נוספו לאחרונה ל-Flutter. כדי לוודא שגרסת Flutter המותקנת היא הגרסה היציבה הנוכחית, מריצים את הפקודה הבאה.

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-AU)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] IntelliJ IDEA Community Edition (version 2024.3.1.1)
[✓] VS Code (version 1.101.0)
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!

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

flutter channel stable
flutter upgrade

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

  • מחשב הפיתוח (לגרסת build למחשב של הפלאגין והאפליקציה לדוגמה)
  • מכשיר פיזי עם Android או iOS שמחובר למחשב ומוגדר למצב פיתוח
  • הסימולטור של iOS (נדרשת התקנה של כלי Xcode)
  • Android Emulator (נדרשת הגדרה ב-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 ממפה את קוד Dart של הפלאגין לספריות lib, ספריות ספציפיות לפלטפורמה בשמות android,‏ ios,‏ linux,‏ macos ו-windows, והכי חשוב, לספרייה example.

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

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

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

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

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=/

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

אפליקציית 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 לצורך הידור.

כדי להשתמש בגרסה המתאימה של NDK, צריך לבצע שינוי ב-example/android/app/build.gradle.kts.

example/android/app/build.gradle.kts)

android {
    // Modify the next line from `flutter.ndkVersion` to the following:
    ndkVersion = "27.0.12077973"
    // ...
}

מוודאים שמכשיר 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

אחרי שמפעילים מכשיר Android, פיזי או במכונה וירטואלית, מריצים את הפקודה הבאה:

cd ffigen_app/example
flutter run

ב-Flutter תתבקשו לבחור את המכשיר שבו תרצו להריץ אותו. בוחרים את המכשיר המתאים מהמכשירים שמופיעים ברשימה.

אפליקציית 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

אחרי שמפעילים מכשיר iOS, פיזי או סימולטור, מריצים את הפקודה הבאה:

cd ffigen_app/example
flutter run

ב-Flutter תתבקשו לבחור את המכשיר שבו תרצו להריץ אותו. בוחרים את המכשיר המתאים מהמכשירים שמופיעים ברשימה.

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

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

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

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

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

בשלב הזה מגדירים את השילוב כך שיפעל ב-Windows, ב-Linux וב-Android. השילוב של iOS ו-macOS מחייב הגדרה נוספת (מעבר למה שמפורט בשלב הזה) כדי לכלול את הספרייה המתומצת בקובץ ההפעלה הסופי של Flutter. ההגדרות הנדרשות הנוספות מפורטות בשלב הבא.

אחזור Duktape

קודם צריך לקבל עותק של קוד המקור של duktape על ידי הורדה מהאתר duktape.org.

ב-Windows אפשר להשתמש ב-PowerShell עם Invoke-WebRequest:

PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz

ב-Linux, wget היא בחירה טובה.

$ wget https://duktape.org/duktape-2.7.0.tar.xz
--2022-12-22 16:21:39--  https://duktape.org/duktape-2.7.0.tar.xz
Resolving duktape.org (duktape.org)... 104.198.14.52
Connecting to duktape.org (duktape.org)|104.198.14.52|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1026524 (1002K) [application/x-xz]
Saving to: 'duktape-2.7.0.tar.xz'

duktape-2.7.0.tar.x 100%[===================>]   1002K  1.01MB/s    in 1.0s

2022-12-22 16:21:41 (1.01 MB/s) - 'duktape-2.7.0.tar.xz' saved [1026524/1026524]

הקובץ הוא ארכיון tar.xz. ב-Windows, אפשר להוריד את הכלים של 7Zip ולהשתמש בהם באופן הבא.

PS> 7z x .\duktape-2.7.0.tar.xz

7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15

Scanning the drive for archives:
1 file, 1026524 bytes (1003 KiB)

Extracting archive: .\duktape-2.7.0.tar.xz
--
Path = .\duktape-2.7.0.tar.xz
Type = xz
Physical Size = 1026524
Method = LZMA2:26 CRC64
Streams = 1
Blocks = 1

Everything is Ok

Size:       19087360
Compressed: 1026524

צריך להריץ את 7z פעמיים, בפעם הראשונה כדי לחלץ את דחיסת ה-xz, ובפעם השנייה כדי להרחיב את הארכיון של ה-tar.

PS> 7z x .\duktape-2.7.0.tar

7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15

Scanning the drive for archives:
1 file, 19087360 bytes (19 MiB)

Extracting archive: .\duktape-2.7.0.tar
--
Path = .\duktape-2.7.0.tar
Type = tar
Physical Size = 19087360
Headers Size = 543232
Code Page = UTF-8
Characteristics = GNU ASCII

Everything is Ok

Folders: 46
Files: 1004
Size:       18281564
Compressed: 19087360

בסביבות Linux מודרניות, tar מחלץ את התוכן בשלב אחד באופן הבא.

$ tar xvf duktape-2.7.0.tar.xz
x duktape-2.7.0/
x duktape-2.7.0/README.rst
x duktape-2.7.0/Makefile.sharedlibrary
x duktape-2.7.0/Makefile.coffee
x duktape-2.7.0/extras/
x duktape-2.7.0/extras/README.rst
x duktape-2.7.0/extras/module-node/
x duktape-2.7.0/extras/module-node/README.rst
x duktape-2.7.0/extras/module-node/duk_module_node.h
x duktape-2.7.0/extras/module-node/Makefile
[... and many more files]

התקנת LLVM

כדי להשתמש ב-ffigen, צריך להתקין את LLVM, ש-ffigen משתמש בו כדי לנתח כותרות C. ב-Windows, מריצים את הפקודה הבאה.

PS> winget install -e --id LLVM.LLVM
Found LLVM [LLVM.LLVM] Version 15.0.5
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Downloading https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.5/LLVM-15.0.5-win64.exe
  ██████████████████████████████   277 MB /  277 MB
Successfully verified installer hash
Starting package install...
Successfully installed

כדי להשלים את התקנת LLVM במחשב Windows, צריך להגדיר את נתיבי המערכת כך שיכללו את C:\Program Files\LLVM\bin בנתיב החיפוש הבינארי. אפשר לבדוק אם הוא הוטמע בצורה נכונה באופן הבא.

PS> clang --version
clang version 15.0.5
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin

ב-Ubuntu, אפשר להתקין את התלות ב-LLVM באופן הבא. למהדורות אחרות של Linux יש יחסי תלות דומים ל-LLVM ול-Clang.

$ sudo apt install libclang-dev
[sudo] password for brett:
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  libclang-15-dev
The following NEW packages will be installed:
  libclang-15-dev libclang-dev
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Need to get 26.1 MB of archives.
After this operation, 260 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-15-dev amd64 1:15.0.2-1 [26.1 MB]
Get:2 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-dev amd64 1:15.0-55.1ubuntu1 [2962 B]
Fetched 26.1 MB in 7s (3748 kB/s)
Selecting previously unselected package libclang-15-dev.
(Reading database ... 85898 files and directories currently installed.)
Preparing to unpack .../libclang-15-dev_1%3a15.0.2-1_amd64.deb ...
Unpacking libclang-15-dev (1:15.0.2-1) ...
Selecting previously unselected package libclang-dev.
Preparing to unpack .../libclang-dev_1%3a15.0-55.1ubuntu1_amd64.deb ...
Unpacking libclang-dev (1:15.0-55.1ubuntu1) ...
Setting up libclang-15-dev (1:15.0.2-1) ...
Setting up libclang-dev (1:15.0-55.1ubuntu1) ...

כמו למעלה, אפשר לבדוק את התקנת LLVM ב-Linux באופן הבא.

$ clang --version
Ubuntu clang version 15.0.2-1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

הגדרה של ffigen

יכול להיות שב-pubpsec.yaml ברמה העליונה שנוצרה מהתבנית יהיו גרסאות מיושנות של חבילת ffigen. מריצים את הפקודה הבאה כדי לעדכן את יחסי התלות ב-Dart בפרויקט הפלאגין:

flutter pub upgrade --major-versions

עכשיו, כשחבילת ffigen מעודכנת, צריך להגדיר אילו קבצים ffigen ישתמש בהם כדי ליצור את קובצי הקישור. משנים את התוכן של הקובץ ffigen.yaml של הפרויקט כך שיתאים לתוכן הבא.

ffigen.yaml

# Run with `dart run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
  Bindings for `src/duktape.h`.

  Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
output: 'lib/duktape_bindings_generated.dart'
headers:
  entry-points:
    - 'src/duktape.h'
  include-directives:
    - 'src/duktape.h'
preamble: |
  // ignore_for_file: always_specify_types
  // ignore_for_file: camel_case_types
  // ignore_for_file: non_constant_identifier_names
comments:
  style: any
  length: full
ignore-source-errors: true

ההגדרה הזו כוללת את קובץ הכותרת של C שצריך להעביר ל-LLVM, את קובץ הפלט שצריך ליצור, את התיאור שצריך להוסיף בחלק העליון של הקובץ ואת קטע המבוא שמשמש להוספת אזהרת איתור שגיאות בקוד.

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

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

באפליקציה בסביבת הייצור, צריך להסיר את כל האזהרות של המהדר לפני ששולחים את האפליקציה. עם זאת, ביצוע הפעולה הזו עבור Duktape לא נכלל בהיקף של סדנת הקוד הזו.

פרטים נוספים על שאר המפתחות והערכים זמינים במסמכי התיעוד של ffigen.

צריך להעתיק קבצים ספציפיים של Duktape מההפצה של Duktape למיקום שבו ffigen מוגדר למצוא אותם.

cp duktape-2.7.0/src/duktape.c src/
cp duktape-2.7.0/src/duktape.h src/
cp duktape-2.7.0/src/duk_config.h src/

מבחינה טכנית, צריך להעתיק רק את duktape.h ל-ffigen, אבל אתם עומדים להגדיר את CMake כדי ליצור את הספרייה שצריכה את כל השלושה. מריצים את ffigen כדי ליצור את הקישור החדש:

$ dart run ffigen --config ffigen.yaml
Building package executable... (1.5s)
Built ffigen:ffigen.
[INFO]   : Running in Directory: '/Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05'
[INFO]   : Input Headers: [file:///Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/src/duktape.h]
[WARNING]: No definition found for declaration -(Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread
[WARNING]: No definition found for declaration -(Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread
[WARNING]: Generated declaration '__builtin_va_list' starts with '_' and therefore will be private.
[INFO]   : Finished, Bindings generated in /Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/lib/duktape_bindings_generated.dart

בכל מערכת הפעלה יוצגו אזהרות שונות. אפשר להתעלם מהן בינתיים, כי ידוע ש-Duktape 2.7.0 מקמפל עם clang ב-Windows, ב-Linux וב-macOS.

הגדרת CMake

CMake היא מערכת ליצירת מערכת 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)

if (ANDROID)
  # Support Android 15 16k page size
  target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()

קובץ התצורה של CMake מוסיף את קובצי המקור, וחשוב יותר, משנה את התנהגות ברירת המחדל של קובץ הספרייה שנוצר ב-Windows כך שייצא את כל הסמלים של C כברירת מחדל. זוהי דרך לעקוף את CMake כדי לעזור בהעברת ספריות בסגנון Unix, כמו Duktape, לעולם Windows.

מחליפים את התוכן של lib/ffigen_app.dart בקוד הבא.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;

import 'duktape_bindings_generated.dart';

const String _libName = 'ffigen_app';

final DynamicLibrary _dylib = () {
  if (Platform.isMacOS || Platform.isIOS) {
    return DynamicLibrary.open('$_libName.framework/$_libName');
  }
  if (Platform.isAndroid || Platform.isLinux) {
    return DynamicLibrary.open('lib$_libName.so');
  }
  if (Platform.isWindows) {
    return DynamicLibrary.open('$_libName.dll');
  }
  throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final DuktapeBindings _bindings = DuktapeBindings(_dylib);

class Duktape {
  Duktape() {
    ctx = _bindings.duk_create_heap(
      nullptr,
      nullptr,
      nullptr,
      nullptr,
      nullptr,
    );
  }

  void evalString(String jsCode) {
    var nativeUtf8 = jsCode.toNativeUtf8();
    _bindings.duk_eval_raw(
      ctx,
      nativeUtf8.cast<Char>(),
      0,
      0 |
          DUK_COMPILE_EVAL |
          DUK_COMPILE_SAFE |
          DUK_COMPILE_NOSOURCE |
          DUK_COMPILE_STRLEN |
          DUK_COMPILE_NOFILENAME,
    );
    ffi.malloc.free(nativeUtf8);
  }

  int getInt(int index) {
    return _bindings.duk_get_int(ctx, index);
  }

  void dispose() {
    _bindings.duk_destroy_heap(ctx);
    ctx = nullptr;
  }

  late Pointer<duk_hthread> ctx;
}

הקובץ הזה אחראי לטעינת קובץ ספריית הקישור הדינמי (.so ל-Linux ול-Android, .dll ל-Windows) ולמתן מעטפת שמציגה ממשק דינמי יותר של Dart לקוד C הבסיסי.

מכיוון שהקובץ הזה מייבא ישירות את החבילה ffi, צריך להעביר את החבילה מ-dev_dependencies אל dependencies. דרך מהירה לעשות זאת היא להריץ את הפקודה הבאה:

dart pub add ffi

מחליפים את התוכן של main.dart בדוגמה בקוד הבא.

example/lib/main.dart

import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';

const String jsCode = '1+2';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Duktape duktape;
  String output = '';

  @override
  void initState() {
    super.initState();
    duktape = Duktape();
    setState(() {
      output = 'Initialized Duktape';
    });
  }

  @override
  void dispose() {
    duktape.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(fontSize: 25);
    const spacerSmall = SizedBox(height: 10);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Duktape Test')),
        body: Center(
          child: Container(
            padding: const EdgeInsets.all(10),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(output, style: textStyle, textAlign: TextAlign.center),
                spacerSmall,
                ElevatedButton(
                  child: const Text('Run JavaScript'),
                  onPressed: () {
                    duktape.evalString(jsCode);
                    setState(() {
                      output = '$jsCode => ${duktape.getInt(-1)}';
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

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

cd example
flutter run

האפליקציה אמורה לפעול כך:

הצגת Duktape שמופעל באפליקציה של Windows

הצגת פלט JavaScript של Duktape באפליקציה ל-Windows

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

Android

Android היא מערכת הפעלה מבוססת-ליבה של Linux, והיא דומה במידה מסוימת למהדורות Linux למחשב. מערכת ה-build של CMake יכולה להסתיר את רוב ההבדלים בין שתי הפלטפורמות. כדי ליצור אפליקציה ולהריץ אותה ב-Android, צריך לוודא שמהדורת Android Emulator פועלת (או שמכשיר 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++. המשמעות היא שצריך לשלב את החבילה הזו בתשתית ה-build של CocoaPods. כדי לאפשר שימוש חוזר בקוד C שכבר הגדרתם ל-build באמצעות CMake בשלב הקודם, תצטרכו להוסיף קובץ העברה יחיד ב-platform runner של macOS.

macos/Classes/duktape.c

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

הקובץ הזה משתמש ביכולות של מעבד הטקסט המקדים (C preprocessor) כדי לכלול את קוד המקור מקוד המקור המקורי שהגדרתם בשלב הקודם. פרטים נוספים על התהליך מופיעים בקובץ 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. הטמעת לולאת הקריאה, ההערכה וההדפסה (REPL)

הרבה יותר כיף לבצע אינטראקציה עם שפת תכנות בסביבה אינטראקטיבית מהירה. ההטמעה המקורית של סביבה כזו הייתה Read Eval Print Loop (REPL) של LISP. בשלב הזה תרימו משהו דומה באמצעות Duktape.

הכנת הדברים לקראת הייצור

הקוד הנוכחי שמקיים אינטראקציה עם ספריית C של Duktape מניח ששום דבר לא יכול להשתבש. אה, והוא לא טוען את ספריות הקישורים הדינמיים של Duktape במהלך הבדיקה. כדי שהשילוב יהיה מוכן לשימוש בסביבת הייצור, צריך לבצע כמה שינויים ב-lib/ffigen_app.dart.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;
import 'package:path/path.dart' as p;                               // Add this import

import 'duktape_bindings_generated.dart';

const String _libName = 'ffigen_app';

final DynamicLibrary _dylib = () {
  if (Platform.isMacOS || Platform.isIOS) {
    // Add from here...
    if (Platform.environment.containsKey('FLUTTER_TEST')) {
      return DynamicLibrary.open(
        'build/macos/Build/Products/Debug/$_libName/$_libName.framework/$_libName',
      );
    }
    // To here.
    return DynamicLibrary.open('$_libName.framework/$_libName');
  }
  if (Platform.isAndroid || Platform.isLinux) {
    // Add from here...
    if (Platform.environment.containsKey('FLUTTER_TEST')) {
      return DynamicLibrary.open(
        'build/linux/x64/debug/bundle/lib/lib$_libName.so',
      );
    }
    // To here.
    return DynamicLibrary.open('lib$_libName.so');
  }
  if (Platform.isWindows) {
    // Add from here...
    if (Platform.environment.containsKey('FLUTTER_TEST')) {
      return switch (Abi.current()) {
        Abi.windowsArm64 => DynamicLibrary.open(
          p.canonicalize(
            p.join(r'build\windows\arm64\runner\Debug', '$_libName.dll'),
          ),
        ),
        Abi.windowsX64 => DynamicLibrary.open(
          p.canonicalize(
            p.join(r'build\windows\x64\runner\Debug', '$_libName.dll'),
          ),
        ),
        _ => throw 'Unsupported platform',
      };
    }
    // To here.
    return DynamicLibrary.open('$_libName.dll');
  }
  throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final DuktapeBindings _bindings = DuktapeBindings(_dylib);

class Duktape {
  Duktape() {
    ctx = _bindings.duk_create_heap(
      nullptr,
      nullptr,
      nullptr,
      nullptr,
      nullptr,
    );
  }

  // Modify this function
  String evalString(String jsCode) {
    var nativeUtf8 = jsCode.toNativeUtf8();
    final evalResult = _bindings.duk_eval_raw(
      ctx,
      nativeUtf8.cast<Char>(),
      0,
      0 |
          DUK_COMPILE_EVAL |
          DUK_COMPILE_SAFE |
          DUK_COMPILE_NOSOURCE |
          DUK_COMPILE_STRLEN |
          DUK_COMPILE_NOFILENAME,
    );
    ffi.malloc.free(nativeUtf8);

    if (evalResult != 0) {
      throw _retrieveTopOfStackAsString();
    }

    return _retrieveTopOfStackAsString();
  }

  // Add this function
  String _retrieveTopOfStackAsString() {
    Pointer<Size> outLengthPtr = ffi.calloc<Size>();
    final errorStrPtr = _bindings.duk_safe_to_lstring(ctx, -1, outLengthPtr);
    final returnVal = errorStrPtr.cast<ffi.Utf8>().toDartString(
      length: outLengthPtr.value,
    );
    ffi.calloc.free(outLengthPtr);
    return returnVal;
  }

  void dispose() {
    _bindings.duk_destroy_heap(ctx);
    ctx = nullptr;
  }

  late Pointer<duk_hthread> ctx;
}

לשם כך צריך להוסיף את החבילה path.

flutter pub add path

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

הוספת חבילות

כשיוצרים REPL, מוצגת אינטראקציה בין המשתמש לבין מנוע JavaScript של Duktape. המשתמש מזין שורות קוד, ו-Duktape משיב בתוצאת החישוב או בחריגה. תוכלו להשתמש ב-freezed כדי לצמצם את כמות הקוד הסטנדרטי שצריך לכתוב. תוכלו גם להשתמש ב-google_fonts כדי שהתוכן המוצג יהיה תואם יותר לנושא, וב-flutter_riverpod לניהול המצב.

מוסיפים את יחסי התלות הנדרשים לאפליקציית הדוגמה:

cd example
flutter pub add flutter_riverpod freezed_annotation google_fonts
flutter pub add -d build_runner freezed

בשלב הבא יוצרים קובץ כדי לתעד את האינטראקציה עם REPL:

example/lib/duktape_message.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'duktape_message.freezed.dart';

@freezed
class DuktapeMessage with _$DuktapeMessage {
  factory DuktapeMessage.evaluate(String code) = DuktapeMessageCode;
  factory DuktapeMessage.response(String result) = DuktapeMessageResponse;
  factory DuktapeMessage.error(String log) = DuktapeMessageError;
}

הכיתה הזו משתמשת בתכונה של freezed מסוג 'איחוד' כדי לאפשר ביטוי בטוח מסוג של הצורה של כל שורה שמוצגת ב-REPL כאחד משלושה סוגים. בשלב הזה, סביר להניח שהקוד שלכם מציג סוג כלשהו של שגיאה, כי יש קוד נוסף שצריך ליצור. כדי לעשות זאת, פועלים לפי השלבים הבאים.

flutter pub run build_runner build

הפקודה הזו יוצרת את הקובץ example/lib/duktape_message.freezed.dart, שעליו מבוסס הקוד שהקלדתם.

בשלב הבא, תצטרכו לבצע כמה שינויים בקובצי התצורה של macOS כדי לאפשר ל-google_fonts לשלוח בקשות לרשת לקבלת נתוני גופנים.

example/macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- ...to here -->
</dict>
</plist>

example/macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- ...to here -->
</dict>
</plist>

פיתוח ה-REPL

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

example/lib/main.dart

import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';

import 'duktape_message.dart';

void main() {
  runApp(const ProviderScope(child: DuktapeApp()));
}

final duktapeMessagesProvider =
    StateNotifierProvider<DuktapeMessageNotifier, List<DuktapeMessage>>((ref) {
      return DuktapeMessageNotifier(messages: <DuktapeMessage>[]);
    });

class DuktapeMessageNotifier extends StateNotifier<List<DuktapeMessage>> {
  DuktapeMessageNotifier({required List<DuktapeMessage> messages})
    : duktape = Duktape(),
      super(messages);
  final Duktape duktape;

  void eval(String code) {
    state = [DuktapeMessage.evaluate(code), ...state];
    try {
      final response = duktape.evalString(code);
      state = [DuktapeMessage.response(response), ...state];
    } catch (e) {
      state = [DuktapeMessage.error('$e'), ...state];
    }
  }
}

class DuktapeApp extends StatelessWidget {
  const DuktapeApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(title: 'Duktape App', home: DuktapeRepl());
  }
}

class DuktapeRepl extends ConsumerStatefulWidget {
  const DuktapeRepl({super.key});

  @override
  ConsumerState<DuktapeRepl> createState() => _DuktapeReplState();
}

class _DuktapeReplState extends ConsumerState<DuktapeRepl> {
  final _controller = TextEditingController();
  final _focusNode = FocusNode();
  var _isComposing = false;

  void _handleSubmitted(String text) {
    _controller.clear();
    setState(() {
      _isComposing = false;
    });
    setState(() {
      ref.read(duktapeMessagesProvider.notifier).eval(text);
    });
    _focusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) {
    final messages = ref.watch(duktapeMessagesProvider);
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text('Duktape REPL'),
        elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0,
      ),
      body: Column(
        children: [
          Flexible(
            child: Ink(
              color: Theme.of(context).scaffoldBackgroundColor,
              child: SafeArea(
                bottom: false,
                child: ListView.builder(
                  padding: const EdgeInsets.all(8.0),
                  reverse: true,
                  itemBuilder: (context, idx) {
                    return switch (messages[idx]) {
                      DuktapeMessageCode code => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          '> ${code.code}',
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleMedium,
                          ),
                        ),
                      ),
                      DuktapeMessageResponse response => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          '= ${response.result}',
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleMedium,
                            color: Colors.blue[800],
                          ),
                        ),
                      ),
                      DuktapeMessageError error => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          error.log,
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleSmall,
                            color: Colors.red[800],
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                      DuktapeMessage message => Padding(
                        padding: const EdgeInsets.symmetric(vertical: 2),
                        child: Text(
                          'Unhandled message $message',
                          style: GoogleFonts.firaCode(
                            textStyle: Theme.of(context).textTheme.titleSmall,
                            color: Colors.red[800],
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    };
                  },
                  itemCount: messages.length,
                ),
              ),
            ),
          ),
          const Divider(height: 1.0),
          SafeArea(
            top: false,
            child: Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildTextComposer(),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTextComposer() {
    return IconTheme(
      data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: Row(
          children: [
            Text('>', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(width: 4),
            Flexible(
              child: TextField(
                controller: _controller,
                decoration: const InputDecoration(border: InputBorder.none),
                onChanged: (text) {
                  setState(() {
                    _isComposing = text.isNotEmpty;
                  });
                },
                onSubmitted: _isComposing ? _handleSubmitted : null,
                focusNode: _focusNode,
              ),
            ),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 4.0),
              child: IconButton(
                icon: const Icon(Icons.send),
                onPressed: _isComposing
                    ? () => _handleSubmitted(_controller.text)
                    : null,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

cd example
flutter run

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

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

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

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

8. מזל טוב

מעולה! סיימתם ליצור תוסף מבוסס-FFI של Flutter ל-Windows, ל-macOS, ל-Linux, ל-Android ול-iOS.

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