mcpField
D
dart-use-ffigen

dart-use-ffigen

github.com/flutter开发3.1k14.4k 次安装2 天前更新
代码仓库

关于 dart-use-ffigen

面向 Dart 开发者的 FFI 绑定生成指南,介绍如何使用 `package:ffigen` 从原生头文件生成绑定。内容覆盖生成器配置、文件组织、符号筛选和静态分析验证。

主要功能

  1. 01从头文件生成 Dart FFI 绑定
  2. 02配置函数与头文件筛选
  3. 03组织生成脚本和输出文件
  4. 04生成符号使用映射
  5. 05运行静态分析检查绑定

适用场景

  1. 01为 C 库创建 Dart 绑定
  2. 02替换手写的原生函数绑定
  3. 03将绑定生成纳入项目流程
  4. 04检查生成代码及分析问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/flutter/agent-plugins --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 配置),因此无需手动格式化。

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

这将自动创建:

  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 提交 issue。

评论

支持 Markdown