- 1:概述
- 2:Dart Hooks
- 2.1:什么是 Dart Hooks?
- 2.2:核心特性
- 2.3:架构
- 2.4:资源类型
- 2.5:实现步骤
- 2.6:Hook 执行流程
- 2.7:优势
- 3:Rust build.rs
- 3.1:什么是 build.rs?
- 3.2:核心特性
- 3.3:架构
- 3.4:常见用例
- 3.5:Cargo.toml 中的配置
- 3.6:关键 Cargo 指令
- 3.7:build.rs 可用的环境变量
- 3.8:构建脚本示例
- 3.9:构建脚本生命周期
- 3.10:优势
- 4:对比分析
- 4.1:相似之处
- 4.2:差异
- 5:使用场景与示例
- 5.1:场景 1:编译 C 库
- 5.2:场景 2:从模式生成代码
- 5.3:场景 3:平台特定原生代码
- 6:最佳实践
- 6.1:Dart Hooks 最佳实践
- 6.2:Rust build.rs 最佳实践
- 7:结论
概述
在我们构建CLI程序时,经常需要在执行代码之前编译其他语言的动态库进行链接来调用其他语言的函数或者执行一些预构建操作。本文档对比分析两个现代构建时自动化系统:Dart Hooks(引入于 Dart 3.10)和 Rust build.rs 脚本。两者都提供了在构建过程中执行自定义代码的机制,特别适用于集成原生代码和执行预编译任务。
Dart Hooks
什么是 Dart Hooks?
Dart hooks 是放置在包的 hook/ 目录中的专用 Dart 脚本,使开发者能够在将资源打包到 Dart 应用之前执行构建时任务,例如编译原生代码或下载资源。
核心特性
- 引入版本:Dart 3.10
- 位置:包根目录中的
hook/目录 - 自动发现:由 Dart SDK 自动发现和执行
- 当前支持:构建钩子(未来计划支持更多钩子类型)
- 用途:无缝编译和集成原生代码资源
架构
my_package/
├── lib/
│ └── my_package.dart
├── hook/
│ └── build.dart # 构建钩子脚本
├── src/
│ └── native_code.c
└── pubspec.yaml
资源类型
目前支持 CodeAsset,表示:
- 从非 Dart 语言(C、C++、Rust 等)编译的动态库
- 可通过带有 @Native 注解的 Dart FFI 访问
- 平台特定的原生二进制文件
实现步骤
- 在
pubspec.yaml中添加依赖:dependencies: hooks: ^0.1.0 code_assets: ^0.1.0 dev_dependencies: native_toolchain_c: ^0.1.0 # 用于 C 编译 - 创建
hook/build.dart:import 'package:hooks/hooks.dart'; import 'package:native_toolchain_c/native_toolchain_c.dart'; const packageName = 'my_package'; void main(List<String> args) async { await build(args, (input, output) async { final cBuilder = CBuilder.library( name: packageName, sources: ['src/$packageName.c'], ); await cBuilder.run(input: input, output: output); }); } - 在 Dart 代码中引用:
import 'dart:ffi'; import 'package:code_assets/code_assets.dart'; @Native<Int32 Function(Int32, Int32)>( assetId: 'package:my_package/my_package', ) external int add(int a, int b);
Hook 执行流程
Dart 构建过程
↓
发现 hooks/ 目录
↓
执行 hook/build.dart
↓
处理 BuildInput
↓
生成 CodeAssets
↓
输出到 BuildOutput
↓
继续主编译
↓
将资源打包到应用
优势
- 透明集成:原生代码与 Dart 无缝协作
- 自动资源打包:无需手动配置
- 跨平台:适用于所有 Dart 平台(移动、Web、桌面)
- 类型安全:具有静态类型检查的 FFI
- 简化管理:集中化的原生库处理
Rust build.rs
什么是 build.rs?
build.rs 文件是放置在包根目录中的特殊 Rust 脚本,Cargo 会在构建主包之前编译并执行它。它支持复杂的构建时操作和原生代码集成。
核心特性
- 位置:包根目录中的
build.rs - 执行时机:在包编译之前编译并运行
- 通信方式:使用
println!("cargo::...")指令与 Cargo 通信 - 重建逻辑:基于文件更改和依赖智能重建
- 生态系统:成熟的生态系统,包含
cc、bindgen、pkg-config等辅助 crate
架构
my_crate/
├── src/
│ └── lib.rs
├── build.rs # 构建脚本
├── native/
│ └── wrapper.c
└── Cargo.toml
常见用例
- 构建捆绑的 C/C++ 库
- 查找和链接系统库
- 代码生成(例如从协议缓冲区、模式生成)
- 平台特定配置
- 嵌入资源(资产、版本信息)
Cargo.toml 中的配置
[package]
name = "my_crate"
version = "0.1.0"
build = "build.rs" # 可选,默认为 build.rs
[build-dependencies]
cc = "1.0"
bindgen = "0.69"
pkg-config = "0.3"
关键 Cargo 指令
构建脚本通过向 stdout 打印特殊指令与 Cargo 通信:
| 指令 | 用途 | 示例 |
|---|---|---|
cargo::rerun-if-changed |
文件更改时触发重建 | println!("cargo::rerun-if-changed=src/hello.c"); |
cargo::rerun-if-env-changed |
环境变量更改时触发重建 | println!("cargo::rerun-if-env-changed=CC"); |
cargo::rustc-link-lib |
链接原生库 | println!("cargo::rustc-link-lib=sqlite3"); |
cargo::rustc-link-search |
添加库搜索路径 | println!("cargo::rustc-link-search=/usr/local/lib"); |
cargo::rustc-env |
设置编译时环境变量 | println!("cargo::rustc-env=VERSION=1.0.0"); |
cargo::rustc-cfg |
启用条件编译 | println!("cargo::rustc-cfg=has_feature"); |
cargo::rustc-cdylib-link-arg |
传递链接器参数 | println!("cargo::rustc-cdylib-link-arg=-Wl,-rpath,/path"); |
build.rs 可用的环境变量
构建脚本可访问 Cargo 设置的许多环境变量:
OUT_DIR:生成文件的输出目录TARGET:正在编译的目标三元组HOST:主机三元组(构建机器)CARGO_MANIFEST_DIR:包含 Cargo.toml 的目录PROFILE:构建配置文件(debug/release)DEP_<name>_<key>:来自依赖的元数据- 还有更多...
构建脚本示例
示例 1:编译 C 代码
// build.rs
fn main() {
// 当 C 源文件更改时重新运行
println!("cargo::rerun-if-changed=src/wrapper.c");
// 使用 cc crate 编译 C 代码
cc::Build::new()
.file("src/wrapper.c")
.include("native/include")
.compile("wrapper");
}
示例 2:代码生成
// build.rs
use std::env;
use std::fs;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("generated.rs");
// 生成 Rust 代码
let generated_code = r#"
pub const VERSION: &str = "1.0.0";
pub const BUILD_TIME: &str = "2025-01-15";
"#;
fs::write(&dest_path, generated_code).unwrap();
println!("cargo::rerun-if-changed=build.rs");
}
然后在 Rust 代码中包含:
// src/lib.rs
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
pub fn print_version() {
println!("Version: {}", VERSION);
}
示例 3:平台特定配置
// build.rs
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
match target_os.as_str() {
"windows" => {
println!("cargo::rustc-link-lib=user32");
println!("cargo::rustc-cfg=windows_platform");
}
"macos" => {
println!("cargo::rustc-link-lib=framework=CoreFoundation");
println!("cargo::rustc-cfg=macos_platform");
}
"linux" => {
println!("cargo::rustc-link-lib=pthread");
println!("cargo::rustc-cfg=linux_platform");
}
_ => {}
}
}
示例 4:使用 bindgen 生成绑定
// build.rs
use std::env;
use std::path::PathBuf;
fn main() {
println!("cargo::rerun-if-changed=wrapper.h");
// 从 C 头文件生成 Rust 绑定
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("无法生成绑定");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("无法写入绑定文件!");
}
构建脚本生命周期
Cargo 构建过程
↓
检查 build.rs 是否存在
↓
使用 build-dependencies 编译 build.rs
↓
执行编译后的构建脚本
↓
捕获 stdout 指令 (cargo::...)
↓
应用指令(链接库、设置环境变量等)
↓
使用应用的配置编译主包
↓
链接最终二进制文件
优势
- 灵活性:可执行任意 Rust 代码
- 深度集成:与 Cargo 构建系统深度集成
- 成熟生态系统:丰富的辅助 crate(cc、bindgen、pkg-config)
- 增量构建:智能重建检测
- 交叉编译:全面支持跨平台构建
对比分析
| 特性 | Dart Hooks | Rust build.rs |
|---|---|---|
| 成熟度 | 新(Dart 3.10+) | 成熟(自 Cargo 1.0) |
| 位置 | hook/build.dart |
build.rs |
| 语言 | Dart | Rust |
| 主要用例 | 原生资源编译 | 构建时自动化(通用) |
| 发现机制 | 自动目录扫描 | 文件名约定 |
| 通信方式 | Input/Output 对象 | stdout 加 cargo:: 前缀 |
| 依赖声明 | 通过 pubspec.yaml |
通过 [build-dependencies] |
| 资源类型 | CodeAsset(动态库) | 任意(库、生成的代码等) |
| 生态系统 | 成长中(native_toolchain_c) | 广泛(cc、bindgen、pkg-config) |
| 重建检测 | 自动 | 手动(rerun-if-changed) |
| 平台支持 | 所有 Dart 平台 | 所有 Rust 目标 |
| 类型安全 | 带 @Native 注解的 FFI | 生成的绑定或手动 FFI |
| 学习曲线 | 较低(如果熟悉 Dart) | 中等(需要 Cargo 知识) |
相似之处
✓ 都在主编译之前执行
✓ 都支持原生代码集成
✓ 都支持代码生成
✓ 都支持平台特定逻辑
✓ 都使用各自的语言编写
✓ 都与各自的构建系统无缝集成
差异
Dart Hooks:
- 更具主见性(专注于资源生成)
- 较新,API 仍在演进
- 结构化的输入/输出系统
- 内置资源管理
Rust build.rs:
- 更通用
- 经过实战检验且稳定
- 基于文本的指令系统
- 开发者手动管理输出文件
使用场景与示例
场景 1:编译 C 库
Dart Hooks 方式:
// hook/build.dart
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
final cBuilder = CBuilder.library(
name: 'mylib',
sources: [
'src/native/mylib.c',
'src/native/utils.c',
],
includes: ['src/native/include/'],
);
await cBuilder.run(input: input, output: output);
});
}
Rust build.rs 方式:
// build.rs
fn main() {
println!("cargo::rerun-if-changed=src/native/mylib.c");
println!("cargo::rerun-if-changed=src/native/utils.c");
cc::Build::new()
.files(&["src/native/mylib.c", "src/native/utils.c"])
.include("src/native/include")
.compile("mylib");
}
场景 2:从模式生成代码
Dart Hooks 方式:
// hook/build.dart
import 'dart:io';
import 'package:hooks/hooks.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
// 读取模式文件
final schema = File('schema/api.proto').readAsStringSync();
// 生成 Dart 代码(简化示例)
final generatedCode = generateFromProto(schema);
// 写入输出目录
final outFile = File('${input.outputDirectory}/generated.dart');
await outFile.writeAsString(generatedCode);
});
}
Rust build.rs 方式:
// build.rs
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
println!("cargo::rerun-if-changed=schema/api.proto");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
// 实际使用 prost 或类似工具进行 proto 编译
// 这是一个简化示例
let schema = fs::read_to_string("schema/api.proto").unwrap();
let generated_code = generate_from_proto(&schema);
fs::write(out_dir.join("generated.rs"), generated_code).unwrap();
}
fn generate_from_proto(schema: &str) -> String {
// 代码生成逻辑
String::new()
}
场景 3:平台特定原生代码
Dart Hooks 方式:
// hook/build.dart
import 'dart:io';
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
final sources = <String>['src/common.c'];
// 添加平台特定源文件
if (Platform.isWindows) {
sources.add('src/windows_impl.c');
} else if (Platform.isMacOS) {
sources.add('src/macos_impl.c');
} else if (Platform.isLinux) {
sources.add('src/linux_impl.c');
}
final cBuilder = CBuilder.library(
name: 'platform_lib',
sources: sources,
);
await cBuilder.run(input: input, output: output);
});
}
Rust build.rs 方式:
// build.rs
fn main() {
let mut build = cc::Build::new();
build.file("src/common.c");
// 添加平台特定源文件
#[cfg(target_os = "windows")]
build.file("src/windows_impl.c");
#[cfg(target_os = "macos")]
build.file("src/macos_impl.c");
#[cfg(target_os = "linux")]
build.file("src/linux_impl.c");
build.compile("platform_lib");
}
最佳实践
Dart Hooks 最佳实践
- 保持钩子简单且专注
- 每个钩子应有单一、明确的职责
- 复杂逻辑应放在独立的库中
- 优雅处理错误
void main(List<String> args) async { await build(args, (input, output) async { try { await cBuilder.run(input: input, output: output); } catch (e) { print('构建失败: $e'); rethrow; } }); } - 使用合适的资源类型
- 目前仅有 CodeAsset,但应使用最具体的可用类型
- 记录原生依赖
- 清晰记录所需的系统库或工具
- 在所有目标平台上测试
- 钩子在不同平台上可能表现不同
Rust build.rs 最佳实践
- 始终使用
rerun-if-changedfn main() { println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=src/wrapper.c"); // ... 其余构建脚本 } - 使用
OUT_DIR存放生成的文件let out_dir = env::var("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("generated.rs"); - 利用现有 crate
- 使用
cc进行 C/C++ 编译 - 使用
bindgen生成 FFI 绑定 - 使用
pkg-config查找系统库
- 使用
- 正确处理错误
fn main() { if let Err(e) = try_build() { eprintln!("构建失败: {}", e); std::process::exit(1); } } fn try_build() -> Result<(), Box<dyn std::error::Error>> { // 构建逻辑 Ok(()) } - 保持构建脚本快速
- 最小化不必要的工作
- 缓存昂贵的计算
- 使用条件编译避免每次构建都运行
- 使用功能标志控制可选行为
fn main() { #[cfg(feature = "native")] compile_native_code(); } - 记录构建要求
- 记录所需的系统工具(编译器等)
- 记录影响构建的环境变量
- 在不满足要求时提供清晰的错误消息
- 避免修改源文件
- 仅写入
OUT_DIR - 构建期间绝不修改
src/
- 仅写入
结论
Dart Hooks 和 Rust build.rs 都为构建时自动化和原生代码集成提供了强大的机制。
选择 Dart Hooks 的场景:
- 在 Dart 生态系统中工作
- 需要简单、有主见的原生资源编译
- 想要自动资源打包
- 偏好使用输入/输出对象的结构化方法
选择 Rust build.rs 的场景:
- 在 Rust 生态系统中工作
- 需要最大的灵活性和控制
- 有复杂的构建需求
- 想要利用成熟的构建工具生态系统
这两个系统都展示了现代编程语言如何认识到构建时自动化的重要性,并为集成原生代码和执行预编译任务提供了一流的支持。随着 Dart Hooks 成熟并扩展支持更多钩子类型,这两个系统之间的差距可能会缩小,但每个系统可能会保留其为各自生态系统量身定制的独特优势。