
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 优化。
指导 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 run、dart test、dart build 以及 flutter run)中自动完成构建与捆绑。Code Assets 的打包流程由放置在 Package 根目录 hook/ 文件夹下的两个 Hook 脚本驱动:
hook/build.dart:针对具体的宿主/目标架构,将本地 C 源码编译为机器码,或直接将预构建的原生二进制文件打包为 Code Assets。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(例如CBuilder和CLibrary)来运行编译工具链。严禁通过 Shell 命令直接调用底层的gcc、clang或msvc。 - 前言与 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 -->





