Flutter Web平台外部函数接口插件web_ffi的使用
Flutter Web平台外部函数接口插件web_ffi的使用
web_ffi
是一个用于在Web平台上使用 dart:ffi
的解决方案。它使与 WebAssembly
的交互变得更加容易和便捷。
一、简介
1.1 web_ffi简介
- 功能:
web_ffi
提供了一个兼容dart:ffi
的API,但所有调用都通过dart:js
转换到浏览器中运行的WebAssembly
。 - 支持平台:目前仅支持使用 emscripten 编译的
WebAssembly
,因为 emscripten 还会生成WebAssembly
所需的 JavaScript 导入文件。如果需要支持其他平台/编译器,请在 GitHub 上提交问题。
1.2 使用教程
有关如何使用此包(包括 emscripten 的编译器设置)的教程,请参阅 example/README,但在阅读之前请先阅读本 README!
二、与 dart:ffi 的差异
虽然 web_ffi
尽量模仿 dart:ffi
API,但仍存在一些差异:
- 数组扩展:
web_ffi
基于dart:ffi API 2.12.0
设计,因此目前不支持数组扩展(它们是在 dart 2.13.0 中引入的)。 - 结构体支持:当前不支持结构体(但可以使用不透明结构体)。
- 额外类和函数:
web_ffi
包含一些dart:ffi
中不存在的类和函数,这些类和函数被标记为@extra
。 - Memory 类:新增了
Memory
类,它是 重要的 并将在下文详细解释。 - DynamicLibrary 构造函数:
DynamicLibrary
的构造函数不同,需要传入Module
类的一个实例。 - Opaque 类继承:如果你继承了
Opaque
类,必须在使用前通过registerOpaqueType<T>()
注册扩展类!并且你的类不能有类型参数。 - 原生函数交互规则:当查找函数或将其转换为 Dart 函数时,实际的类型参数不会被检查,只有名称会被匹配。对于返回类型有一些特殊约束,详情请参见 return_types.md。
三、内存管理
当你想要使用 web_ffi
时,首先应该调用 Memory.init()
来初始化内存系统。根据使用的 WebAssembly 模块类型(wasm32 或 wasm64),你可能需要指定指针大小,默认值为 4 表示 32 位指针,如果是 wasm64 则应调用 Memory.init(8)
。
每个指针对象都绑定到一个 Memory
对象,可以通过 Pointer.boundMemory
访问。创建指针时,可以通过 Pointer.fromAddress()
的 bindTo
参数显式指定要绑定的 Memory
对象。如果没有指定,则会使用全局默认的 Memory.global
,若未设置则会抛出异常。
此外,每个 DynamicLibrary
也绑定到了一个 Memory
对象,同样可以通过 DynamicLibrary.boundMemory
获取。由于 Memory
实现了 Allocator
接口,这在某些情况下可能会派上用场。
四、使用示例
下面是一个完整的使用 web_ffi
的例子,演示了如何将 C 库(以 Opus 为例)移植到 Web 平台。
4.1 创建代理 FFI 文件
创建 lib/src/proxy_ffi.dart
文件:
export 'package:web_ffi/web_ffi.dart' if (dart.library.ffi) 'dart:ffi';
4.2 编写绑定代码
假设我们已经有了自动生成的绑定代码(由 ffi_tool 生成),保存在 lib/src/generated.dart
文件中:
/// Contains methods and structs from the opus_libinfo group of opus_defines.h.
///
/// AUTOMATICALLY GENERATED FILE. DO NOT MODIFY.
library opus_libinfo;
import 'proxy_ffi.dart' as ffi;
typedef _opus_get_version_string_C = ffi.Pointer<ffi.Uint8> Function();
typedef _opus_get_version_string_Dart = ffi.Pointer<ffi.Uint8> Function();
class FunctionsAndGlobals {
FunctionsAndGlobals(ffi.DynamicLibrary _dynamicLibrary)
: _opus_get_version_string = _dynamicLibrary.lookupFunction<
_opus_get_version_string_C, _opus_get_version_string_Dart>(
'opus_get_version_string',
);
/// Gets the libopus version string.
///
/// Applications may look for the substring "-fixed" in the version string to determine whether they have a fixed-point or floating-point build at runtime.
///
/// @returns Version string
ffi.Pointer<ffi.Uint8> opus_get_version_string() {
return _opus_get_version_string();
}
final _opus_get_version_string_Dart _opus_get_version_string;
}
4.3 编译 C 代码为 WebAssembly
使用 emscripten 编译 C 代码到 WebAssembly,并生成必要的 JavaScript 文件。确保传递 -s MODULARIZE=1
和 -s EXPORT_NAME=libopus
参数给 emcc
,并导出所需的符号。以下是 Dockerfile 示例:
#Current version: 2.0.21
FROM emscripten/emsdk
RUN git clone --branch v1.3.1 https://github.com/xiph/opus.git
WORKDIR ./opus
RUN apt-get update \
&& DEBIAN_FRONTENTD="noninteractive" apt-get install -y --no-install-recommends \
autoconf \
libtool \
automake
ENV CFLAGS='-O3 -fPIC'
ENV CPPFLAGS='-O3 -fPIC'
RUN ./autogen.sh \
&& emconfigure ./configure \
--disable-intrinsics \
--disable-rtcd \
--disable-extra-programs \
--disable-doc \
--enable-static \
--disable-stack-protector \
--with-pic=ON \
&& emmake make
RUN mkdir emc_out \
&& emcc -O3 -s MAIN_MODULE=1 -s EXPORT_NAME=libopus -s MODULARIZE=1 ./.libs/libopus.a -o ./emc_out/libopus.js
WORKDIR ./emc_out
编译完成后,我们将得到 libopus.js
和 libopus.wasm
文件。
4.4 初始化 Web FFI
4.4.1 非 Web 平台
创建 lib/src/init_ffi.dart
文件:
// Notice that in this file, we import dart:ffi and not proxy_ffi.dart
import 'dart:ffi';
// For dart:ffi platforms, this can be a no-op (empty function)
Future<void> initFfi() async {}
DynamicLibrary openOpus() {
return DynamicLibrary.open('libopus.so');
}
4.4.2 Web 平台
4.4.2.1 Flutter 项目
更新 pubspec.yaml
文件,添加对 inject_js
插件的依赖以及 assets 配置:
name: web_ffi_example_flutter
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
inject_js: ^2.0.0
web_ffi:
path: ../..
flutter:
assets:
- assets/libopus.js
- assets/libopus.wasm
然后编写 lib/src/init_web.dart
文件:
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:inject_js/inject_js.dart' as Js;
import 'package:web_ffi/web_ffi.dart';
import 'package:web_ffi/web_ffi_modules.dart';
const String _basePath = 'assets';
Module? _module;
Future<void> initFfi() async {
if (_module == null) {
Memory.init();
await Js.importLibrary('$_basePath/libopus.js');
String path = '$_basePath/libopus.wasm';
Uint8List wasmBinaries = (await rootBundle.load(path)).buffer.asUint8List();
_module = await EmscriptenModule.compile(wasmBinaries, 'libopus');
}
}
DynamicLibrary openOpus() {
Module? m = _module;
if (m != null) {
return DynamicLibrary.fromModule(m);
} else {
throw StateError('You can not open opus before calling initFfi()!');
}
}
4.4.2.2 非 Flutter 项目
创建 web/index.html
文件,添加 <script>
标签引用 libopus.js
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>web_ffi Test Page</title>
<script defer src="main.dart.js"></script>
<script src="libopus.js"></script>
</head>
<body>
</body>
</html>
编写 lib/src/init_web.dart
文件:
import 'package:web_ffi/web_ffi.dart';
import 'package:web_ffi/web_ffi_modules.dart';
Module? _module;
Future<void> initFfi() async {
if (_module == null) {
Memory.init();
_module = await EmscriptenModule.process('libopus');
}
}
DynamicLibrary openOpus() {
Module? m = _module;
if (m != null) {
return DynamicLibrary.fromModule(m);
} else {
throw StateError('You can not open opus before calling initFfi()!');
}
}
4.5 更新代理文件
更新 lib/src/proxy_ffi.dart
文件,使其导出正确的初始化文件:
export 'package:web_ffi/web_ffi.dart' if (dart.library.ffi) 'dart:ffi';
export 'init_web.dart' if (dart.library.ffi) 'init_ffi.dart';
4.6 编写应用代码
4.6.1 字符串处理辅助函数
创建 lib/src/c_strings.dart
文件,用于处理从 C 指针转换为 Dart 字符串的操作:
import 'dart:convert';
import 'proxy_ffi.dart';
String fromCString(Pointer<Uint8> cString) {
int len = 0;
while (cString[len] != 0) {
len++;
}
return len > 0 ? ascii.decode(cString.asTypedList(len)) : '';
}
Pointer<Uint8> toCString(String dartString, Allocator allocator) {
List<int> bytes = ascii.encode(dartString);
Pointer<Uint8> cString = allocator.allocate<Uint8>(bytes.length);
cString.asTypedList(bytes.length).setAll(0, bytes);
return cString;
}
4.6.2 Flutter 应用代码
编辑 lib/main.dart
文件:
import 'package:flutter/material.dart';
import 'src/proxy_ffi.dart';
import 'src/c_strings.dart';
import 'src/generated.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initFfi();
DynamicLibrary dynLib = openOpus();
FunctionsAndGlobals opusLibinfo = FunctionsAndGlobals(dynLib);
String version = fromCString(opusLibinfo.opus_get_version_string());
runApp(MyApp(version));
}
class MyApp extends StatelessWidget {
final String _opusVersion;
const MyApp(this._opusVersion);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'web_ffi Demo',
home: Scaffold(
appBar: AppBar(
title: Text('web_ffi Demo'),
centerTitle: true,
),
body: Container(
alignment: Alignment.center,
child: Text(_opusVersion),
)),
);
}
}
4.6.3 非 Flutter 应用代码
创建 lib/example_no_flutter.dart
文件:
library example_no_flutter;
export 'src/proxy_ffi.dart';
export 'src/generated.dart';
export 'src/c_strings.dart';
编写 bin/main.dart
文件:
import 'package:web_ffi_example_no_flutter/example_no_flutter.dart';
Future<void> main() async {
await initFfi();
DynamicLibrary opus = openOpus();
FunctionsAndGlobals opusLibinfo = FunctionsAndGlobals(opus);
Pointer<Uint8> cString = opusLibinfo.opus_get_version_string();
print(fromCString(cString));
}
4.7 运行应用
4.7.1 Flutter 项目
只需执行以下命令即可启动应用:
flutter run -d chrome
你应该会在浏览器中看到文本字段显示 libopus 1.3.1
。
4.7.2 非 Flutter 项目
首先使用 dart2js
编译 main.dart
文件:
dart2js ./bin/main.dart -o ./web/main.dart.js
接着使用 dhttpd 担任简单 HTTP 服务器来提供网页内容:
pub global activate dhttpd
cd web
pub global run dhttpd -p 8080
现在你可以导航到 http://localhost:8080,并在浏览器的开发者工具控制台中查看输出结果:libopus 1.3.1
。
以上就是关于如何在 Flutter Web 项目中使用 web_ffi
插件与 WebAssembly 进行交互的完整指南。希望这个例子能帮助你更好地理解和使用该插件。
更多关于Flutter Web平台外部函数接口插件web_ffi的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter Web平台外部函数接口插件web_ffi的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
在Flutter Web平台上使用web_ffi
插件来调用外部函数接口(Foreign Function Interface, FFI)是一种强大的技术,可以让你在Dart代码中调用C/C++编写的原生代码。尽管web_ffi
主要用于桌面和移动平台上的Flutter应用,但其在Web上的实现需要一些特殊考虑,通常涉及使用Wasm(WebAssembly)模块来桥接原生代码。
以下是一个基本的例子,展示如何在Flutter Web项目中使用web_ffi
和Wasm模块。请注意,由于Web平台的限制,直接使用传统的FFI调用(如在桌面或移动平台上)是不可能的,因此我们需要使用Wasm作为中介。
1. 准备Wasm模块
首先,你需要有一个用C/C++编写的函数,并将其编译成Wasm模块。假设你有一个简单的C函数add
,它接受两个整数并返回它们的和:
// add.c
int add(int a, int b) {
return a + b;
}
使用Emscripten编译这个C文件为Wasm模块:
emcc add.c -s WASM=1 -o add.js -o add.wasm
这将生成两个文件:add.js
和add.wasm
。
2. 在Flutter Web项目中集成Wasm
将生成的add.js
和add.wasm
文件放入你的Flutter Web项目的web
目录下。
3. 加载Wasm模块并在Dart中调用
在Flutter Web项目中,你需要使用Dart的dart:html
库来加载Wasm模块,并通过JavaScript与Wasm进行交互。由于web_ffi
在Web上的直接支持有限,我们将直接使用Dart与Wasm的交互方式。
import 'dart:html' as html;
import 'dart:js' as js;
void main() async {
// Load the WebAssembly module
html.ScriptElement script = html.ScriptElement()
..src = 'add.js'
..defer = false
..async = false;
document.body!.append(script);
await js.context.callMethod('Promise', ['all', [
js.context.callMethod('fetch', ['add.wasm']),
]]).then((response) {
return response['arrayBuffer']();
}).then((buffer) {
// Assume `Module` is the global JavaScript object created by Emscripten
js.context.callMethod('Module', ['_malloc', buffer.byteLength]);
js.context.callMethod('Module', ['HEAPU8', [buffer]]);
js.context.callMethod('Module', ['_free', buffer.byteLength]);
// Initialize the module (this is usually handled by the generated JS, but we do it manually here)
js.context.callMethod('Module', ['_init']);
});
// Call the `add` function from WebAssembly
int result = js.context.callMethod('Module', ['_add', 5, 3]);
print('Result from WebAssembly: $result');
}
在这个例子中,我们首先加载add.js
脚本,该脚本由Emscripten生成,负责加载和初始化Wasm模块。然后,我们手动加载add.wasm
文件并将其内容传递给Wasm模块。最后,我们通过Module._add
调用Wasm中的add
函数。
注意:这里的代码假设Emscripten生成的JavaScript代码创建了一个全局的Module
对象,该对象包含了Wasm模块的所有导出函数。这通常是Emscripten的默认行为,但你可能需要根据你的构建配置进行调整。
结论
虽然web_ffi
在Flutter Web上的直接使用受到限制,但通过使用Wasm模块和Dart的dart:html
及dart:js
库,你仍然可以实现与原生代码的交互。这种方法需要更多的手动设置和对Web技术的理解,但它提供了在Web平台上使用原生代码的强大能力。