指导智能体使用 `package:ffigen` 自动生成 FFI 绑定,而非手动编写。当任务涉及编写新的 FFI 绑定、扩展 C/Objective-C/Swift 集成或替换手写的 `dart:ffi` 设置时,使用此技能。
使用 package:ffigen 生成 FFI 绑定
目录
简介
使用 package:ffigen(FfiGenerator)自动化和标准化 FFI 绑定的生成。手动编写 FFI 绑定容易出错、脆弱且强烈不推荐。
约束条件
- 禁止手写 FFI 绑定:如果存在原生头文件(
.h文件)或由构建步骤生成,切勿手动编写DynamicLibrary.lookup、@Native外部函数或原始结构体类。始终使用FfiGenerator生成它们。 - 生成器位置:生成器脚本应位于目标包根目录下的
tool/ffigen.dart。 - 头文件位置:如果原生头文件是第三方的,应位于目标包内的
third_party/目录下(否则放在包根目录的src/目录下也可接受)。如果头文件不在这些标准位置之一,请通知用户将头文件移动到标准位置(例如third_party/)会更整洁。 - 精确包含过滤:除非特别需要,避免导入整个原生库。始终使用正向匹配应用精确的包含列表,以最小化生成代码的大小和认知负担(例如使用
Functions.includeSet或在include闭包中过滤匹配)。 - 输出设置:如果生成的 FFI 绑定与第三方库交互(或引用第三方头文件),生成的文件必须始终放在
lib/src/third_party/下。主要的 FFI 绑定文件必须严格使用.g.dart扩展名(例如sqlite3.g.dart)。 - 前言与许可头:始终在
Output类中提供优质preamble以指定许可。这必须与第三方原生库的许可一致,明确包含目标原生头文件的版权头,并包含自动生成警告(例如// Generated by package:ffigen. Do not edit manually.)。 - 不提交过时的绑定:确保在完成任务前运行生成器脚本并检查生成的文件是否已更改。始终通过运行
dart analyze验证包。 - 记录使用与摇树优化:如果包集成到标准运行时执行或通过原生钩子编译原生资源:
- 在
Functions下设置recordUse: (_) => true以启用所有函数的记录使用。 - 在
Output中指定recordUseMapping目标(必须严格是lib/src/third_party/下的.g.dart文件,例如lib/src/third_party/sqlite3.record_use_mapping.g.dart)以注册绑定用于符号摇树优化。
- 在
FFIgen 概述
要构建程序化生成器,使用从 package:ffigen/ffigen.dart 导入的核心配置对象:
1. FfiGenerator
协调配置、解析和代码生成的父类。
FfiGenerator({
Headers headers = const Headers(),
Enums enums = Enums.excludeAll,
Functions functions = Functions.excludeAll,
Globals globals = Globals.excludeAll,
Integers integers = const Integers(),
Macros macros = Macros.excludeAll,
Structs structs = Structs.excludeAll,
Typedefs typedefs = Typedefs.excludeAll,
Unions unions = Unions.excludeAll,
UnnamedEnums unnamedEnums = UnnamedEnums.excludeAll,
ObjectiveC? objectiveC,
required Output output,
}).generate();
2. Headers
配置 Clang 头文件解析目标和编译器标志。
entryPoints:目标头文件Uri输入列表。include:过滤函数bool Function(Uri header),处理传递性头文件导入。compilerOptions:自定义预处理器/包含编译器标志,直接传递给 libclang。ignoreSourceErrors:设置为true以静默解析过程中第三方头文件内部的错误。
3. Functions
指定要在 Dart 中暴露哪些原生 C/C++ 函数。
include:匹配函数(例如(decl) => {'my_func'}.contains(decl.originalName)或Functions.includeSet({'my_func'}))。isLeaf:将函数声明为叶函数((decl) => true),如果它们不回调 Dart 或阻塞线程执行。recordUse:启用原生资源摇树优化的元数据生成(在dart-lang/native中至关重要)。设置为(_) => true。
4. Output
配置目标生成文件。
dartFile:主要 FFI 绑定将写入的目标Uri。recordUseMapping:记录使用元数据映射的目标Uri(对链接时摇树优化至关重要)。preamble:插入生成文件顶部的文本(许可、注释)。format:设置为true以自动运行 Dart 格式化器。
逐步工作流程
步骤 1:检查/添加依赖
打开包的 pubspec.yaml 并验证 dev_dependencies 包含 ffigen。使用 Dart MCP 服务器或在 pub.dev 上查找最新版本(例如 ^20.1.1)。
您可以使用 CLI 自动添加:
dart pub add dev:ffigen
步骤 2:动态构建路径
在包的 tool/ 目录下创建程序化生成器脚本(例如 tool/ffigen.dart)。
相对于 Platform.script 解析路径,以确保从任何工作目录成功运行:
final packageRoot = Platform.script.resolve('../');
final headerFile = packageRoot.resolve('third_party/library.h');
final targetBindings = packageRoot.resolve('lib/src/third_party/bindings.g.dart');
步骤 3:编写脚本(tool/ffigen.dart)
定义 void main() 并使用动态选项运行 FfiGenerator(参见下面的完整示例)。
步骤 4:运行代码生成
在目标包文件夹内的终端中执行脚本:
dart run tool/ffigen.dart
步骤 5:静态分析
验证生成的绑定是否正确并解决任何分析问题。FFIgen 会自动对输出文件运行 Dart 格式化器(通过 format: true 配置),因此无需手动格式化。
- 在目标包内运行静态分析器:
dart analyze - 处理警告/提示:如果
dart analyze报告生成文件中的样式或提示警告,将相应的警告代码附加到生成器脚本preamble配置中的ignore_for_file:列表(例如添加camel_case_types、non_constant_identifier_names等)。不要修改包的全局规则。 - 处理编译错误:如果
dart analyze报告生成文件中的实际编译器或分析器错误(而非警告),不要尝试手动编辑生成的文件。直接向用户报告这些错误详情,以便他们可以在 github.com/dart-lang/native 仓库提交问题。
具体示例:绑定 C 库
假设我们正在处理 pkgs/code_assets/example/sqlite 下的 SQLite 包,该包将 SQLite C 库源代码嵌入 third_party/sqlite/ 中,并通过 FFI 访问它。
C 头文件(third_party/sqlite/sqlite3.h)
// The author disclaims copyright to this source code.
#ifndef SQLITE3_H_
#define SQLITE3_H_
const char *sqlite3_libversion(void);
#endif // SQLITE3_H_
之前:手动 FFI 绑定(反模式)
开发者可能尝试手工制作此集成。它脆弱、阻碍摇树优化元数据,并且极易出现 ABI 和结构映射问题:
// lib/src/sqlite3_manual.dart
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';
// 缺陷 1:硬编码的 DynamicLibrary 查找阻碍与现代原生资源编译的集成。
final ffi.DynamicLibrary _dylib = ffi.DynamicLibrary.open('libsqlite3.so');
// 缺陷 2:手动函数类型匹配需要编写冗余的动态查找样板代码,且缺乏摇树优化元数据。
typedef _sqlite3_libversion_C = ffi.Pointer<ffi.Char> Function();
typedef _sqlite3_libversion_Dart = ffi.Pointer<ffi.Char> Function();
final _sqlite3_libversion_Dart sqlite3LibVersion = _dylib
.lookup<ffi.NativeFunction<_sqlite3_libversion_C>>('sqlite3_libversion')
.asFunction();
之后:通过 FFIgen 生成(正确模式)
在 tool/ffigen.dart 创建程序化脚本:
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:ffigen/ffigen.dart';
void main() {
// 相对于 Platform.script 动态解析路径
final packageRoot = Platform.script.resolve('../');
final entryHeader = packageRoot.resolve('third_party/sqlite/sqlite3.h');
final bindingsOutput = packageRoot.resolve('lib/src/third_party/sqlite3.g.dart');
final treeShakeMapping = packageRoot.resolve('lib/src/third_party/sqlite3.record_use_mapping.g.dart');
FfiGenerator(
headers: Headers(
entryPoints: [entryHeader],
),
functions: Functions(
include: (decl) => {'sqlite3_libversion'}.contains(decl.originalName),
// 对包优化和摇树优化至关重要
recordUse: (_) => true,
),
output: Output(
dartFile: bindingsOutput,
recordUseMapping: treeShakeMapping,
format: true,
preamble: '''
// AUTO-GENERATED FILE - DO NOT MODIFY.
// Generated via ffigen.
// To regenerate: dart run tool/ffigen.dart
// ignore_for_file: type=lint, unused_import, unused_element, deprecated_member_use_from_same_package, experimental_member_use
''',
),
).generate();
print('Successfully generated sqlite3 FFI bindings.');
}
运行生成器脚本
在包根目录中运行:
dart run tool/ffigen.dart
这将自动创建:
lib/src/third_party/sqlite3.g.dartlib/src/third_party/sqlite3.record_use_mapping.g.dart
验证清单
在完成绑定生成任务之前,始终执行以下验证:
- 正确设置:验证目标生成文件位于
lib/src/third_party/内(第三方许可代码必需),且主要 FFI 绑定文件严格使用.g.dart扩展名。 - 静态分析:运行
dart analyze并确保包中没有编译器/分析器错误或警告。- 如果生成绑定中报告了静态警告,通过向生成器的
preamble配置添加ignore_for_file规则来抑制它们(不要修改全局包规则)。 - 如果生成绑定中报告了实际的编译器或分析器错误,不要手动编辑生成的文件。向用户报告详情,并引导他们在 github.com/dart-lang/native 提交问题。
- 如果生成绑定中报告了静态警告,通过向生成器的






