指引 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 設定),因此無需手動格式化。
- 在目標 package 內執行靜態分析器:
dart analyze - 處理警告/Lint 提醒:若
dart analyze在生成的檔案中回報程式碼風格或 lint 警告,請將對應的警告代碼附加至生成器腳本preamble設定中的ignore_for_file:清單(例如加入camel_case_types、non_constant_identifier_names等)。請勿修改 package 的全域規則。 - 處理編譯錯誤:若
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
這將會自動建立:
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,並確保 package 中完全沒有編譯器/分析器錯誤或警告。- 若在生成的綁定中出現靜態警告,請在生成器的
preamble設定中加入ignore_for_file規則來抑制警告(請勿修改 package 的全域規則)。 - 若在生成的綁定中出現實際的編譯器或分析器錯誤,請勿手動修改生成檔。請將詳細資訊回報給使用者,並引導他們至 github.com/dart-lang/native 提交 issue。
- 若在生成的綁定中出現靜態警告,請在生成器的






