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