
dart-setup-ffi-assets
熱門引導 Agent 利用 Dart 的 Native Assets Hook 系統(透過 `hook/build.dart` 與 `hook/link.dart` 並搭配 `package:hooks` 和 `package:native_toolchain_c`),將 C/C++ 原始碼編譯並打包為動態或靜態函式庫(Code Assets)。當使用者要求「設定 native assets」、「編譯 C/C++ 原始碼」、「打包動態函式庫」、「建置原生 C 程式碼」、「連結原生資產」、「實作 build.dart 或 link.dart hooks」或「在 Dart/Flutter 中整合 C/C++ interop」時使用。協助 Agent 避免手動調度編譯工具鏈,並能設定經 Hash 安全校驗的二進位檔下載,或透過 `package:record_use` 映射進行進階的連結器 Tree-shaking 最佳化。
引導 Agent 利用 Dart 的 Native Assets Hook 系統(透過 `hook/build.dart` 與 `hook/link.dart` 並搭配 `package:hooks` 和 `package:native_toolchain_c`),將 C/C++ 原始碼編譯並打包為動態或靜態函式庫(Code Assets)。當使用者要求「設定 native assets」、「編譯 C/C++ 原始碼」、「打包動態函式庫」、「建置原生 C 程式碼」、「連結原生資產」、「實作 build.dart 或 link.dart hooks」或「在 Dart/Flutter 中整合 C/C++ interop」時使用。協助 Agent 避免手動調度編譯工具鏈,並能設定經 Hash 安全校驗的二進位檔下載,或透過 `package:record_use` 映射進行進階的連結器 Tree-shaking 最佳化。
透過 Native Assets Hook 將 C 程式碼編譯為 Code Assets
運用建置(build)與連結(link)Hook,在 Dart 的 Native Assets 架構下整合並自動化原生 C/C++ 原始碼的編譯與打包,將其生成為 Code Assets。
目錄
簡介
在 Dart 的 Native Assets 特性支援下,套件可以將原生程式碼(如 C/C++ 函式庫)打包為 Code Assets,並在標準開發流程(例如 dart run、dart test、dart build 與 flutter run)中自動完成綁定。Code Assets 的打包過程由放置於套件 hook/ 目錄下的兩個程式化 Hook 腳本所驅動:
hook/build.dart:將本機 C 原始碼編譯為機器碼,或將預建置的原生二進位檔打包為特定主機/目標架構的 Code Assets。hook/link.dart:連結已建置的 Code Assets,並套用進階的 Tree-shaking 最佳化,藉此移除未使用的原生符號(Symbols)並縮減執行期二進位檔體積。
限制與規範
[!IMPORTANT]
所有檔案路徑解析皆須保持跨平台獨立性。切勿硬編碼(Hardcode)絕對目標路徑、Shell 腳本或系統命令變數。請一律使用Platform.script.resolve()或以Uri為基礎的路徑解析,以確保腳本具備完整的可攜性。
- Hook 位置:編譯與打包 Hook 必須嚴格存放於套件根目錄下的
hook/資料夾內:hook/build.dart(建置執行階段)hook/link.dart(選擇性的打包 / 連結 / Tree-shaking 階段)
- 編譯工具鏈標準:請使用
package:native_toolchain_c提供的程式化 API(如CBuilder與CLibrary)來執行編譯工具鏈。切勿透過 Shell 命令直接呼叫原始的gcc、clang或msvc。 - 前言與授權標頭(License Headers):所有手寫及自動生成的原始碼檔案(包含 Bindings、輔助腳本與 Hooks)均須包含目標套件的版權與授權標頭。
- Tree-shaking 映射:若使用編譯器 Tree-shaking 功能,必須利用 FFIgen 生成的 record_use 映射表,將目標 Dart 方法名稱(例如
Method.name)對映回原始的原生 C 符號名稱。該映射檔檔名必須放在lib/src/third_party/目錄下,且副檔名必須為.g.dart(例如sqlite3.record_use_mapping.g.dart)。 - 預編譯函式庫的完整性防護機制:若採用動態下載模式:
- 加密校驗:下載的預建置二進位檔必須與包含 MD5 或 SHA-256 Hash 值的前置查詢表進行比對驗證,以確保二進位檔完整性並防止竄改。
- 平滑降級與復原:提供備用方案(例如透過
local_build等旗標切換至本機編譯執行),以支援離線環境下的開發者。
原生 Interop 套件
用於 Code Assets 的程式化建置與連結 Hook 依賴三個專用的原生 Interop 套件:
| 相依套件 | 用途 | 主要 API 抽象 |
|---|---|---|
package:hooks |
定義執行邊界的關鍵協調器。 | build(args, callback), link(args, callback) |
package:native_toolchain_c |
偵測本機編譯器(MSVC、Xcode/Clang、GCC)並執行建置工具鏈。 | CLibrary, CBuilder, LinkerOptions.treeshake |
package:code_assets |
建立傳遞給動態載入器的程式碼元資料(Metadata)記錄模型。 | 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 函式庫的編譯元資料(Metadata)。這能讓建置與連結 Hook 共享資產、名稱及原始碼的單一事實來源(Single Source of Truth)。
步驟 3:實作 Build 與 Link Hook 腳本
在 hook/build.dart 中編寫編譯協調腳本,並在 hook/link.dart 中編寫無用程式碼消除(Dead-code elimination)邏輯。
步驟 4:執行 Hook 週期
執行標準測試套件時,背景會自動動態觸發建置與連結 Hook 的生命週期:
dart test
選擇整合方式
在 Dart 中整合並交付 C/C++ 原生資產主要有兩種方法。請根據你的專案需求選擇最合適的方案:
| 比較面向 | 方法 1:本機編譯與 Tree-Shaking | 方法 2:下載預編譯二進位檔 |
|---|---|---|
| 主要應用場景 | 當 C/C++ 原始碼直接包含在套件中,且希望極致最佳化體積時。 | 當本機編譯速度較慢/複雜度高,或想避免開發者環境建置工具鏈時。 |
| 主機工具鏈需求 | 需要預先安裝對應平台的 C 編譯器(Xcode tools、MSVC、GCC)。 | 開發者與使用者機器上無需安裝或設定任何編譯器。 |
| 二進位檔最佳化 | 極佳。未使用的符號會被完全 Tree-shake 清除,大幅縮減函式庫體積。 | 標準。直接交付標準編譯的二進位檔。 |
| 離線支援能力 | 完全合規。可完全在離線環境下運作。 | 需要網路存取以下載函式庫,但可提供離線備用機制。 |
方法 1:本機編譯與連結器 Tree-Shaking(推薦)
在此方案中,建置 Hook 會呼叫本機工具鏈(GCC、Clang、MSVC)直接編譯原始碼。隨後,連結 Hook 會利用編譯器選項過濾輸出符號,僅保留使用者程式碼中呼叫到的目標方法。這是 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 並勾選 使用 C++ 的桌面開發 工作負載。
注意:若在主機 PATH 中找不到相容的工具鏈,建置 Hook 腳本將拋出編譯執行例外。若無法確保開發環境皆備妥工具鏈,請務必指定編譯器限制或改採方法 2。
C 原始碼與 Bindings 設定
假設在 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 Bindings,藉此支援記錄使用狀況追蹤,並在 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)並存放在 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 Method 引用對映回原始 C 符號名稱
symbolsToKeep: input.recordedUses?.calls.keys.cast<Method>().map(
(e) => recordUseMapping[e.name]!,
),
),
);
});
}
方法 2:下載預編譯動態函式庫
另一種做法是在中央建置機器上預先編譯並歸檔二進位檔,並在執行建置 Hook 期間下載目標二進位檔。這符合 download_asset Hook 套件所展示的範例模式。
為何選擇下載預編譯二進位檔?
- 主機環境限制:編譯大型
<!-- truncated for translation batch; full body continues in source -->





