Rust 跨平台开发心得:Windows/macOS/Linux

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::PathPathBuf,不要手动拼字符串:

// 错误
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

常见跨平台问题清单

  1. 行尾符:Windows 的 \r\n vs Unix 的 \n,读文件时注意 .trim() 或用 lines() 迭代。
  2. 文件锁:Windows 上打开的文件不能删除/移动,Linux 可以。涉及文件操作的逻辑要注意这个差异。
  3. 环境变量:Windows 大小写不敏感,Linux 敏感。
  4. 可执行文件后缀:Windows 需要 .exe,可以用 std::env::consts::EXE_SUFFIX 获取。
  5. 信号处理SIGTERM/SIGINT 在 Windows 上行为不同,推荐用 ctrlc crate 统一处理。

把这些坑踩过一遍之后,Rust 的跨平台体验还是相当不错的。类型系统和 cfg 编译时检查能帮你避免很多运行时才暴露的平台差异问题。