dart-use-ffigen

dart-use-ffigen

热门

指导 Agent 使用 `package:ffigen` 自动生成 FFI 绑定,而非手动编写。当任务涉及编写新的 FFI 绑定、扩展 C/Objective-C/Swift 集成,或替换手写的 `dart:ffi` 配置时,请使用此 Skill。

2792Star
164Fork
更新于 2026/8/5
SKILL.md
只读
名称
dart-use-ffigen
描述

指导 Agent 使用 `package:ffigen` 自动生成 FFI 绑定,而非手动编写。当任务涉及编写新的 FFI 绑定、扩展 C/Objective-C/Swift 集成,或替换手写的 `dart:ffi` 配置时,请使用此 Skill。

使用 package:ffigen 生成 FFI 绑定

目录

简介

使用 package:ffigenFfiGenerator)实现 FFI 绑定的自动化与标准化生成。手写 FFI 绑定不仅容易出错、难以维护,而且极不推荐。

约束条件

  • 禁止手写 FFI 绑定:只要存在原生头文件(.h 文件)或是通过构建步骤生成的,就绝对不要手动编写 DynamicLibrary.lookup@Native 外置函数或原始 struct 类。请始终使用 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 已集成到标准运行时流程中,或通过原生钩子(native hooks)编译原生资产:
    • 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:启用原生资产摇树优化(Tree Shaking)所需的元数据生成(在 dart-lang/native 中至关重要)。设置为 (_) => true

4. Output

配置目标生成文件。

  • dartFile:写入主 FFI 绑定文件的目标 Uri
  • recordUseMapping:使用记录元数据映射的目标 Uri(对链接阶段的摇树优化至关重要)。
  • preamble:插入到生成文件顶部的文本(协议声明、注解等)。
  • format:设置为 true 可自动运行 Dart 格式化工具。

分步操作流程

步骤 1:检查/添加依赖

打开 Package 的 pubspec.yaml,确认 dev_dependencies 中已包含 ffigen。可以通过 Dart MCP server 查询或直接在 pub.dev 上查找最新版本(例如 ^20.1.1)。

也可以通过 CLI 命令行自动添加:

dart pub add dev:ffigen

步骤 2:动态构建路径

在 Package 的 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:运行代码生成

在目标 Package 根目录下打开终端并执行脚本:

dart run tool/ffigen.dart

步骤 5:静态代码分析

验证生成的绑定代码是否正确,并修复相关的分析问题。FFIgen 会在输出文件上自动运行 Dart 格式化工具(配置了 format: true),因此无需手动格式化。

  1. 在目标 Package 目录下运行静态分析器:
    dart analyze
    
  2. 处理警告/Lints 规范提示:如果 dart analyze 在生成的文件中报告了代码风格或 lint 警告,请将对应的警告代号追加到生成器脚本 preamble 配置中的 ignore_for_file: 列表中(例如添加 camel_case_typesnon_constant_identifier_names 等)。切勿修改 Package 的全局规则。
  3. 处理编译错误:如果 dart analyze 在生成的文件中报告了实际的编译器或分析错误(而非普通警告),请勿尝试手动修改生成的文件。请直接将错误详情反馈给用户,以便他们在 github.com/dart-lang/native 仓库提交 Issue。

具体示例:绑定 C 语言库

假设我们正在操作 pkgs/code_assets/example/sqlite 下的 SQLite Package,该 Package 在 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';

// 缺陷 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),
      // 对 Package 优化和摇树优化至关重要
      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.');
}

运行生成器脚本

在 Package 根目录下运行此命令:

dart run tool/ffigen.dart

这将自动生成:

  1. lib/src/third_party/sqlite3.g.dart
  2. lib/src/third_party/sqlite3.record_use_mapping.g.dart

验证清单

在完成绑定代码生成任务前,请务必执行以下检查:

  1. 配置无误:确认生成的标的文件已存放在 lib/src/third_party/ 目录中(第三方授权代码必须存放在此),且主 FFI 绑定文件严格使用 .g.dart 后缀。
  2. 静态分析:运行 dart analyze,确保整个 Package 中没有任何编译器/分析器错误或警告。
    • 若在生成的绑定代码中报告了静态警告,请通过在生成器 preamble 配置中添加 ignore_for_file 规则来忽略它们(不要修改全局 Package 规则)。
    • 若在生成的绑定代码中报告了实际的编译器或分析器错误,请勿手动编辑生成的文件。请向用户报告详细信息,并引导他们在 github.com/dart-lang/native 提交 Issue。