#[cfg_attr(…, path = …)] for platform-specific module implementations

I wrote about #[cfg_attr] and some interesting use cases for it some years ago. Time for another dose!

I have seen people write things like this often:

#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use self::unix::*;

#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use self::windows::*;

… and commonly even more variations …

Figure 1: multiple mod and use statements in order to switch between platform‐specific implementations.

This can get unwieldy.

There are two features that you can combine to make this simpler and smaller:

Here’s how it goes when combined:

#[cfg_attr(unix, path = "unix.rs")]
#[cfg_attr(windows, path = "windows/mod.rs")]
… and perhaps even more variations …
mod platform;
pub use self::platform::*;

Figure 2: just one mod statement decorated by multiple attributes in order to switch between implementations, and one use statement.

And so in this example, on Unix platforms it’ll load unix.rs as the platform module, while on Windows it’ll load windows/mod.rs [FYI, if you went for windows/mod.rs because windows contained other modules inside it, know that from Rust 1.30 onwards you can actually have a file named windows.rs beside that windows/ subdirectory, instead of it having to be windows/mod.rs. The Rust Reference even recommends switching to this new style, though I’m not sold on it.] as the platform module, and so in either case it can subsequently do a reexport if desired.


Mind you, for complex enough specifications, even the single mod version can still be messy:

#[cfg_attr(
	all(
		target_arch = "wasm32",
		any(target_os = "emscripten", target_os = "unknown"),
	),
	path = "wasm32/mod.rs"
)]
#[cfg_attr(windows, path = "windows/mod.rs")]
#[cfg_attr(
	not(any(
		all(
			target_arch = "wasm32",
			any(target_os = "emscripten", target_os = "unknown"),
		),
		windows,
	)),
	path = "common/mod.rs"
)]
mod imp;

Figure 3: a very slightly unwieldy situation seen in os_str_bytes.

(While thinking about os_str_bytes, I wonder what the chances are of ever convincing the Rust core and libs teams to make cross‐platform OsStr[u8] and OsStringVec<u8> conversions (ideally plus checked and unchecked conversions for the other way). As it stands, OsStr::to_str forces them to be roughly UTF-8 already, and the current implementation depends on compatibility, which I can’t honestly imagine ever changing, but the docs stubbornly insist on being mysterious, and the source code stubbornly insists of OsStr, OsString, Path and PathBuf that their “representation and layout are considered implementation details, are not documented and must not be relied upon”. But such musings as these don’t fit in a figure caption. I can already just tell that half the comments about this article are going to be about this little side point.)

And truly they can get much worse than that; the worst cases I’ve seen might even be enough for me to reach for cfg-if (which I am loath to do—​I reckon the significant majority of its uses are unwarranted) and separate mod statements, rather than using #[cfg_attr].