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 (讀取顯示列印迴圈) 的應用程式
課程內容
在本程式碼研究室中,您將學習在電腦和行動平台上建構以 FFI 為基礎的 Flutter 外掛程式所需的實用知識,包括:
- 產生以 Dart FFI 為基礎的 Flutter 外掛程式範本
- 使用
ffigen
套件產生 C 程式庫的繫結程式碼 - 使用 CMake 建構 Android、Windows 和 Linux 適用的 Flutter FFI 外掛程式
- 使用 CocoaPods 建構適用於 iOS 和 macOS 的 Flutter FFI 外掛程式
軟硬體需求
- 使用 Android Studio 4.1 以上版本進行 Android 開發作業
- iOS 和 macOS 開發作業適用的 Xcode 13 以上版本
- 搭配「使用 C++ 的電腦開發」的 Visual Studio 2022 或 Visual Studio Build Tools 2022Windows 電腦開發工作負載
- 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.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
您可以使用下列任何裝置執行本程式碼研究室中的程式碼:
- 開發電腦 (適用於電腦版外掛程式和範例應用程式)
- 與電腦連線,並設為開發人員模式的實體 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 編譯。首先,請確認您已將 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
參數指定裝置的步驟。
恭喜!您已成功在 5 種不同的作業系統上建構並執行應用程式。接著,建立原生外掛程式,並使用 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,您可以透過 Invoke-WebRequest
使用 PowerShell:
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
新增至二進位檔搜尋路徑,在 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 `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
這項設定包括要傳遞至 LLVM 的 C 標頭檔案、要產生的輸出檔案、要放在檔案頂端的說明,以及用於新增 Lint 警告的前置部分。
檔案結尾有一個設定項目,需要進一步說明。自 ffigen
11.0.0 版起,如果剖析標頭檔案時出現 clang
產生的警告或錯誤,繫結產生器預設不會產生繫結。
Duktape 標頭檔案如已編寫,在 macOS 上會觸發 clang
,以產生警告,因為 Duktape 的指標缺少是否可為空值類型指定碼。如要完整支援 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
以產生新的繫結:
$ 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 在 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)
CMake 設定會新增來源檔案,更重要的是修改在 Windows 上產生的程式庫檔案的預設行為,以匯出所有 C 符號。這項 CMake 可協助將 Unix 樣式程式庫 (Dktape) 移植到 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 和 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. 實作讀取評估列印迴圈
在快速的互動環境中,與程式設計語言互動會更加有趣。這類環境的原始實作方式是 LISP 的讀取評估列印迴圈 (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 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 引擎之間的互動。使用者輸入程式碼,Duktape 傳回計算結果或發生例外狀況的回應。您將使用 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,
),
),
],
),
),
);
}
}
本程式碼有太多工作內容,但不在本程式碼研究室的說明範圍內。建議您執行程式碼,並在參閱相關說明文件後修改程式碼。
$ cd example $ flutter run
8. 恭喜
恭喜!您已成功建立以 Flutter FFI 為基礎的外掛程式,該外掛程式適用於 Windows、macOS、Linux、Android 和 iOS!
建立外掛程式後,您可能會想在線上共用,好讓其他人使用。您可以參閱開發外掛程式套件,瞭解將外掛程式發布至 pub.dev 的完整說明文件。