1. 简介
Dart 的 FFI(外部函数接口)允许 Flutter 应用利用公开 C API 的现有原生库。Dart 支持在 Android、iOS、Windows、macOS 和 Linux 上运行 FFI。对于 Web 操作系统,Dart 支持 JavaScript 互操作性,但该主题不在此 Codelab 的讨论范围内。
您将构建的内容
在此 Codelab 中,您将构建一个使用 C 库的移动和桌面插件。使用此 API,您将编写一个利用该插件的简单示例应用。您的插件和应用将具备以下功能:
- 将 C 库源代码导入新的 Flutter 插件
- 自定义插件,以便允许在 Windows、macOS、Linux、Android 和 iOS 环境上构建插件
- 构建应用以使用插件实现 JavaScript REPL(“读取-求值-输出”循环)
学习内容
在此 Codelab 中,您将学习在桌面和移动平台上构建基于 FFI 的 Flutter 插件所需的实用知识,包括:
- 生成基于 Dart FFI 的 Flutter 插件模板
- 使用
ffigen
软件包为 C 库生成绑定代码 - 使用 CMake 构建适用于 Android、Windows 和 Linux 的 Flutter FFI 插件
- 使用 CocoaPods 构建适用于 iOS 和 macOS 的 Flutter FFI 插件
所需条件
- Android Studio 4.1 或更高版本(用于 Android 开发)
- Xcode 13 或更高版本(用于 iOS 和 macOS 开发)
- 随带“Desktop development with C++”工作负载的 Visual Studio 2022 或 Visual Studio Build Tools 2022(用于 Windows 开发)
- Flutter SDK
- 您的目标开发平台所需的任何构建工具(例如,CMake 和 CocoaPods 等)。
- 适用于您的目标开发平台的 LLVM。
ffigen
将使用 LLVM 编译器工具套件来解析 C 头文件,以构建 Dart 中公开的 FFI 绑定。 - 一个代码编辑器,例如 Visual Studio Code。
2. 使用入门
Flutter 最近才添加了 ffigen
工具。您可以运行以下命令,确认您所安装的 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
模板将插件的 Dart 代码放置在 lib
下,以及名称为 android
、ios
、linux
、macos
和 windows
的平台特定目录下,还有最重要的 example
目录下。
对于习惯于常规 Flutter 开发的开发者来说,此结构可能会让人感到奇怪,因为顶层并未定义可执行文件。插件应当包含在其他 Flutter 项目中,但您将在 example
目录中填充代码,以确保您的插件代码能正常运行。
让我们开始吧!
4. 构建并运行示例
为确保已正确安装构建系统和前提条件,并且适用于所有受支持的平台,请为每个目标构建并运行生成的示例应用。
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 模拟器 (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. 在 Windows、Linux 和 Android 上使用 Duktape
您将在此 Codelab 中使用的 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
将使用 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 的步骤。您可以通过以下方式测试是否已正确安装 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
此配置包括传递给 LLVM 的 C 头文件、要生成的输出文件、放置在文件顶部的描述,以及用于添加 lint 警告的前导部分。如需详细了解键和值,请参阅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 解决方案,可帮助将 Duktape 这种 Unix 风格的库移植到 Windows 环境。
将 lib/ffigen_app.dart
的内容替换为以下代码。
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io';
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 规范的接口。
将示例应用的 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. 实现“读取-求值-输出”循环
在快速交互环境中,与编程语言进行交互会更有趣。这种环境的原始实现是 LISP 的读取-求值-输出循环 (REPL)。在此步骤中,您将使用 Duktape 实现类似的功能。
准备发布到生产环境
当前与 Duktape C 库进行交互的代码假定不会发生任何错误。而且,在测试过程中,该代码不会加载 Duktape 动态链接库。如需让此集成能够发布到生产环境,您需要对 lib/ffigen_app.dart
进行一些更改。
lib/ffigen_app.dart
import 'dart:ffi';
import 'dart:io';
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 $ 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
的 union 类型功能来支持轻松通过三种类型之一来表示 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. 恭喜
恭喜!您已经成功为 Windows、macOS、Linux、Android 和 iOS 创建了一个基于 Flutter FFI 的插件!
在创建插件后,您可能希望在线共享该插件,以便他人使用。有关如何将插件发布到 pub.dev 的完整文档,请参阅开发插件软件包。