关于 dart-use-ffigen
面向 Dart 开发者的技能,指导使用 `package:ffigen` 生成原生 FFI 绑定,并规范配置、文件位置、许可信息和验证流程,也涵盖树摇所需的使用记录元数据。
主要功能
- 01使用 FfiGenerator 生成绑定
- 02精确筛选原生声明
- 03配置输出位置与许可前言
- 04生成使用记录映射
- 05运行生成器和 Dart 分析
适用场景
- 01为 C 库创建 Dart 绑定
- 02为原生资产树摇添加符号元数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dart-lang/ skills --skill dart-use-ffigen安装后在 Claude Code 中输入/dart-use-ffigen确认已加载。
详细介绍
使用 package:ffigen 生成 FFI 绑定
目录
简介
使用 package:ffigen(FfiGenerator)自动生成并标准化 FFI 绑定。手动编写 FFI 绑定容易出错、脆弱,因此强烈不建议这样做。
约束
- 不得手动编写 FFI 绑定:如果原生头文件(
.h文件)存在或由构建步骤生成,切勿手动编写DynamicLibrary.lookup、@Native外部函数或原始结构体类。始终使用FfiGenerator生成。 - 生成器位置:生成器脚本应位于目标 package 根目录下的
tool/ffigen.dart。 - 头文件位置:如果原生头文件属于第三方,应将其放在目标 package 下的
third_party/中(也可以放在 package 根目录下的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验证 package。 - 记录使用情况与 Tree Shaking:如果 package 集成到标准运行时执行中,或通过原生钩子编译原生资源:
- 在
Functions下设置recordUse: (_) => true,为所有函数启用使用情况记录。 - 在
Output中指定recordUseMapping目标(该目标必须严格是lib/src/third_party/下的.g.dart文件,例如lib/src/third_party/sqlite3.record_use_mapping.g.dart),以注册绑定,供符号 Tree Shaking 使用。
- 在
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:如果函数不会回调 Dart 或阻塞线程执行,则将其声明为叶函数((decl) => true)。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 - 处理警告/Lint:如果
dart analyze报告生成文件中存在样式或 lint 警告,请将相应的警告代码追加到生成器脚本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 软件包,其中包含位于 third_party/sqlite/ 中的 SQLite C 库源代码,并通过 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';
// Flaw 1: Hardcoded DynamicLibrary lookup blocks integration with modern native asset compilation.
final ffi.DynamicLibrary _dylib = ffi.DynamicLibrary.open('libsqlite3.so');
// Flaw 2: Manual function type matching requires writing redundant dynamic lookup boilerplate and lacks tree-shaking metadata.
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() {
// Resolve paths dynamically relative to 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),
// Essential for package optimization and tree-shaking
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 提交 issue。
- 如果生成的绑定报告了静态警告,请在生成器的