To make manageable #Rust stacks over time, I’ve ended up to following conclusions on how to make I/O connectors between library crates:
Read
, Seek
and Write
traits that specify an API that is a more constrained versions of std::io
counterparts. Here the masking is used for benefit so that all in-crate code depends on exactly to internal traits.std.rs
containing std::io
implementations and macro shenanigans to use it [1]. This way std
can be compiled out if required (e.g. when used with embedded_io
).This way my own crates are pretty easy to glue to std
, embedded_io
and other crates providing the I/O backend so this sort of indirection makes a lot of sense. The goal is to have a structure with clean separation of internals and connectors to the outside world (which can or might not include std
).
[1]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
mod std;
If the masking gets in the way (which it rarely actually does because compiler is smart), some corner cases can be sorted by calling convention e.g. std::io::Read::read(self, buf)
. This happens super rarely in practice.