引導代理使用 `package:ffigen` 自動產生 FFI 繫結,而非手動撰寫。當任務涉及撰寫新的 FFI 繫結、擴充 C/Objective-C/Swift 整合,或取代手刻的 `dart:ffi` 設定時,請使用此技能。
使用 package:ffigen 產生 FFI 繫結
目錄
簡介
使用 package:ffigen (FfiGenerator) 自動化並標準化 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:如果函式不回呼 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 設定),因此不需要手動格式化。
- 在目標套件內執行靜態分析器:
dart analyze - 處理警告/提示:如果
dart analyze在產生的檔案中報告樣式或 lint 警告,請在產生器腳本的preamble設定中附加對應的警告碼到ignore_for_file:清單(例如加入camel_case_types、non_constant_identifier_names等)。請勿修改套件的全域規則。 - 處理編譯錯誤:如果
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
這將自動建立:
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並確保套件中沒有任何編譯器/分析器錯誤或警告。- 如果產生的繫結中報告了靜態警告,請在產生器的
preamble設定中加入ignore_for_file規則來抑制(請勿修改全域套件規則)。 - 如果產生的繫結中報告了實際的編譯器或分析器錯誤,請勿手動編輯產生的檔案。向使用者回報詳細資訊,並引導他們在 github.com/dart-lang/native 提交問題。
- 如果產生的繫結中報告了靜態警告,請在產生器的






