Rust 天然支持交叉编译和多平台,但实际做跨平台项目时还是会遇到不少坑。这篇文章总结了条件编译、平台特定 API 封装、CI 矩阵测试等方面的实践经验。
条件编译基础
Rust 的条件编译通过 cfg 属性实现,编译器根据目标平台自动设置相关 flag:
#[cfg(target_os = "windows")]
fn get_config_dir() -> PathBuf {
let appdata = std::env::var("APPDATA").unwrap();
PathBuf::from(appdata).join("myapp")
}
#[cfg(target_os = "macos")]
fn get_config_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join("Library/Application Support/myapp")
}
#[cfg(target_os = "linux")]
fn get_config_dir() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
PathBuf::from(xdg).join("myapp")
} else {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home).join(".config/myapp")
}
}
更好的做法是用 dirs crate 统一处理,但理解底层 cfg 机制对于需要调用平台原生 API 的场景很有必要。
cfg 的组合语法
// AND
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
// OR
#[cfg(any(target_os = "windows", target_os = "macos"))]
// NOT
#[cfg(not(target_os = "windows"))]
// 在表达式中使用
let timeout = if cfg!(target_os = "windows") { 5000 } else { 3000 };
封装平台差异
推荐的代码组织方式是把平台特定代码放在独立模块中,对外暴露统一接口:
src/
├── platform/
│ ├── mod.rs
│ ├── windows.rs
│ ├── macos.rs
│ └── linux.rs
├── main.rs
platform/mod.rs:
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::*;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;
每个平台模块实现相同的 public 函数签名。这样调用方完全不用关心平台差异。
平台特定 API 示例:获取系统内存信息
Windows 需要调用 Win32 API,Linux 读 /proc/meminfo,macOS 用 sysctl:
// platform/linux.rs
pub fn total_memory_mb() -> u64 {
let content = std::fs::read_to_string("/proc/meminfo").unwrap();
for line in content.lines() {
if line.starts_with("MemTotal:") {
let kb: u64 = line.split_whitespace()
.nth(1).unwrap()
.parse().unwrap();
return kb / 1024;
}
}
0
}
// platform/windows.rs
use windows::Win32::System::SystemInformation::*;
pub fn total_memory_mb() -> u64 {
unsafe {
let mut info = MEMORYSTATUSEX::default();
info.dwLength = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
GlobalMemoryStatusEx(&mut info).unwrap();
info.ullTotalPhys / (1024 * 1024)
}
}
文件路径处理
跨平台最常见的坑就是路径分隔符。始终使用 std::path::Path 和 PathBuf,不要手动拼字符串:
// 错误
let path = format!("{}/config/{}", home, filename);
// 正确
let path = PathBuf::from(home).join("config").join(filename);
另一个坑是 Windows 的路径前缀(\\?\)和大小写不敏感。如果需要比较路径,在 Windows 上要先 canonicalize:
fn paths_equal(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
}
CI 矩阵测试
GitHub Actions 的矩阵策略可以同时在三个平台上测试:
name: CI
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable, nightly]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/nust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- run: cargo test --all-features
- run: cargo clippy -- -D warnings
if: matrix.rust == 'stable'
平台特定测试
#[test]
#[cfg(target_os = "linux")]
fn test_proc_meminfo_readable() {
assert!(std::fs::metadata("/proc/meminfo").is_ok());
}
#[test]
#[cfg(target_os = "windows")]
fn test_registry_access() {
// Windows 特定的注册表测试
}
交叉编译
Rust 支持交叉编译到其他平台,但需要对应的 linker:
# 添加目标
rustup target add x86_64-pc-windows-gnu
rustup target add x86_64-unknown-linux-gnu
rustup target add aarch64-apple-darwin
# 交叉编译
cargo build --target x86_64-pc-windows-gnu --release
对于有 C 依赖的项目,交叉编译会麻烦很多。推荐用 cross 工具,它用 Docker 容器提供完整的交叉编译环境:
cargo install cross
cross build --target x86_64-unknown-linux-gnu --release
cross build --target aarch64-unknown-linux-gnu --release
常见跨平台问题清单
- 行尾符:Windows 的
\r\nvs Unix 的\n,读文件时注意.trim()或用lines()迭代。 - 文件锁:Windows 上打开的文件不能删除/移动,Linux 可以。涉及文件操作的逻辑要注意这个差异。
- 环境变量:Windows 大小写不敏感,Linux 敏感。
- 可执行文件后缀:Windows 需要
.exe,可以用std::env::consts::EXE_SUFFIX获取。 - 信号处理:
SIGTERM/SIGINT在 Windows 上行为不同,推荐用ctrlccrate 统一处理。
把这些坑踩过一遍之后,Rust 的跨平台体验还是相当不错的。类型系统和 cfg 编译时检查能帮你避免很多运行时才暴露的平台差异问题。