dart-setup-ffi-assets について
Dart パッケージ作者向けに、Native Assets hooks で C/C++ コードを Code Assets としてビルド・同梱する方法を解説します。シンボル削減を伴うローカルビルドと、検証済みの事前ビルドライブラリの取得を扱います。
主な機能
- 01Native Assets の build・link hooks の設定
- 02CBuilder と CLibrary による C ライブラリのビルド
- 03使用マッピングに基づくシンボル削減
- 04事前ビルド済み動的ライブラリの取得と検証
- 05オフライン向けローカルビルドへのフォールバック
活用シーン
- 01Dart パッケージのビルドに C/C++ ソースを含める
- 02FFI の使用状況に応じてネイティブシンボルを削減する
- 03コンパイラツールチェーンのない環境に事前ビルドライブラリを提供する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ flutter/ agent-plugins --skill dart-setup-ffi-assetsインストール後、Claude Code で/dart-setup-ffi-assetsを入力して読み込まれたか確認します。
詳細
Native Assets フックを使用した C コードのコードアセットへのコンパイル
ビルドフックとリンクフックを使用して、ネイティブ C/C++ ソースコードのコンパイルとパッケージ化を Code Assets として統合および自動化します。これは、Dart の包括的な Native Assets 機能の一部です。
目次
- はじめに
- 制約事項
- ネイティブ相互運用パッケージ
- ステップごとのワークフロー
- 統合方法の選択
- 方法 1:リンカーのツリーシェイキングを使用したローカルコンパイル(推奨)
- 方法 2:コンパイル済み動的ライブラリのダウンロード
- 検証チェックリスト
はじめに
Dart の Native Assets 機能では、パッケージにネイティブコード(C/C++ ライブラリなど)を Code Assets として含め、通常の開発サイクル(例:dart run、dart test、dart build、flutter run)で自動的にバンドルできます。Code Assets のパッケージ化は、パッケージの hook/ フォルダー内に配置する、次の 2 つのプログラム可能なフックスクリプトによって行われます。
hook/build.dart:ローカルの C ソースをマシンコードにコンパイルするか、特定のホスト/ターゲットアーキテクチャ向けに、ビルド済みのネイティブバイナリをコードアセットとしてバンドルします。hook/link.dart:ビルドされたコードアセットをリンクし、高度なツリーシェイキング最適化を適用して、使用されていないネイティブシンボルを削除し、実行時のバイナリサイズを圧縮します。
制約
重要
ファイルの解決はすべてプラットフォームに依存しない方法で行ってください。絶対的なターゲットパス、シェルスクリプト、システムコマンド変数をハードコードしないでください。スクリプトの完全な移植性を確保するため、常に
Platform.script.resolve()またはUriベースの解決を使用してください。
- フックの配置場所:コンパイルおよびパッケージング用のフックは、パッケージのルートにある
hook/ディレクトリ内に厳密に配置する必要があります。hook/build.dart(ビルド実行フェーズ)hook/link.dart(任意のパッケージング/リンク/ツリーシェイキングフェーズ)
- コンパイルツールチェーンの標準:コンパイルツールチェーンの実行には、
package:native_toolchain_cのプログラム用 API(例:CBuilder、CLibrary)を使用してください。シェルコマンドから生のgcc、clang、msvcを呼び出さないでください。 - プリアンブルとライセンスヘッダー:手書きおよび生成されたすべてのソースファイル(バインディング、ヘルパー、フックを含む)には、対象パッケージの著作権およびライセンスヘッダーを必ず含めてください。
- ツリーシェイキングのマッピング:コンパイラのツリーシェイキングを利用する場合は、FFIgen が生成する record use マッピングを使って、対象となる Dart メソッド名(例:
Method.name)を元のネイティブ C シンボル名に対応付ける必要があります。マッピングファイルはlib/src/third_party/以下に配置し、拡張子は必ず.g.dartにしてください(例:sqlite3.record_use_mapping.g.dart)。 - プリコンパイル済みライブラリの整合性保護:動的ダウンロード方式を採用する場合:
- 暗号学的検証:ダウンロードしたプリビルドバイナリは、MD5 または SHA-256 ハッシュを含む事前設定済みのルックアップテーブルと照合し、バイナリの整合性を保証して改ざんを防ぐ必要があります。
- 適切なフォールバック:
local_buildなどのフラグを使ったローカルでのコンパイラ実行など、オフラインの開発者向けのフォールバックを用意してください。
ネイティブ相互運用パッケージ
Code Assets のプログラム用ビルドフックとリンクフックでは、次の3つの専用ネイティブ相互運用パッケージを使用します。
| 依存関係 | 用途 | 主な API 抽象化 |
|---|---|---|
package:hooks | 実行境界を定義する主要なオーケストレーターです。 | build(args, callback), link(args, callback) |
package:native_toolchain_c | ローカルコンパイラ(MSVC、Xcode/Clang、GCC)を検出し、ビルドツールチェーンを実行します。 | CLibrary, CBuilder, LinkerOptions.treeshake |
package:code_assets | 動的ローダーに渡されるコードメタデータのレコードをモデル化します。 | CodeAsset, DynamicLoadingBundled |
ステップごとのワークフロー
ステップ1:依存関係を追加する
Code Assets のフックとツールチェーンの依存関係をパッケージに追加します。これらの依存関係は pub.dev から直接取得する必要があります。
CLI を使って自動的に追加できます。
dart pub add code_assets hooks native_toolchain_c record_use dev:ffigen
または、対象パッケージの pubspec.yaml に手動で宣言します。
dependencies:
code_assets: ^1.0.0
hooks: ^0.1.0
native_toolchain_c: ^0.1.0
record_use: ^0.6.0
dev_dependencies:
ffigen: ^20.1.1
ステップ2:C の仕様を定義する
対象となる C ライブラリのコンパイルメタデータを lib/src/c_library.dart に定義します。これにより、ビルドフックとリンクフックで、アセット、名前、ソースの単一の信頼できる情報源を共有できます。
ステップ3:ビルドフックとリンクフックのスクリプトを実装する
コンパイルのオーケストレーションスクリプトを hook/build.dart に、デッドコードの除去ロジックを hook/link.dart に記述します。
ステップ4:フックのサイクルを実行する
標準のテストスイートを実行すると、ビルドフックとリンクフックのライフサイクルがバックグラウンドで動的に開始されます。
dart test
統合方法の選択
Dart で C/C++ のネイティブアセットを統合して提供する方法は、主に 2 つあります。プロジェクトの要件に合った方法を選択してください。
| 項目 | 方法 1:ローカルコンパイルとツリーシェイキング | 方法 2:コンパイル済みファイルのダウンロード |
|---|---|---|
| 主な用途 | C/C++ のソースコードをパッケージに直接含め、サイズを最大限に最適化したい場合。 | ローカルでのコンパイルに時間がかかる、または複雑な場合や、開発者のホストツールチェーンへの依存を避けたい場合。 |
| ホストツールチェーンの要件 | プラットフォームの C コンパイラーが事前にインストールされている必要があります(Xcode ツール、MSVC、GCC)。 | 開発者やユーザーのマシンでコンパイラーのセットアップは不要です。 |
| バイナリーの最適化 | 高度。未使用のシンボルが完全にツリーシェイキングされ、ライブラリーのサイズが小さくなります。 | 標準。コンパイル済みの標準バイナリーがそのまま同梱されます。 |
| オフラインでのセットアップ | 完全に対応しています。完全オフラインで動作します。 | ライブラリーのダウンロードにネットワークアクセスが必要ですが、オフライン時のフォールバックがあります。 |
方法 1:リンカーのツリーシェイキングを使用したローカルコンパイル(推奨)
この方法では、ビルドフックがローカルのツールチェーン(GCC、Clang、MSVC)を呼び出して、ソースファイルを直接コンパイルします。その後、リンクフックがコンパイラーオプションを使用して出力シンボルをフィルタリングし、ユーザーコードで呼び出された対象メソッドのみを保持します。これは、pkgs/code_assets/example/sqlite にある標準的で堅牢な SQLite のパターンです。
必要なホストコンパイラーツールチェーン
package:native_toolchain_c は実際の動的コンパイルをホストオペレーティングシステムのデフォルトツールチェーンに委ねるため、開発マシンには次のコンパイラーパッケージのいずれかが事前にインストールされている必要があります。
- macOS:Xcode Command Line Tools。次のコマンドでインストールします。
xcode-select --install - Linux:GCC または Clang。次のコマンドでインストールします。
sudo apt install build-essential - Windows:MSVC(Microsoft Visual C++)。Visual Studio Installer をインストールし、Desktop development with C++ ワークロードを選択します。
注:ホストのパス上に互換性のあるツールチェーンが見つからない場合、ビルドフックスクリプトはコンパイル実行例外をスローします。コンパイラーの制約を指定するか、ツールチェーンを保証できない場合は方法 2 を採用してください。
C ソースとバインディングのセットアップ
third_party/sqlite/sqlite3.c に単純な数学関数を定義する C ソースがあり、エントリーポイントのヘッダーが third_party/sqlite/sqlite3.h にあるものとします。
#ifndef SQLITE3_H_
#define SQLITE3_H_
const char *sqlite3_libversion(void);
#endif // SQLITE3_H_
プログラムによる FFIgen スクリプト(tool/ffigen.dart)を使用して、lib/src/third_party/sqlite3.g.dart に FFI バインディングを作成します。これにより、使用状況の記録が可能になり、lib/src/third_party/sqlite3.record_use_mapping.g.dart にルックアップ用のメタデータマップが生成されます。
// AUTO-GENERATED FILE - DO NOT MODIFY.
// Generated via ffigen.
const recordUseMapping = {
'sqlite3_libversion': 'sqlite3_libversion',
};
C ライブラリのビルド仕様の定義
lib/src/c_library.dart にライブラリー仕様を一元的に定義します。
import 'package:native_toolchain_c/native_toolchain_c.dart';
/// The C build specification for the sqlite library.
final cLibrary = CLibrary(
name: 'sqlite3',
assetName: 'src/third_party/sqlite3.g.dart',
sources: ['third_party/sqlite/sqlite3.c'],
);
hook/build.dart の実装
CLibrary.build を使用して hook/build.dart を実装します。これにより、フックのターゲットディレクトリー内にライブラリーがダイナミックライブラリー(例:.so、.dylib、.dll)としてビルドされます。
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:sqlite/src/c_library.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
if (input.config.buildCodeAssets) {
await cLibrary.build(
input: input,
output: output,
defines: {
if (input.config.code.targetOS == OS.windows)
// Ensure C functions are explicitly exported in the Windows DLL
'SQLITE_API': '__declspec(dllexport)',
},
);
}
});
}
hook/link.dart の実装
hook/link.dart にリンク最適化フェーズを実装します。コンパイラーのツリーシェイキングオプション(LinkerOptions.treeshake)を使用して、シンボル使用状況の記録に基づき、縮小されデッドコードが除去されたバイナリーをコンパイルします。
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:record_use/record_use.dart';
import 'package:sqlite/src/c_library.dart';
import 'package:sqlite/src/third_party/sqlite3.record_use_mapping.g.dart';
void main(List<String> arguments) async {
await link(arguments, (input, output) async {
await cLibrary.link(
input: input,
output: output,
linkerOptions: LinkerOptions.treeshake(
// Map Dart Method references back to raw C symbol names
symbolsToKeep: input.recordedUses?.calls.keys.cast<Method>().map(
(e) => recordUseMapping[e.name]!,
),
),
);
});
}
方法 2:コンパイル済みダイナミックライブラリーのダウンロード
別の方法として、中央のビルドマシンであらかじめバイナリーをコンパイルしてアーカイブし、ビルドフックの実行時に対象のバイナリーをダウンロードできます。これは、download_asset フックパッケージで示されているパラダイムに沿った方法です。
なぜプリコンパイル済みバイナリをダウンロードするのですか?
- ホスト環境の制約: 大規模な C/C++ ライブラリをローカルでコンパイルするには、完全なコンパイラー環境(GCC、Xcode/SDK、Visual Studio)が必要ですが、エンド開発者のホストマシンには備わっていない場合があります。
- コンパイル速度: プリコンパイル済みバイナリのダウンロードは数ミリ秒で完了しますが、コンパイルには数分以上かかる場合があります。
- プラットフォーム間の橋渡し: ホストのアーキテクチャが限られている場合に、クロスコンパイルの制約を回避できます。
プリコンパイル済みバイナリの動的ダウンロードの実装
ビルドフックを設定して、ローカルコンパイルのフラグ(例:local_build)を検出します。指定されていない場合、フックは HttpClient を使用してプラットフォーム固有のライブラリをダウンロードし、MD5 ハッシュを計算して、設定済みのハッシュ検索テーブルと照合してダウンロードの安全性を確認し、バイナリファイルを CodeAsset として登録します。
1. ターゲットハッシュの定義(lib/src/hook_helpers/hashes.dart)
パッケージのソース内で、プラットフォームごとのファイルに対するターゲット MD5 ハッシュチェックを定義します。
const assetHashes = {
'libnative_add_macos_arm64.dylib': '4a88f50438a98402db2dbd47b59eb412',
'libnative_add_linux_x64.so': '9f5e15043aa98402dcdbbd47b59ea520',
'native_add_windows_x64.dll': 'a881e5043ba98402acdebd47b59fa321',
};
2. フックのダウンローダーヘルパー(lib/src/hook_helpers/download.dart)
ターゲットファイル名の動的な照合を使用して、ダウンロードと整合性チェックのロジックを実装します。
import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:crypto/crypto.dart';
const version = '1.0.0';
Uri downloadUri(String target) => Uri.parse(
'https://github.com/my-org/my-native-repo/releases/download/$version/$target',
);
Future<File> downloadAsset(
OS targetOS,
Architecture targetArchitecture,
Directory outputDir,
) async {
final fileName = targetOS.dylibFileName('native_add_${targetOS.name}_${targetArchitecture.name}');
final uri = downloadUri(fileName);
final client = HttpClient()..findProxy = HttpClient.findProxyFromEnvironment;
final request = await client.getUrl(uri);
final response = await request.close();
if (response.statusCode != 200) {
throw ArgumentError('Download target $uri failed: Code ${response.statusCode}');
}
final targetFile = File.fromUri(outputDir.uri.resolve(fileName));
await targetFile.create(recursive: true);
await response.pipe(targetFile.openWrite());
return targetFile;
}
Future<String> hashAsset(File file) async {
return md5.convert(await file.readAsBytes()).toString();
}
3. hook/build.dart の実装
ローカルコンパイルへのフォールバックを組み込んだ、最終的なダウンロードビルドフックを記述します。
import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:my_download_package/src/hook_helpers/hashes.dart';
import 'package:my_download_package/src/hook_helpers/download.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
final localBuild = input.userDefines['local_build'] as bool? ?? false;
if (localBuild) {
final name = 'native_add_${input.config.code.targetOS.name}_${input.config.code.targetArchitecture.name}';
final builder = CBuilder.library(
name: name,
assetName: 'native_add.dart',
sources: ['src/native_add.c'],
);
await builder.run(input: input, output: output);
} else {
final targetOS = input.config.code.targetOS;
final targetArch = input.config.code.targetArchitecture;
final outputDir = Directory.fromUri(input.outputDirectory);
final file = await downloadAsset(targetOS, targetArch, outputDir);
final fileHash = await hashAsset(file);
final expectedFileName = targetOS.dylibFileName('native_add_${targetOS.name}_${targetArch.name}');
final expectedHash = assetHashes[expectedFileName];
if (fileHash != expectedHash) {
throw Exception(
'Security Mismatch: File $expectedFileName hash verification failed! '
'Found hash: $fileHash, expected: $expectedHash.'
);
}
output.assets.code.add(
CodeAsset(
package: input.packageName,
name: 'native_add.dart',
linkMode: DynamicLoadingBundled(),
file: file.uri,
),
);
}
});
}
検証チェックリスト
ビルドフックまたはリンクフックの実装を完了とする前に、必ず以下を確認してください。
1. ローカル実行サンドボックス
ユニットテストを実行し、ネイティブアセットのコンパイル/リンク処理がランタイム例外やビルドツールの例外なしに完了することを確認します。
dart test
2. ターゲット出力の確認
パッケージのターゲットディレクトリに移動し、ホストシステム用の動的バイナリアセットが作成されていることを確認します。
- macOS:
.dart_tool/resources/またはターゲットディレクトリに.dylibファイルが含まれていることを確認します。 - Linux:
.dart_tool/resources/またはターゲットディレクトリに.soファイルが含まれていることを確認します。 - Windows:
.dart_tool/resources/またはターゲットディレクトリに.dllファイルが含まれていることを確認します。
3. ツリーシェイキングによる除去の確認
リンクフックが実際に未使用のネイティブシンボルを除去し、バイナリパッケージを圧縮していることを確認するため、以下の検証を行います。
- CLI/アプリの本番用バンドルをコンパイルします。
dart build cli bin/main.dart - 動的ライブラリを含むコンパイル済みビルドディレクトリに移動します。
- エクスポートされた動的シンボルテーブルを調べます。
- macOS:
nm -gU build/cli/lib/libsqlite3.dylib - Linux:
nm -D build/cli/lib/libsqlite3.so - Windows(MSVC Developer Command Prompt を使用):
dumpbin /EXPORTS build\cli\lib\sqlite3.dll
- macOS:
- ターゲットのエクスポートを確認:コマンドの出力に明示的に保持されたエントリーポイント関数(例:
sqlite3_libversion)のみが含まれ、参照されていない/除去されたシンボルが出力されていないことを確認します。 - バンドルがない場合:アプリケーションがネイティブライブラリのメソッドをインポートも呼び出しもしていない場合:
- リンクフックのログに
Skipping linking as no symbols are to be kept.と出力されることを確認します。 - 本番バンドルにライブラリがビルド/配置されていないことを確認します(
.dylib/.so/.dllファイルは生成されず、バンドルサイズが節約されます)。
- リンクフックのログに
4. オフライン対応の確認(ユーザー定義)
オフライン対応が完全に有効になっており、ダウンロードのフォールバックが完全オフラインで正常に実行されることを確認します。
- パッケージの
pubspec.yaml(またはワークスペースのルートにあるpubspec.yaml)で、パッケージのlocal_build: true定義を設定します。hooks: user_defines: <your_package_name>: local_build: true - マシンのネットワークアダプターを無効にするか、ネットワーク接続のないサンドボックスシェルで実行します。
- ユニットテストを実行します。
dart test - テストスイートがホストのコンパイラーを使用してローカルのソースファイルを正常にコンパイルし、コンパイルエラーがなく、ネットワーク経由のダウンロード要求が一切試みられないことを確認します。