dart-use-ffigen について
ネイティブヘッダーから `package:ffigen` で FFI バインディングを生成する、Dart 開発者向けガイドです。ジェネレーター設定、ファイル構成、シンボルの絞り込み、生成コードの静的解析を扱います。
主な機能
- 01ヘッダーから Dart FFI バインディングを生成
- 02関数とヘッダーのフィルター設定
- 03生成スクリプトと出力先の整理
- 04使用記録マッピングの生成
- 05生成バインディングの静的解析
活用シーン
- 01C ライブラリ向け Dart バインディングの作成
- 02手書きのネイティブバインディングの置き換え
- 03バインディング生成をプロジェクトの手順に追加
- 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を使用して生成してください。 - ジェネレーターの配置場所:ジェネレータースクリプトは、対象パッケージのルート内にある
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 フォーマッターを自動的に実行します。
手順ごとのワークフロー
Step 1: 依存関係の確認・追加
パッケージの pubspec.yaml を開き、dev_dependencies に ffigen が含まれていることを確認します。Dart MCP サーバーを使用するか、pub.dev で最新バージョン(例:^20.1.1)を確認してください。
CLI を使って自動的に追加できます:
dart pub add dev:ffigen
Step 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');
Step 3: スクリプトを作成する(tool/ffigen.dart)
void main() を定義し、動的なオプションを指定して FfiGenerator を実行します(以下の完全な例を参照してください)。
Step 4: コード生成を実行する
対象パッケージのフォルダー内で、ターミナルからスクリプトを実行します:
dart run tool/ffigen.dart
Step 5: 静的解析
生成されたバインディングが正しいことを確認し、解析上の問題を解決します。FFIgen は format: true の設定によって、出力ファイルに対して Dart フォーマッターを自動的に実行するため、手動でのフォーマットは不要です。
- 対象パッケージ内で静的アナライザーを実行します:
dart analyze - 警告/Lint への対処:
dart analyzeが生成ファイル内でスタイルまたは lint の警告を報告した場合は、対応する警告コードをジェネレータースクリプトのpreamble設定にあるignore_for_file:リストへ追加します(例:camel_case_types、non_constant_identifier_namesなど)。パッケージ全体のルールは変更しないでください。 - コンパイルエラーへの対処:
dart analyzeが生成ファイル内で実際のコンパイラーエラーまたは解析エラー(警告ではないもの)を報告した場合、生成ファイルを手動で編集しないでください。エラーの詳細をユーザーに直接報告し、リポジトリの github.com/dart-lang/native に issue を作成できるようにしてください。
具体例:C ライブラリのバインディング
FFI を介してアクセスする SQLite の C ライブラリソースを third_party/sqlite/ に組み込んだ pkgs/code_assets/example/sqlite パッケージを扱うとします。
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_
BEFORE:手動による 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();
AFTER: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
これにより、次のファイルが自動的に作成されます:
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 にイシューを作成するよう案内してください。
- 生成されたバインディングで静的解析の警告が報告された場合は、グローバルなパッケージルールを変更せず、ジェネレーターの