跳转至正文

使用 FFI 绑定原生代码

若要在 Flutter 程序中使用原生代码,请使用 dart:ffi 库及 package_ffi 模板。

Flutter 应用可以使用 dart:ffi 库调用原生 API。FFI 表示 foreign function interface(外部函数接口)。功能相近的其他说法还包括 原生接口语言绑定

自 Flutter 3.38 起,推荐通过 flutter create --template=package_ffi 命令绑定原生代码。该模板使用 build hooksbuild.dart 脚本中配置原生构建,不再需要面向各操作系统的专用构建文件。此方式同时适用于 Flutter 与独立的 Dart 项目。

若你需要使用 Flutter Plugin API,或在 Android 上配置 Google Play services 运行时,请使用标准插件模板(flutter create --template=plugin)。

创建 FFI 包

#

要创建 FFI 包,请运行以下命令:

flutter create --template=package_ffi native_add
cd native_add

这将创建一个包含以下专用内容的包:

  • lib/native_add.dart:定义该包 API 的 Dart 代码。

  • lib/native_add_bindings_generated.dart:为原生代码生成的 Dart 绑定。

  • src/native_add.c:原生 C 源代码。

  • src/native_add.h:原生代码的 C 头文件。

  • hook/build.dart:由 Flutter SDK 运行以编译原生代码的脚本。

  • ffigen.yaml:供 package:ffigen 生成 Dart 绑定的配置文件。

  • pubspec.yaml:包定义文件,用于启用 build.dart hook。

原生代码

#

原生代码位于 src/native_add.csrc/native_add.h。C 函数 sum 定义在 .c 文件中,其签名在头文件中。该函数被标记为导出,以便从 Dart 调用。

构建 hook

#

原生代码会自动编译并打包进你的应用。这由 hook/build.dart 脚本完成,它是一个 build hook

这意味着你不再需要编写面向各操作系统的构建文件(例如 Linux/Windows 的 CMakeLists.txt、iOS/macOS 的 .podspec,或 Android 的 build.gradle)来编译原生代码。

构建 hook 使用 package:native_toolchain_c 将 C 代码编译为动态库。你可以自定义该文件以构建其他原生语言,或下载预编译二进制文件。

Dart 代码

#

Dart 代码定义该包的公共 API。

生成绑定

#

要绑定原生代码,模板使用 package:ffigen 从头文件(src/native_add.h)生成绑定。生成配置在 ffigen.yaml 中。

这将生成 lib/native_add_bindings_generated.dart

调用原生函数

#

lib/native_add_bindings_generated.dart 中的生成绑定包含 @Native() external 函数。这些函数在运行时自动解析为构建 hook(在构建时运行)输出的 code asset。这意味着无需为 dlopen 动态库编写面向各操作系统的逻辑,使 Dart 代码真正跨平台。

主库文件 lib/native_add.dart 对外暴露这些函数。你的应用随后可通过导入 package:native_add/native_add.dart 调用它们。

测试

#

生成的包在 test/native_add_test.dart 中包含单元测试,演示如何测试原生函数。

其他用例

#

系统库

#

要链接系统库,请修改 build.dart hook 以指定链接模式。不再编译源代码,而是创建 CodeAsset 并设置其 linkMode

在 Android、iOS、Linux 和 macOS 上,对许多系统库可使用 LookupInProcess() 在主进程中查找符号。

在 Windows 上,通常使用 DynamicLoadingSystem() 并提供 DLL 名称。

以下是一个链接系统库以获取主机名的 build.dart 示例:

dart
// hook/build.dart
import 'package:hooks/hooks.dart';
import 'package:code_assets/code_assets.dart';

void main(List<String> args) async {
  await build(args, (input, output) async {
    final targetOS = input.target.os;
    switch (targetOS) {
      case OS.android || OS.iOS || OS.linux || OS.macOS:
        output.assets.code.add(
          CodeAsset(
            package: 'host_name',
            name: 'src/third_party/unix.dart',
            linkMode: LookupInProcess(),
          ),
        );
      case OS.windows:
        output.assets.code.add(
          CodeAsset(
            package: 'host_name',
            name: 'src/third_party/windows.dart',
            linkMode: DynamicLoadingSystem(Uri.file('ws2_32.dll')),
          ),
        );
      default:
        throw Exception('Unsupported target os: $targetOS');
    }
  });
}

随后 Dart 文件(unix.dartwindows.dart)将包含使用这些系统库符号的 external 函数。

在 Android 上打包 libc++_shared.so

#

