Rust One API, Per-Platform Modules
The platform-module pattern gives each system its own small file behind one shared set of functions. Shared code calls the shared functions and never names a system.
One module name, two files
The mod line appears twice. Each version names its file with #[path], and only one version survives the build:
#[cfg(target_os = "windows")]
#[path = "imp_windows.rs"]
mod imp;
#[cfg(not(target_os = "windows"))]
#[path = "imp_unix.rs"]
mod imp;The unix file
Each file offers the same functions with the same types. Only the answers differ:
// imp_unix.rs
pub fn separator() -> char {
'/'
}
pub fn home_key() -> &'static str {
"HOME"
}The windows file
Same shape, Windows answers. The backslash wears its usual escape, as every Rust backslash does:
// imp_windows.rs
pub fn separator() -> char {
'\\'
}
pub fn home_key() -> &'static str {
"USERPROFILE"
}Or let cfg_attr pick the file
One mod line can carry both choices. cfg_attr attaches the path attribute that matches this build:
#[cfg_attr(target_os = "windows", path = "imp_windows.rs")]
#[cfg_attr(not(target_os = "windows"), path = "imp_unix.rs")]
mod imp;Callers never see the seam
Re-export the functions once. The rest of the program names the shared API, and which file loaded is not its business:
pub use imp::{home_key, separator};Example
#[cfg(target_os = "windows")]
mod imp {
pub fn separator() -> char { '\\' }
pub fn home_key() -> &'static str { "USERPROFILE" }
}
#[cfg(not(target_os = "windows"))]
mod imp {
pub fn separator() -> char { '/' }
pub fn home_key() -> &'static str { "HOME" }
}
fn main() {
println!("separator = {}", imp::separator());
println!("home key = {}", imp::home_key());
let folder = ["home", "ada"].join(&imp::separator().to_string());
println!("folder = {folder}");
}Key points
- The contract is the function list.
- Every platform file must offer the same names with the same types.
- Otherwise that platform's build breaks.
- Keep the platform files thin: facts and small translations belong there, and real logic belongs in shared code
- Before hand-writing a platform answer, check the standard library: MAIN_SEPARATOR and env::consts already carry several of these facts
- The file that loses the cfg race is not checked at all — not even for typos.
- Inside std, one public API stands in front of per-system modules — one seam, many systems.
- Platform facts then live in exactly one small place.
- The platform-module pattern hides each system's file behind one shared set of functions.
In this example, both modules live in one file, so you can see the whole pattern at once. The windows module is removed before checking, and the other one answers: separator is /, and the home key is HOME. The join line builds home/ada with whichever separator won. In a real project the two modules move into their own files, as the snippets show. The two-file version of this program prints the same lines here. The windows body tells a different story. Switched on in a scratch build, it prints separator = \ and home key = USERPROFILE. The Testing the Other Platform's Code lesson is about living with that.
Note
This is the standard library's own shape.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
In the platform-module pattern, what must every platform file agree on?