1. 簡介
Dart 的 FFI (外部函式介面) 可讓 Flutter 應用程式使用現有的原生資料庫,以便公開 C API。Dart 支援 Android、iOS、Windows、macOS 和 Linux 上的 FFI。在網頁上,Dart 支援 JavaScript 互通性,但本程式碼研究室未涵蓋這個主題。
建構項目
在本程式碼研究室中,您將建構使用 C 程式庫的行動裝置和電腦外掛程式。您將使用這個 API 編寫可運用外掛程式的範例應用程式。您的外掛程式和應用程式將:
- 將 C 程式庫原始碼匯入新的 Flutter 外掛程式
- 自訂外掛程式,讓外掛程式可在 Windows、macOS、Linux、Android 和 iOS 上建構
- 建構使用 JavaScript REPL (Read Reveal Print Loop) 外掛程式的應用程式
課程內容
在本程式碼研究室中,您將學習在電腦和行動平台上建構以 FFI 為基礎的 Flutter 外掛程式所需的實務知識,包括:
- 產生以 Dart FFI 為基礎的 Flutter 外掛程式範本
- 使用
ffigen
套件為 C 程式庫產生繫結程式碼 - 使用 CMake 建構 Android、Windows 和 Linux 適用的 Flutter FFI 外掛程式
- 使用 CocoaPods 建構適用於 iOS 和 macOS 的 Flutter FFI 外掛程式
軟硬體需求
- 用於 Android 開發的 Android Studio 4.1 以上版本
- 適用於 iOS 和 macOS 開發作業的 Xcode 13 以上版本
- Visual Studio 2022 或 Visual Studio Build Tools 2022,搭配「Desktop development with C++」工作負載,用於 Windows 桌面開發
- Flutter SDK
- 您要開發的平台所需的任何建構工具 (例如 CMake、CocoaPods 等)。
- LLVM 適用於您要開發的平台。
ffigen
會使用 LLVM 編譯器工具套件,剖析 C 標頭檔案,以建構 Dart 中公開的 FFI 繫結。 - 程式碼編輯器,例如 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
您可以使用下列任一裝置執行本程式碼研究室的程式碼:
- 您的開發電腦 (用於插件和範例應用程式的桌面版本)
- 已連接至電腦並設為開發人員模式的實體 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. 建構並執行範例
為確保已正確安裝建構系統和必要條件,且可在每個支援的平台上運作,請為每個目標建構及執行產生的範例應用程式。
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 版本。本程式碼研究室使用 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 進行編譯。
您需要變更 example/android/app/build.gradle.kts
,才能使用適當的 NDK 版本。
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
參數指定裝置。
恭喜,您已成功在五個不同的作業系統上建構及執行應用程式。接下來,請使用 FFI 建構原生外掛程式,並透過 Dart 與其進行介面連結。
5. 在 Windows、Linux 和 Android 上使用 Duktape
您在本程式碼研究室中會使用的 C 程式庫是 Duktape。Duktape 是可嵌入的 JavaScript 引擎,主要著重於可攜性和小巧的占用空間。在這個步驟中,您將設定外掛程式來編譯 Duktape 程式庫,將其連結至外掛程式,然後使用 Dart 的 FFI 存取該程式庫。
這個步驟會將整合設定設為在 Windows、Linux 和 Android 上運作。iOS 和 macOS 整合作業需要額外的設定 (除了本步驟中詳細說明的部分),才能將編譯的程式庫納入最終的 Flutter 可執行檔。下一個步驟會說明其他必要的設定。
擷取 Duktape
首先,請從 duktape.org 網站下載 duktape
原始碼的副本。
針對 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
會使用 LLVM 剖析 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
新增至二進位搜尋路徑,以便在 Windows 機器上完成 LLVM 安裝作業。您可以按照下列步驟測試是否已正確安裝。
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) ...
如上所述,您可以按照下列方式在 Linux 上測試 LLVM 安裝作業。
$ 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
這項設定包含要傳遞至 LLVM 的 C 標頭檔案、要產生的輸出檔案、要放在檔案頂端的說明,以及用於新增 Lint 警告的前言部分。
檔案結尾有一個需要進一步說明的設定項目。自 ffigen
11.0.0 版起,如果 clang
在剖析標頭檔案時產生警告或錯誤,則繫結產生器預設不會產生繫結。
由於 Duktape 指標缺少可空性類型指定符,因此 Duktape 標頭檔案會在 macOS 上觸發 clang
並產生警告。如要全面支援 macOS 和 iOS,Duktape 需要在 Duktape 程式碼庫中新增這些類型指標。在此期間,我們決定將 ignore-source-errors
標記設為 true
,藉此忽略這些警告。
在正式版應用程式中,您應先移除所有編譯器警告,再發布應用程式。不過,針對 Duktape 執行這項操作不在本程式碼研究室的範圍內。
如要進一步瞭解其他鍵和值,請參閱 ffigen
說明文件。
您必須將特定 Duktape 檔案從 Duktape 發布項目複製到 ffigen
設定的尋找位置。
cp duktape-2.7.0/src/duktape.c src/ cp duktape-2.7.0/src/duktape.h src/ cp duktape-2.7.0/src/duk_config.h src/
從技術層面來說,您只需要為 ffigen
複製 duktape.h
,但您即將設定 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 已知可在 Windows、Linux 和 macOS 上使用 clang
進行編譯,因此您可以暫時忽略這些訊息。
設定 CMake
CMake 是建構系統產生系統。這個外掛程式會使用 CMake 產生 Android、Windows 和 Linux 的建構系統,將 Duktape 納入產生的 Flutter 二進位檔。您需要修改範本產生的 CMake 設定檔,如下所示。
src/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(ffigen_app_library VERSION 0.0.1 LANGUAGES C)
add_library(ffigen_app SHARED
duktape.c # Modify
)
set_target_properties(ffigen_app PROPERTIES
PUBLIC_HEADER duktape.h # Modify
PRIVATE_HEADER duk_config.h # Add
OUTPUT_NAME "ffigen_app" # Add
)
# Add from here...
if (WIN32)
set_target_properties(ffigen_app PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif (WIN32)
# ... to here.
target_compile_definitions(ffigen_app PUBLIC DART_SHARED_LIB)
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 的解決方法,可協助將 Duktape 等 Unix 風格的程式庫移植至 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;
}
這個檔案負責載入動態連結程式庫檔案 (Linux 和 Android 為 .so
,Windows 為 .dll
),並提供包裝函式,向底層 C 程式碼公開更多 Dart 慣用介面。
由於這個檔案會直接匯入 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
您應該會看到執行中的應用程式,如下所示:
這兩張螢幕截圖分別顯示按下「Run JavaScript」按鈕前後的畫面。這會示範如何從 Dart 執行 JavaScript 程式碼,並在畫面上顯示結果。
Android
Android 是基於 Linux 核心的作業系統,與桌面 Linux 發行版本有些類似。CMake 建構系統可隱藏這兩個平台之間的大部分差異。如要在 Android 上建構及執行應用程式,請確認 Android 模擬器正在執行 (或已連結 Android 裝置)。執行應用程式。例如:
cd example flutter run -d emulator-5554
您現在應該會看到在 Android 上執行的範例應用程式:
6. 在 macOS 和 iOS 上使用 Duktape
接下來,我們要讓外掛程式在 macOS 和 iOS 這兩個密切相關的作業系統上運作。請先從 macOS 開始。雖然 CMake 支援 macOS 和 iOS,但您無法重複使用為 Linux 和 Android 執行的工作,因為 macOS 和 iOS 上的 Flutter 會使用 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
macOS 平台上的 Flutter 會使用 CocoaPods 匯入 C 和 C++ 程式碼。也就是說,這個套件必須整合至 CocoaPods 建構基礎結構。如要讓先前步驟中已設定為使用 CMake 建構的 C 程式碼可重複使用,您必須在 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
在快速互動環境中,與程式設計語言互動會更有趣。這種環境的原始實作方式是 LISP 的 Read Eval Print Loop (REPL)。您將在這個步驟中實作類似 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 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
載入動態連結程式庫的程式碼已擴充,可處理在測試執行器中使用外掛程式的情況。這可讓您編寫整合測試,以便以 Flutter 測試的方式測試此 API。用於評估 JavaScript 程式碼字串的程式碼已擴充,可正確處理錯誤狀況,例如程式碼不完整或不正確。這段額外程式碼說明如何處理字串以位元組陣列形式傳回,且需要轉換為 Dart 字串的情況。
新增套件
建立 REPL 時,您會顯示使用者與 Duktape JavaScript 引擎之間的互動情形。使用者輸入程式碼行,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. 恭喜
恭喜!您已成功建立適用於 Windows、macOS、Linux、Android 和 iOS 的 Flutter FFI 外掛程式!
建立外掛程式後,您可能會想在線上分享,讓其他人也能使用。如要進一步瞭解如何將外掛程式發布至 pub.dev,請參閱「開發外掛程式套件」一文。