尽管 libc++_shared.so 随 Android NDK 提供,它并非系统库。若你的应用或包使用 C++ 标准库,或包含依赖它的 多个共享库,你的应用需要打包 libc++_shared.so

要在应用中打包该库,请添加对 package:android_libcpp_shared 的依赖;该包使用自己的 build hook,从本地安装的 NDK 为各目标架构打包 libc++_shared.so

闭源库

#

你也可以使用 build hook 链接预编译的闭源库。推荐做法是在构建时下载预编译二进制文件,并通过文件哈希校验其完整性。

In your build.dart hook, you would:

  1. Download the library from a URL.
  2. Verify the hash of the downloaded file.
  3. Place the library in the build output directory.
  4. Create a CodeAsset with DynamicLoading pointing to the library.

build.dart hook 中,你需要:

  1. 从 URL 下载库。
  2. 校验已下载文件的哈希。
  3. 将库放入构建输出目录。
  4. 创建指向该库的 DynamicLoadingCodeAsset

以下是创建 CodeAsset 的简化示例:

dart
// hook/build.dart
import 'package:hooks/hooks.dart';
import 'package:code_assets/code_assets.dart';

void main(List<String> args) async {
  await build(args, (input, output) async {
    // 1. Download the library from a URL.
    // 2. Verify the hash of the downloaded file.
    // 3. Place the library in the build output directory.

    output.assets.code.add(
      CodeAsset(
        package: input.packageName,
        name: 'src/my_lib.dart', // Dart file with bindings
        linkMode: DynamicLoadingBundled(),
        file: input.outputDirectory.resolve('my_lib.so'),
      ),
    );
  });
}

你需要为不同架构和平台准备不同版本的预编译库。

更多示例请参阅 code_assets 包示例

动态库命名指南

#

为打包 code asset 的包实现 build.dart hook 时,务必在所有目标架构和 SDK 上为动态库保持一致的命名。

在 Apple 平台(iOS 和 macOS)上,动态库被打包进 framework。Flutter 的构建系统依赖这些名称生成元数据,并打包 XCFrameworks 等可分发格式。

跨架构一致性

#

对于给定的 asset ID,你的 hook 会被多次调用,每个架构一次。无论目标架构如何(例如 arm64x64),hook 必须生成相同的文件名。

  • 原因? 在单次 SDK 构建中,Flutter 使用 lipo 将各架构的二进制合并为单个通用(fat)二进制。若各架构文件名不同,工具会非确定性地选取其一并发出警告。此外,若动态库被重命名,运行时错误信息会让用户困惑。

  • 建议做法:避免在文件名中添加架构后缀(例如使用 libsqlite3.dylib 而非 libsqlite3_arm64.dylib)。改为将文件写入 input.outputDirectory(每个架构唯一),或写入 input.outputDirectoryShared 下按架构划分的子目录(例如 input.outputDirectoryShared.resolve('$architecture/'))。

跨 SDK 一致性(iOS)

#

为 iOS 构建时,你的 hook 会针对不同的 SDK 和架构被多次调用。真机(iphoneos)与模拟器(iphonesimulator)的调用必须为同一 asset ID 生成相同的 framework 名称。

  • 原因? Flutter 使用 xcodebuild -create-xcframework 合并这些输出。Xcode 要求 XCFramework 内所有平台 slice 共享同一 framework 名称以实现无缝链接。若文件名不同,Flutter 工具无法创建正确的 XCFramework,flutter build ios-framework 等命令会失败。

  • 建议做法:模拟器构建不要使用 _sim_simulator 等后缀。XCFramework 结构已在内部处理平台分离(例如 MyLib.xcframework/ios-arm64_x86_64-simulator/MyLib.framework)。改为将文件写入 input.outputDirectory(每个 SDK 唯一),或写入 input.outputDirectoryShared 下按 SDK 划分的子目录。

asset 集合的一致性

#

对于给定目标平台,你的 hook 必须在所有 SDK 上生成相同的 Asset ID 集合。

  • 原因? Apple 的构建系统与 App Store 校验要求应用内所有 framework 与目标设备兼容。若你为模拟器(iphonesimulator)生成 asset 但未为真机(iphoneos)生成,得到的 XCFramework 会包含在设备上无对应项的 slice。这可能导致构建失败,或 Apple 因设备构建包含仅模拟器二进制而拒绝应用。

  • 建议做法:确保 build.dart hook 逻辑一致处理所有受支持的 SDK。若为一个 SDK 生成 asset,必须为该平台所有其他 SDK 生成对应 asset。对于 SDK 专用代码,可为其他 SDK 使用桩实现。