Cross-Platform Systems Code
Write portable systems code with cfg, conditional compilation, and platform abstraction crates.
Recipe
#[cfg(unix)]
fn home_dir() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(std::path::PathBuf::from)
}
#[cfg(windows)]
fn home_dir() -> Option<std::path::PathBuf> {
std::env::var_os("USERPROFILE").map(std::path::PathBuf::from)
}When to reach for this: Tools and libraries that ship on Linux, macOS, and Windows without maintaining separate codebases.
Working Example
use std::path::{Path, PathBuf};
#[cfg(unix)]
mod platform {
use std::os::unix::fs::PermissionsExt;
pub fn is_executable(meta: &std::fs::Metadata) -> bool {
meta.permissions().mode() & 0o111 != 0
}
}
#[cfg(windows)]
mod platform {
pub fn is_executable(meta: &std::fs::Metadata) -> bool {
meta.is_file() && meta.file_name().to_string_lossy().ends_with(".exe")
}
}
pub fn find_in_path(name: &str) -> Option<PathBuf> {
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(name);
if candidate.is_file() {
if let Ok(meta) = std::fs::metadata(&candidate) {
if platform::is_executable(&meta) {
return Some(candidate);
}
}
}
}
None
}What this demonstrates:
cfg(unix)/cfg(windows)isolate platform code- Shared API with platform-specific implementations
std::env::split_pathshandlesPATHseparator differences- Executable checks differ per OS
Deep Dive
cfg Attributes
| Attribute | Matches |
|---|---|
unix | Linux, macOS, BSD |
windows | Windows MSVC/GNU |
target_os = "linux" | Linux only |
target_arch = "aarch64" | ARM64 |
Abstraction Crates
| Crate | Role |
|---|---|
std::path::Path | / vs \ separators |
dirs / etcetera | Known folders (config, cache) |
tempfile | Cross-platform temp files |
which | PATH lookup |
CI Matrix
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]Gotchas
- Path string assumptions - Hardcoded
/breaks on Windows. Fix: AlwaysPath::join. - Line endings -
\r\non Windows. Fix: Usestd::io::BufReadlines or#[cfg(windows)]trimming. - Case-sensitive paths - macOS default is case-insensitive. Fix: Normalize paths in tests; document behavior.
- Feature drift between targets - Code compiles on Linux only. Fix: Required CI on all three OS targets.
- Unix-only dependencies in default features - Windows build fails. Fix: Gate with
cfgor optional features.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Separate binaries per OS | Radically different UX | 90% shared logic |
cfg_if! macro | Many nested cfgs | Simple two-platform split |
| WASM target | Sandboxed portability | Need raw OS access |
FAQs
cfg vs runtime detection?
Prefer cfg for compile-time differences. Runtime std::env::consts::OS for logging only.
How do I test Windows code on Linux?
Cross-compile and run tests in CI on windows-latest runners.
Socket differences?
std::net is mostly portable. Edge cases (Unix sockets) need cfg(unix).
Filesystem watching?
notify crate abstracts inotify, FSEvents, and ReadDirectoryChangesW.
Max path length?
Windows has legacy MAX_PATH limits. Use \\?\ prefix for long paths when needed.
Process APIs?
std::process::Command is cross-platform. Unix-specific pre_exec needs cfg.
Symbol naming on Windows?
#[no_mangle] exports differ from Unix. Test dlopen/LoadLibrary per platform.
Android / iOS?
Use target_os = "android" / "ios" cfgs. Mobile has stricter sandbox rules.
WSL considerations?
Treat WSL as Linux for cfg(unix). Path translation across /mnt/c needs care.
Document platform support?
State supported OS list in README and fail with clear errors on unsupported platforms.
Related
- Systems Basics - portable std usage
- Signals & Process Control - OS-specific signals
- libc & Syscalls - platform-specific low level
- Systems Programming Best Practices - portability checklist
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.