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 代码”、“链接 native assets”、“实现 build.dart 或 link.dart hook”或“在 Dart/Flutter 中集成 C/C++ 互操作”。帮助 Agent 摆脱手动编排编译工具链的繁琐流程,并支持安全且经过哈希校验的预编译二进制下载,或者通过 `package:record_use` 映射实现高级链接器 Tree-Shaking 优化。

2792Star
164Fork
更新于 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 代码”、“链接 native assets”、“实现 build.dart 或 link.dart hook”或“在 Dart/Flutter 中集成 C/C++ 互操作”。帮助 Agent 摆脱手动编排编译工具链的繁琐流程,并支持安全且经过哈希校验的预编译二进制下载,或者通过 `package:record_use` 映射实现高级链接器 Tree-Shaking 优化。

使用 Native Assets Hook 将 C 代码编译为 Code Assets

借助构建与链接 Hook,将原生 C/C++ 源码无缝集成并自动编译打包为 Dart 整体 Native Assets 体系下的 Code Assets

目录


简介

在 Dart 的 Native Assets 特性下,Package 可以将原生代码(如 C/C++ 库)打包为 Code Assets,并在标准开发生命周期(如 dart rundart testdart build 以及 flutter run)中自动完成构建与捆绑。Code Assets 的打包流程由放置在 Package 根目录 hook/ 文件夹下的两个 Hook 脚本驱动:

  1. hook/build.dart:针对具体的宿主/目标架构,将本地 C 源码编译为机器码,或直接将预构建的原生二进制文件打包为 Code Assets。
  2. hook/link.dart:对构建好的 Code Assets 进行链接,应用高级 Tree-Shaking 剪枝优化,剔除未使用的原生符号,从而大幅压缩运行时二进制体积。

约束条件

[!IMPORTANT]
必须保持所有文件路径解析跨平台独立。切勿硬编码绝对目标路径、Shell 脚本或系统命令变量。请始终使用 Platform.script.resolve() 或基于 Uri 的解析方式,以确保脚本具备完全的可移植性。

  • Hook 放置位置:编译与打包 Hook 必须严格存放在 Package 根目录下的 hook/ 目录中:
    • hook/build.dart(构建执行阶段)
    • hook/link.dart(可选的打包/链接/Tree-Shaking 阶段)
  • 编译工具链标准:请使用 package:native_toolchain_c 提供的声明式 API(例如 CBuilderCLibrary)来运行编译工具链。严禁通过 Shell 命令直接调用底层的 gccclangmsvc
  • 前言与 License 声明头:所有手动编写及自动生成的源文件(包括 bindings、helper 和 hook)都必须严格包含目标 Package 的版权与 License 声明头。
  • 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 哈希表进行校验,确保二进制文件的完整性,防止被篡改。
    • 平滑容灾:需为离线开发者提供降级方案(例如通过 local_build 等标志位回退到本地编译)。

原生互操作 Package

用于 Code Assets 的声明式 build 和 link hook 主要依赖以下三个专用的原生互操作 Package:

依赖项 用途 核心 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 步:添加依赖

为你的 Package 添加 Code Assets hook 与工具链依赖。请直接从 pub.dev 拉取这些依赖。

你可以使用命令行工具自动添加:

dart pub add code_assets hooks native_toolchain_c record_use dev:ffigen

或者手动在目标 Package 的 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 库的编译元数据。这样可以保证 build 和 link hook 共享同一份关于资产、名称和源码的单一事实来源(Single Source of Truth)。

第 3 步:实现 Build 和 Link Hook 脚本

hook/build.dart 中编写编译编排脚本,并在 hook/link.dart 中实现死代码消除(Dead-code elimination)逻辑。

第 4 步:运行 Hook 周期

运行标准的测试套件会在后台动态触发 build 和 link hook 的生命周期:

dart test

选择集成方案

在 Dart 中集成与交付 C/C++ 原生资产主要有两种方案,请根据你的项目需求进行选择:

维度 方案 1:本地编译 + Tree-Shaking 方案 2:预编译下载
核心使用场景 C/C++ 源码直接打在 Package 内,且追求极限的体积优化。 本地编译缓慢/复杂,或希望避免对开发者宿主环境提出工具链要求。
宿主工具链要求 需要预先安装对应平台的 C 编译器(Xcode 工具、MSVC、GCC)。 开发者或用户机器上无需配置任何编译器。
二进制文件优化 极致。未使用的符号会被彻底剪枝(Tree-shaken),显著减小库体积。 标准。直接原样交付标准编译好的二进制文件。
离线支持 完全合规。可纯离线工作。 需要网络权限来下载库,但支持离线降级方案。

方案 1:本地编译 + 链接器 Tree-Shaking(推荐)

在这种方案中,build hook 会调用本地工具链(GCC、Clang、MSVC)直接编译源文件。随后,link hook 利用编译器选项过滤输出符号,仅保留用户代码中实际调用到的目标方法。这是 pkgs/code_assets/example/sqlite 下标准的 SQLite 稳健实践模式。

宿主编译器工具链依赖

由于 package:native_toolchain_c 将实际的动态编译委托给了宿主操作系统的默认工具链,因此开发机器上必须预装以下编译器 Package 之一:

  • 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 中未找到兼容的工具链,build hook 脚本抛出编译执行异常。如果无法保证环境配有工具链,请务必指定编译器约束或改用方案 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';

/// 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。这会在 hook 的目标目录下将库编译为动态库(如 .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)
            // 确保在 Windows DLL 中显式导出 C 函数
            '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:下载预编译动态库

另一种方案是在中央构建机器上预先编译好二进制文件并归档,然后在 build hook 执行期间下载目标二进制文件。这与 download_asset hook package 中展示的范式一致。

为什么要下载预编译二进制文件?

  • 宿主环境限制:编译大型

<!-- truncated for translation batch; full body continues in source -->