使用 FFI 绑定原生代码
若要在 Flutter 程序中使用原生代码,请使用 dart:ffi 库及 package_ffi 模板。
Flutter 应用可以使用 dart:ffi 库调用原生 API。FFI 表示 foreign function interface(外部函数接口)。功能相近的其他说法还包括 原生接口 和 语言绑定。
自 Flutter 3.38 起,推荐通过
flutter create --template=package_ffi 命令绑定原生代码。该模板使用
build hooks 在
build.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.darthook。
原生代码
#
原生代码位于 src/native_add.c 与 src/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 示例:
// 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 函数。
在 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:
- Download the library from a URL.
- Verify the hash of the downloaded file.
- Place the library in the build output directory.
- Create a
CodeAssetwithDynamicLoadingpointing to the library.
在 build.dart hook 中,你需要:
- 从 URL 下载库。
- 校验已下载文件的哈希。
- 将库放入构建输出目录。
- 创建指向该库的
DynamicLoading的CodeAsset。
以下是创建 CodeAsset 的简化示例:
// 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 会被多次调用,每个架构一次。无论目标架构如何(例如 arm64 与 x64),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.darthook 逻辑一致处理所有受支持的 SDK。若为一个 SDK 生成 asset,必须为该平台所有其他 SDK 生成对应 asset。对于 SDK 专用代码,可为其他 SDK 使用桩实现。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。