
dart-setup-ffi-assets
熱門引導代理程式使用 Dart 的 Native Assets hook 系統(透過 hook/build.dart 和 hook/link.dart,利用 package:hooks 和 package:native_toolchain_c),將 C/C++ 原始碼編譯並打包成動態或靜態函式庫(Code Assets)。當使用者要求:『設定原生資產』、『編譯 C/C++ 原始碼』、『打包動態函式庫』、『建置原生 C 程式碼』、『連結原生資產』、『實作 build.dart 或 link.dart hooks』或『在 Dart/Flutter 中整合 C/C++ 互操作』時使用。協助代理程式避免手動工具鏈編排,並設定安全的雜湊驗證二進位檔下載或進階連結器 tree-shaking(搭配 package:record_use 映射)。
引導代理程式使用 Dart 的 Native Assets hook 系統(透過 hook/build.dart 和 hook/link.dart,利用 package:hooks 和 package:native_toolchain_c),將 C/C++ 原始碼編譯並打包成動態或靜態函式庫(Code Assets)。當使用者要求:『設定原生資產』、『編譯 C/C++ 原始碼』、『打包動態函式庫』、『建置原生 C 程式碼』、『連結原生資產』、『實作 build.dart 或 link.dart hooks』或『在 Dart/Flutter 中整合 C/C++ 互操作』時使用。協助代理程式避免手動工具鏈編排,並設定安全的雜湊驗證二進位檔下載或進階連結器 tree-shaking(搭配 package:record_use 映射)。
使用原生資產 Hooks 將 C 程式碼編譯為 Code Assets
透過建置與連結 hooks,將原生 C/C++ 原始碼整合並自動化編譯與打包成 Code Assets,納入 Dart 的 Native Assets 功能範疇。
目錄
簡介
在 Dart 的 Native Assets 功能下,套件可以將原生程式碼(如 C/C++ 函式庫)打包為 Code Assets,並在標準開發流程(例如 dart run、dart test、dart build 和 flutter run)中自動捆綁。Code Assets 的打包由兩個程式化的 hook 腳本驅動,這些腳本放置在套件的 hook/ 資料夾中:
hook/build.dart:將本機 C 原始碼編譯為機器碼,或將預先建置的原生二進位檔打包為特定主機/目標架構的程式碼資產。hook/link.dart:連結已建置的程式碼資產,套用進階 tree-shaking 最佳化,去除未使用的原生符號並壓縮執行檔二進位檔大小。
限制條件
[!IMPORTANT]
保持所有檔案解析方式與平台無關。切勿硬編碼絕對目標路徑、shell 腳本或系統命令變數。務必使用Platform.script.resolve()或Uri為基礎的解析方式,以確保腳本完全可攜。
- Hook 位置:編譯與打包的 hooks 必須嚴格放置在套件根目錄的
hook/目錄中:hook/build.dart(建置執行階段)hook/link.dart(選擇性的打包/連結/tree-shaking 階段)
- 編譯工具鏈標準:使用
package:native_toolchain_c的程式化 API(例如CBuilder和CLibrary)來執行編譯工具鏈。切勿透過 shell 命令直接呼叫原始的gcc、clang或msvc。 - 前言與授權標頭:每個手動建立與產生的原始檔(包括繫結、輔助程式和 hooks)都必須嚴格包含目標套件的版權與授權標頭。
- Tree-Shaking 映射:若使用編譯器 tree-shaking,必須使用 FFIgen 產生的記錄使用映射,將目標 Dart 方法名稱(例如
Method.name)對應回原始的原生 C 符號名稱。映射檔案必須位於lib/src/third_party/下,並嚴格使用.g.dart副檔名(例如sqlite3.record_use_mapping.g.dart)。 - 預編譯函式庫的完整性保護:若採用動態下載模式:
- 加密驗證:下載的預建置二進位檔必須根據預先設定的查詢表(包含 MD5 或 SHA-256 雜湊值)進行檢查,以確保二進位檔的完整性並防止竄改。
- 優雅復原:支援離線開發者,提供備援方案(例如透過
local_build等旗標執行本機編譯器)。
原生互操作套件
用於 Code Assets 的程式化建置與連結 hooks 利用了三個專門的原生互操作套件:
| 依賴項 | 用途 | 主要 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 hook 與工具鏈依賴項新增至您的套件。您必須直接從 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 規格
在 lib/src/c_library.dart 中定義目標 C 函式庫的編譯中繼資料。這讓建置與連結 hooks 能共享資產、名稱與原始碼的單一事實來源。
步驟 3:實作建置與連結 Hook 腳本
在 hook/build.dart 中撰寫編譯協調腳本,並在 hook/link.dart 中撰寫無效程式碼消除邏輯。
步驟 4:執行 Hook 週期
執行標準測試套件會動態在背景啟動建置與連結 hook 生命週期:
dart test
選擇整合方式
在 Dart 中整合與交付 C/C++ 原生資產主要有兩種方法。請根據您的專案需求選擇合適的方式:
| 面向 | 方法一:本機編譯與 Tree-Shaking | 方法二:預編譯下載 |
|---|---|---|
| 主要使用案例 | C/C++ 原始碼直接包含在套件中,且您希望最大化大小最佳化。 | 本機編譯緩慢/複雜,或希望避免開發者主機工具鏈需求。 |
| 主機工具鏈需求 | 需要預先安裝平台 C 編譯器(Xcode 工具、MSVC、GCC)。 | 開發者/使用者機器上無需任何編譯器設定。 |
| 二進位檔最佳化 | 進階。未使用的符號被完全 tree-shake,減少函式庫大小。 | 標準。標準編譯的二進位檔按原樣提供。 |
| 離線設定 | 完全相容。可完全離線運作。 | 需要網路存取以下載函式庫,並提供離線備援。 |
方法一:本機編譯搭配連結器 Tree-Shaking(建議)
在此方法中,建置 hook 會呼叫本機工具鏈(GCC、Clang、MSVC)直接編譯原始檔。隨後連結 hook 利用編譯器選項過濾輸出符號,僅保留使用者程式碼中呼叫的目標方法。這是 pkgs/code_assets/example/sqlite 下標準且穩健的 SQLite 模式。
前置需求:主機編譯器工具鏈
由於 package:native_toolchain_c 將實際的動態編譯委派給主機作業系統的預設工具鏈,開發機器必須預先安裝下列其中一個編譯器套件:
- macOS:Xcode 命令列工具。透過以下指令安裝:
xcode-select --install - Linux:GCC 或 Clang。透過以下指令安裝:
sudo apt install build-essential - Windows:MSVC(Microsoft Visual C++)。安裝 Visual Studio Installer 並選取 使用 C++ 的桌面開發 工作負載。
注意:若在主機路徑中未發現相容的工具鏈,建置 hook 腳本將會擲回編譯執行例外。請確保指定編譯器限制,或若無法保證工具鏈存在,則採用方法二。
C 原始碼與繫結設定
假設有一個 C 原始碼定義了簡單的數學函式,位於 third_party/sqlite/sqlite3.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 中產生查詢中繼資料映射:
// 自動產生的檔案 - 請勿修改。
// 由 ffigen 產生。
const recordUseMapping = {
'sqlite3_libversion': 'sqlite3_libversion',
};
定義 C 函式庫建置規格
在 lib/src/c_library.dart 中定義集中化的函式庫規格:
import 'package:native_toolchain_c/native_toolchain_c.dart';
/// sqlite 函式庫的 C 建置規格。
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),並放置在 hook 的目標目錄中:
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)
// 確保 C 函式在 Windows DLL 中明確匯出
'SQLITE_API': '__declspec(dllexport)',
},
);
}
});
}
實作 hook/link.dart
在 hook/link.dart 中實作連結最佳化階段。這會利用編譯器 tree-shaking 選項(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(
// 將 Dart 方法參考對應回原始 C 符號名稱
symbolsToKeep: input.recordedUses?.calls.keys.cast<Method>().map(
(e) => recordUseMapping[e.name]!,
),
),
);
});
}
方法二:下載預編譯動態函式庫
另一種方法是先在中央建置機器上編譯二進位檔,將其封存,然後在建置 hook 執行期間下載目標二進位檔。這與 download_asset hook 套件示範的模式相符。
為何下載預編譯二進位檔?
- 主機限制:在本機編譯大型 C/C++ 函式庫需要完整的編譯器設定(GCC、Xcode/SDK、Visual Studio),而最終開發者的主機機器可能不具備這些條件。
- 編譯速度:預編譯下載只需毫秒即可完成,相較於可能長達數分鐘的編譯過程。
- 平台橋接:若主機架構有限,可避免跨編譯的限制。
實作預編譯動態下載
我們設定建置 hook 以偵測本機編譯器旗標(例如 local_build)。若未指定,hook 會使用 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. Hook 下載輔助程式(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('下載目標 $uri 失敗:狀態碼 ${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
撰寫最終的下載建置 hook,並包含本機編譯備援:
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(
'安全性不符:檔案 $expectedFileName 雜湊驗證失敗!'
'找到的雜湊:$fileHash,預期:$expectedHash。'
);
}
output.assets.code.add(
CodeAsset(
package: input.packageName,
name: 'native_add.dart',
linkMode: DynamicLoadingBundled(),
file: file.uri,
),
);
}
});
}
驗證檢查清單
在宣告建置或連結 hook 實作完成之前,請務必執行以下檢查:
1. 本機執行沙盒
執行單元測試,確認原生資產編譯/連結過程完成,且無執行時期或建置工具例外:
dart test
2. 驗證目標輸出
導覽至套件目標目錄,確認已為主機系統建立動態二進位資產:
- macOS:確認
.dart_tool/resources/或目標目錄包含.dylib檔案。 - Linux:確認
.dart_tool/resources/或目標目錄包含.so檔案。 - Windows:確認
.dart_tool/resources/或目標目錄包含.dll檔案。
3. 驗證 Tree-Shaking 去除
為確保連結 hook 確實去除未使用的原生符號並壓縮二進位檔打包,請執行以下驗證:
- 編譯 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 開發人員命令提示字元):
dumpbin /EXPORTS build\cli\lib\sqlite3.dll
- macOS:
- 確認目標匯出:驗證命令輸出僅包含明確保留的進入點函式(例如
sqlite3_libversion),且未輸出任何未參考/已去除的符號。 - 無捆綁情境:若應用程式未匯入或呼叫原生函式庫中的任何方法:
- 驗證連結 hook 記錄:
Skipping linking as no symbols are to be kept. - 驗證正式版本中未建置/放置任何函式庫(未產生
.dylib/.so/.dll檔案,節省捆綁大小)。
- 驗證連結 hook 記錄:
4. 驗證離線相容性(使用者定義)
確認離線相容性已完全啟用,且下載備援在離線時能完美執行:
- 在套件的
pubspec.yaml(或工作區根目錄的pubspec.yaml)中為您的套件設定local_build: true定義:hooks: user_defines: <your_package_name>: local_build: true - 停用機器的網路介面卡,或在沙盒離線 shell 中執行。
- 啟動單元測試:
dart test - 驗證測試套件成功使用主機編譯器編譯本機原始檔,無編譯錯誤,且從未嘗試網路下載請求。





