dart-use-ffigen

dart-use-ffigen

热门

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

406Star
26Fork
更新于 2026/7/20
SKILL.md
readonly只读
name
dart-use-ffigen
description

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

使用 package:ffigen 生成 FFI 绑定

目录

简介

使用 package:ffigenFfiGenerator)自动化和标准化 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 配置),因此无需手动格式化。

  1. 在目标包内运行静态分析器:
    dart analyze
    
  2. 处理警告/提示:如果 dart analyze 报告生成文件中的样式或提示警告,将相应的警告代码附加到生成器脚本 preamble 配置中的 ignore_for_file: 列表(例如添加 camel_case_typesnon_constant_identifier_names 等)。不要修改包的全局规则。
  3. 处理编译错误:如果 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

这将自动创建:

  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 并确保包中没有编译器/分析器错误或警告。
    • 如果生成绑定中报告了静态警告,通过向生成器的 preamble 配置添加 ignore_for_file 规则来抑制它们(不要修改全局包规则)。
    • 如果生成绑定中报告了实际的编译器或分析器错误,不要手动编辑生成的文件。向用户报告详情,并引导他们在 github.com/dart-lang/native 提交问题。