跳到主内容

使用 FFI 绑定到原生代码

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

Flutter 应用程序可以使用 dart:ffi 库来调用原生 API。FFI 代表 外部函数接口。其他类似功能的术语包括 原生接口语言绑定。

从 Flutter 3.38 开始,绑定到原生代码的推荐方法是使用 flutter create --template=package_ffi 命令。此模板使用 构建钩子build.dart 脚本中配置原生构建,不再需要特定于操作系统的构建文件。这种方法适用于 Flutter 和 Dart 独立项目。

如果您需要使用 Flutter 插件 API,或者需要在 Android 上配置 Google Play 服务运行时,请使用标准插件模板 (flutter create --template=plugin)。

创建 FFI 包

#

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

flutter create --template=package_ffi native_add
cd native_add

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

  • lib/native_add.dart:定义包 API 的 Dart 代码。
  • lib/src/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 钩子。

原生代码

#

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

构建钩子

#

原生代码与您的应用程序一起自动编译和捆绑。这是由 hook/build.dart 脚本完成的,该脚本是一个 构建钩子

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

构建钩子使用 package:native_toolchain_c 将 C 代码编译成动态库。您可以自定义此文件以构建其他原生语言或下载预编译的二进制文件。

Dart 代码

#

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

生成绑定

#

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

这将生成 lib/src/native_add_bindings_generated.dart

调用原生函数

#

lib/src/native_add_bindings_generated.dart 中的生成的绑定包含 @Native() external 函数。这些函数在运行时针对构建钩子输出的代码资产自动解析(构建时运行)。这意味着不需要针对 dlopen 动态库的特定于操作系统的逻辑,从而使 Dart 代码真正跨平台。

主库文件 lib/native_add.dart 暴露这些函数。您的应用程序可以通过导入 package:native_add/native_add.dart 来调用这些函数。

测试

#

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

其他用例

#

系统库

#

要链接到系统库,您需要修改 build.dart 钩子以指定链接模式。与其编译源代码,不如创建一个 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.dart, windows.dart) 将包含使用这些系统库中符号的 external 函数。

闭源库

#

您还可以使用构建钩子链接到预编译的闭源库。推荐的方法是在构建时下载预编译的二进制文件并使用文件哈希验证其完整性。

在您的 build.dart 钩子中,您将

  1. 从 URL 下载库。
  2. 验证下载文件的哈希值。
  3. 将库放置在构建输出目录中。
  4. 创建一个 CodeAsset,其 DynamicLoading 指向该库。

以下是 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 包示例

动态库命名指南

#

在实现用于捆绑代码资产的 build.dart 钩子时,确保在所有目标架构和 SDK 上动态库命名一致至关重要。

在 Apple 平台 (iOS 和 macOS) 上,动态库被捆绑到框架中。Flutter 的构建系统依赖于这些名称来生成元数据和打包可分发格式,例如 XCFramework。

跨架构的一致性

#

对于给定的资产 ID,您的钩子将多次被调用,一次每个架构。您的钩子必须为目标架构(例如,arm64x64)生成相同的文件名。

  • 为什么? 在单个 SDK 构建中,Flutter 使用 lipo 将特定于架构的二进制文件组合成单个通用 (fat) 二进制文件。如果架构具有不同的文件名,该工具将非确定性地选择一个并发出警告。此外,如果动态库被重命名,运行时错误消息将让您的用户感到困惑。
  • 推荐操作:避免在文件名中添加架构后缀(例如,使用 libsqlite3.dylib 而不是 libsqlite3_arm64.dylib)。相反,将文件写入 input.outputDirectory(每个架构唯一)或 input.outputDirectoryShared 的特定于架构的子目录(例如,input.outputDirectoryShared.resolve('$architecture/'))。

跨 SDK 的一致性 (iOS)

#

在为 iOS 构建时,您的钩子将使用 SDK 和架构的不同值多次被调用。物理设备 (iphoneos) 和模拟器 (iphonesimulator) 调用都必须为相同的资产 ID 生成相同的框架名称。

  • 为什么? Flutter 使用 xcodebuild -create-xcframework 将这些输出组合在一起。Xcode 要求 XCFramework 内的所有平台切片共享相同的框架名称,以允许无缝链接。如果文件名不同,Flutter 工具将无法创建正确的 XCFramework,并且像 flutter build ios-framework 这样的命令将失败。
  • 推荐操作:不要为模拟器构建使用 _sim_simulator 等后缀。XCFramework 结构已经通过内部处理平台分离(例如,MyLib.xcframework/ios-arm64_x86_64-simulator/MyLib.framework)。相反,将文件写入 input.outputDirectory(每个 SDK 唯一)或 input.outputDirectoryShared 的特定于 SDK 的子目录。

资源集的一致性

#

您的钩子必须为给定目标平台的所有 SDK 生成相同的资产 ID 集。

  • 为什么? Apple 的构建系统和 App Store 验证要求包含在应用程序中的所有框架与目标设备兼容。如果您为模拟器 (iphonesimulator) 生成资产,但未为物理设备 (iphoneos) 生成资产,则生成的 XCFramework 将包含一个没有设备对应项的切片。这可能导致构建失败或 Apple 拒绝包含仅模拟器二进制文件的设备构建的应用程序。
  • 推荐操作:确保您的 build.dart 钩子逻辑一致地处理所有受支持的 SDK。如果您为一种 SDK 生成资产,则必须为该平台的所有其他 SDK 生成相应的资产。对于特定于 SDK 的代码,您可以对其他 SDK 使用存根实现。