dart-use-ffigen

dart-use-ffigen

熱門

指引 Agent 使用 `package:ffigen` 自動生成 FFI 綁定(bindings),無需手動撰寫。當任務需要建立新的 FFI 綁定、擴充 C/Objective-C/Swift 整合,或是替換手動撰寫的 `dart:ffi` 設定時,請使用此 skill。

2792星標
164分支
更新於 2026/8/5
SKILL.md
唯讀
名稱
dart-use-ffigen
描述

指引 Agent 使用 `package:ffigen` 自動生成 FFI 綁定(bindings),無需手動撰寫。當任務需要建立新的 FFI 綁定、擴充 C/Objective-C/Swift 整合,或是替換手動撰寫的 `dart:ffi` 設定時,請使用此 skill。

使用 package:ffigen 生成 FFI 綁定

目錄

簡介

使用 package:ffigen (FfiGenerator) 自動化並標準化 FFI 綁定的生成。手動撰寫 FFI 綁定容易出錯且維護困難,極力不建議使用。

限制事項

  • 禁止手動撰寫 FFI 綁定:如果原生標頭檔(.h 檔案)已存在或由建置步驟生成,切勿手動撰寫 DynamicLibrary.lookup@Native 外部函式或原始 struct 類別。請一律使用 FfiGenerator 來生成。
  • 生成器位置:生成器腳本應位於目標 package 根目錄下的 tool/ffigen.dart
  • 標頭檔位置:如果原生標頭檔屬於第三方,應放在目標 package 的 third_party/ 目錄中(若放在 package 根目錄下的 src/ 目錄也可以接受)。如果標頭檔未處於這些標準位置之一,請提示使用者將標頭檔移動到標準位置(例如 third_party/)會更加整潔。
  • 精準的包含過濾器(Inclusion Filters):除非明確需要,否則避免匯入整個原生函式庫。一律使用正向匹配(positive matches)來套用精確的包含清單,以縮減生成程式碼的體積並降低閱讀負擔(例如使用 Functions.includeSet,或在 include 閉包中過濾匹配項)。
  • 輸出設定:若生成的 FFI 綁定是用於對接第三方函式庫(或參考了第三方標頭檔),生成的檔案必須一律放置於 lib/src/third_party/ 下。主要的 FFI 綁定生成檔必須嚴格使用 .g.dart 副檔名(例如 sqlite3.g.dart)。
  • Preamble 與授權標頭:務必在 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 標頭檔解析目標與編譯器旗標(compiler flags)。

  • entryPoints:目標標頭檔的 Uri 輸入清單。
  • include:處理遞移性標頭檔匯入(transitive header imports)的過濾函式 bool Function(Uri header)
  • compilerOptions:直接傳遞給 libclang 的自訂預處理器(preprocessor)或 include 編譯器旗標。
  • ignoreSourceErrors:設定為 true 可忽略解析第三方標頭檔內部發生的錯誤。

3. Functions

指定要暴露給 Dart 的原生 C/C++ 函式。

  • include:比對函式(例如 (decl) => {'my_func'}.contains(decl.originalName)Functions.includeSet({'my_func'}))。
  • isLeaf:若函式不會回傳 Dart 或不會阻塞執行緒,可將其宣告為 leaf 函式((decl) => true)。
  • recordUse:啟用原生資產 tree shaking 所需的元資料生成(在 dart-lang/native 中至關重要)。設定為 (_) => true

4. Output

設定生成的目標檔案。

  • dartFile:寫入主要 FFI 綁定的目標 Uri
  • recordUseMapping:記錄使用狀態元資料對照表的目標 Uri(對連結階段的 tree shaking 至關重要)。
  • preamble:插入生成檔案頂部的文字(授權宣告、註解等)。
  • format:設定為 true 可自動執行 Dart 程式碼格式化工具(Dart formatter)。

逐步工作流程

步驟 1:檢查並新增相依套件

開啟 package 的 pubspec.yaml,確認 dev_dependencies 中包含 ffigen。可以使用 Dart MCP 伺服器,或在 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. 處理警告/Lint 提醒:若 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 將 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 綁定(反模式)

開發者可能會嘗試手動撰寫此整合。這種做法十分脆弱,會阻礙 tree-shaking 元資料生成,且非常容易出現 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:手動比對函式型別需要撰寫多餘的動態尋找樣板程式碼,且缺乏 tree-shaking 元資料。
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.');
}

執行生成器腳本

在 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。