在 Flutter 外掛程式中使用 FFI

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 (讀取顯示列印迴圈) 的應用程式

Duktape REPL 以 macOS 應用程式的形式執行

課程內容

在本程式碼研究室中,您將學習在電腦和行動平台上建構以 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 等)。
  • 針對應用程式開發平台使用 LLVMffigen 會使用 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 下方、平台專屬目錄 androidioslinuxmacoswindows,最重要的是 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=/

畫面上應會顯示執行中的應用程式視窗,如下所示:

範本產生的 FFI 應用程式以 Windows 應用程式的形式執行

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

畫面上應會顯示執行中的應用程式視窗,如下所示:

系統產生的範本 FFI 應用程式,以 Linux 應用程式的形式執行

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

範本產生的 FFI 應用程式在 Android 模擬器中執行

macOS 和 iOS

如要進行 macOS 和 iOS Flutter 開發作業,請務必使用 macOS 電腦。

先在 macOS 上執行範例應用程式。再次確認 Flutter 偵測到的裝置:

$ flutter devices
2 connected devices:

macOS (desktop) • macos  • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)    • chrome • web-javascript • Google Chrome 108.0.5359.98

使用產生的外掛程式專案執行範例應用程式:

$ cd ffigen_app/example
$ flutter run -d macos

畫面上應會顯示執行中的應用程式視窗,如下所示:

系統產生的範本 FFI 應用程式,以 Linux 應用程式的形式執行

如果是 iOS 裝置,您可以使用模擬器或實體硬體裝置。如果使用模擬器,請先啟動模擬器。flutter devices 指令現在會將模擬工具列為其中一種可用裝置。

$ flutter devices
3 connected devices:

iPhone SE (3rd generation) (mobile) • 1BCBE334-7EC4-433A-90FD-1BC14F3BA41F • ios            • com.apple.CoreSimulator.SimRuntime.iOS-16-1 (simulator)
macOS (desktop)                     • macos                                • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)                        • chrome                               • web-javascript • Google Chrome 108.0.5359.98

啟動模擬工具後,請執行:flutter run

$ cd ffigen_app/example
$ flutter run -d iphone

在 iOS 模擬工具中執行的範本產生的 FFI 應用程式

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/

就技術層面而言,您只需要針對 ffigenduktape.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

您應該會看到應用程式正在執行,如下所示:

顯示在 Windows 應用程式中初始化的 Duktape

在 Windows 應用程式中顯示 Duktape JavaScript 輸出內容

這兩張螢幕截圖顯示按下「Run JavaScript」按鈕前後的比較畫面。這示範如何透過 Dart 執行 JavaScript 程式碼,並在螢幕上顯示結果。

Android

Android 是以核心為基礎的 Linux 作業系統,與桌上型電腦的 Linux 發行版類似,CMake 建構系統可以隱藏這兩個平台之間的大部分差異。如要在 Android 上建構及執行,請確認 Android 模擬器正在執行 (或已連接 Android 裝置)。執行應用程式。例如:

$ cd example
$ flutter run -d emulator-5554

現在,您應該會看到在 Android 上執行的範例應用程式:

顯示在 Android 模擬器中初始化的 Duktape

在 Android 模擬器中顯示 Duktape JavaScript 輸出內容

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

顯示 macOS 應用程式中初始化的 Duktape

在 macOS 應用程式中顯示 Duktape JavaScript 輸出內容

iOS

與 macOS 設定類似,iOS 也需要新增一個轉送 C 檔案。

ios/Classes/duktape.c

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

在這個單一檔案上,您的外掛程式現已設定為在 iOS 上執行。照常執行。

$ flutter run -d iPhone

顯示已在 iOS 模擬器中初始化的 Duktape

在 iOS 模擬器中顯示 Duktape JavaScript 輸出內容

恭喜!您已成功在五個平台上整合原生程式碼。這裡是舉辦慶典的園地!也許是功能更豐富的使用者介面,您將在下一個步驟進行建構。

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

Duktape REPL 在 Linux 應用程式中執行

Duktape REPL 在 Windows 應用程式中執行

在 iOS 模擬器中執行 Duktape REPL

在 Android 模擬器中執行 Duktape REPL

8. 恭喜

恭喜!您已成功建立以 Flutter FFI 為基礎的外掛程式,該外掛程式適用於 Windows、macOS、Linux、Android 和 iOS!

建立外掛程式後,您可能會想在線上共用,好讓其他人使用。您可以參閱開發外掛程式套件,瞭解將外掛程式發布至 pub.dev 的完整說明文件。