dart-setup-ffi-assets

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 最佳化。

2792星標
164分支
更新於 2026/8/5
SKILL.md
唯讀
名稱
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 最佳化。

透過 Native Assets Hook 將 C 程式碼編譯為 Code Assets

運用建置(build)與連結(link)Hook,在 Dart 的 Native Assets 架構下整合並自動化原生 C/C++ 原始碼的編譯與打包,將其生成為 Code Assets

目錄


簡介

在 Dart 的 Native Assets 特性支援下,套件可以將原生程式碼(如 C/C++ 函式庫)打包為 Code Assets,並在標準開發流程(例如 dart rundart testdart buildflutter run)中自動完成綁定。Code Assets 的打包過程由放置於套件 hook/ 目錄下的兩個程式化 Hook 腳本所驅動:

  1. hook/build.dart:將本機 C 原始碼編譯為機器碼,或將預建置的原生二進位檔打包為特定主機/目標架構的 Code Assets。
  2. 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(如 CBuilderCLibrary)來執行編譯工具鏈。切勿透過 Shell 命令直接呼叫原始的 gccclangmsvc
  • 前言與授權標頭(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 -->