dart-setup-ffi-assets

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 映射)。

406星標
26分支
更新於 2026/7/20
SKILL.md
唯讀
名稱
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 映射)。

使用原生資產 Hooks 將 C 程式碼編譯為 Code Assets

透過建置與連結 hooks,將原生 C/C++ 原始碼整合並自動化編譯與打包成 Code Assets,納入 Dart 的 Native Assets 功能範疇。

目錄


簡介

在 Dart 的 Native Assets 功能下,套件可以將原生程式碼(如 C/C++ 函式庫)打包為 Code Assets,並在標準開發流程(例如 dart rundart testdart buildflutter run)中自動捆綁。Code Assets 的打包由兩個程式化的 hook 腳本驅動,這些腳本放置在套件的 hook/ 資料夾中:

  1. hook/build.dart:將本機 C 原始碼編譯為機器碼,或將預先建置的原生二進位檔打包為特定主機/目標架構的程式碼資產。
  2. 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(例如 CBuilderCLibrary)來執行編譯工具鏈。切勿透過 shell 命令直接呼叫原始的 gccclangmsvc
  • 前言與授權標頭:每個手動建立與產生的原始檔(包括繫結、輔助程式和 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)並執行建置工具鏈。 CLibraryCBuilderLinkerOptions.treeshake
package:code_assets 建模傳遞給動態載入器的程式碼中繼資料記錄。 CodeAssetDynamicLoadingBundled

逐步工作流程

步驟 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 確實去除未使用的原生符號並壓縮二進位檔打包,請執行以下驗證:

  1. 編譯 CLI/應用程式的正式版本:
    dart build cli bin/main.dart
    
  2. 導覽至包含動態函式庫的編譯建置目錄。
  3. 查詢匯出的動態符號表:
    • 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
      
  4. 確認目標匯出:驗證命令輸出包含明確保留的進入點函式(例如 sqlite3_libversion),且未輸出任何未參考/已去除的符號。
  5. 無捆綁情境:若應用程式未匯入或呼叫原生函式庫中的任何方法:
    • 驗證連結 hook 記錄:Skipping linking as no symbols are to be kept.
    • 驗證正式版本中未建置/放置任何函式庫(未產生 .dylib/.so/.dll 檔案,節省捆綁大小)。

4. 驗證離線相容性(使用者定義)

確認離線相容性已完全啟用,且下載備援在離線時能完美執行:

  1. 在套件的 pubspec.yaml(或工作區根目錄的 pubspec.yaml)中為您的套件設定 local_build: true 定義:
    hooks:
      user_defines:
        <your_package_name>:
          local_build: true
    
  2. 停用機器的網路介面卡,或在沙盒離線 shell 中執行。
  3. 啟動單元測試:
    dart test
    
  4. 驗證測試套件成功使用主機編譯器編譯本機原始檔,無編譯錯誤,且從未嘗試網路下載請求。