dart-use-ffigen

dart-use-ffigen

熱門

引導代理使用 `package:ffigen` 自動產生 FFI 繫結,而非手動撰寫。當任務涉及撰寫新的 FFI 繫結、擴充 C/Objective-C/Swift 整合,或取代手刻的 `dart:ffi` 設定時,請使用此技能。

406星標
26分支
更新於 2026/7/20
SKILL.md
唯讀
名稱
dart-use-ffigen
描述

引導代理使用 `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 設定),因此不需要手動格式化。

  1. 在目標套件內執行靜態分析器:
    dart analyze
    
  2. 處理警告/提示:如果 dart analyze 在產生的檔案中報告樣式或 lint 警告,請在產生器腳本的 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 提交問題。