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
מה תלמדו
ב-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=/
אמור להופיע חלון של אפליקציה שפועלת, כמו זה:
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=/
אמור להופיע חלון של אפליקציה שפועלת, כמו זה:
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 תתבקשו לבחור את המכשיר שבו תרצו להריץ אותו. בוחרים את המכשיר המתאים מהמכשירים שמופיעים ברשימה.
macOS ו-iOS
כדי לפתח אפליקציות ל-macOS ול-iOS ב-Flutter, צריך להשתמש במחשב עם macOS.
מתחילים בהפעלת אפליקציית הדוגמה ב-macOS. שוב מוודאים אילו מכשירים גלויים ל-Flutter:
$ flutter devices 2 connected devices: macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
מריצים את אפליקציית הדוגמה באמצעות פרויקט הפלאגין שנוצר:
cd ffigen_app/example flutter run -d macos
אמור להופיע חלון של אפליקציה שפועלת, כמו זה:
ב-iOS אפשר להשתמש בסימולטור או במכשיר חומרה אמיתי. אם משתמשים בסימולטור, קודם צריך להפעיל אותו. עכשיו, הפקודה flutter devices
מציגה את הסימולטור כאחד מהמכשירים הזמינים.
$ flutter devices 3 connected devices: iPhone SE (3rd generation) (mobile) • 1BCBE334-7EC4-433A-90FD-1BC14F3BA41F • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-1 (simulator) macOS (desktop) • macos • darwin-arm64 • macOS 13.1 22C65 darwin-arm Chrome (web) • chrome • web-javascript • Google Chrome 108.0.5359.98
אחרי שמפעילים מכשיר iOS, פיזי או סימולטור, מריצים את הפקודה הבאה:
cd ffigen_app/example flutter run
ב-Flutter תתבקשו לבחור את המכשיר שבו תרצו להריץ אותו. בוחרים את המכשיר המתאים מהמכשירים שמופיעים ברשימה.
הסימולטור של 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
האפליקציה אמורה לפעול כך:
בשתי צילומי המסך האלה מוצגים המצב לפני ואחרי הלחיצה על הלחצן הפעלת JavaScript. הדוגמה הזו ממחישה את ההפעלה של קוד JavaScript מ-Dart והצגת התוצאה במסך.
Android
Android היא מערכת הפעלה מבוססת-ליבה של Linux, והיא דומה במידה מסוימת למהדורות Linux למחשב. מערכת ה-build של CMake יכולה להסתיר את רוב ההבדלים בין שתי הפלטפורמות. כדי ליצור אפליקציה ולהריץ אותה ב-Android, צריך לוודא שמהדורת Android Emulator פועלת (או שמכשיר Android מחובר). מריצים את האפליקציה. לדוגמה:
cd example flutter run -d emulator-5554
עכשיו האפליקציה לדוגמה אמורה לפעול ב-Android:
6. שימוש ב-Duktape ב-macOS וב-iOS
עכשיו הגיע הזמן לגרום לפלאגין לפעול ב-macOS וב-iOS, שתי מערכות הפעלה קשורות מאוד. מתחילים עם macOS. מערכת CMake תומכת ב-macOS וב-iOS, אבל לא תוכלו לעשות שימוש חוזר בעבודה שביצעתם ל-Linux ול-Android, כי ב-Flutter ב-macOS וב-iOS נעשה שימוש ב-CocoaPods כדי לייבא ספריות.
הסרת המשאבים
בשלב הקודם פיתחתם אפליקציה שפועלת ב-Android, ב-Windows וב-Linux. עם זאת, יש כמה קבצים שנשארו מהתבנית המקורית ועכשיו צריך לנקות אותם. כדי להסיר אותם, פועלים לפי השלבים הבאים.
rm src/ffigen_app.c rm src/ffigen_app.h rm ios/Classes/ffigen_app.c rm macos/Classes/ffigen_app.c
macOS
ב-Flutter בפלטפורמת macOS נעשה שימוש ב-CocoaPods כדי לייבא קוד C ו-C++. המשמעות היא שצריך לשלב את החבילה הזו בתשתית ה-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
iOS
בדומה להגדרה ב-macOS, גם ב-iOS צריך להוסיף קובץ C יחיד להעברה.
ios/Classes/duktape.c
#include "../../src/duktape.c"
בעזרת הקובץ היחיד הזה, הפלאגין מוגדר עכשיו לפעול גם ב-iOS. מריצים אותו כרגיל.
flutter run -d iPhone
מעולה! השלמתם את השילוב של קוד מקומי בחמש פלטפורמות. זו סיבה לחגיגה! אולי אפילו ממשק משתמש פונקציונלי יותר, שתבנו בשלב הבא.
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
8. מזל טוב
מעולה! סיימתם ליצור תוסף מבוסס-FFI של Flutter ל-Windows, ל-macOS, ל-Linux, ל-Android ול-iOS.
אחרי שיוצרים פלאגין, כדאי לשתף אותו באינטרנט כדי שאנשים אחרים יוכלו להשתמש בו. במאמר פיתוח חבילות של יישומי פלאגין מפורטת התיעוד המלא בנושא פרסום הפלאגין ב-pub.dev.