Flutter Web平台外部函数接口插件web_ffi的使用

发布于 1周前 作者 nodeper 来自 Flutter

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.jslibopus.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

1 回复

更多关于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.jsadd.wasm

2. 在Flutter Web项目中集成Wasm

将生成的add.jsadd.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:htmldart:js库,你仍然可以实现与原生代码的交互。这种方法需要更多的手动设置和对Web技术的理解,但它提供了在Web平台上使用原生代码的强大能力。

回到顶部