os_pipe.rs
A cross-platform Rust crate for opening OS pipes with safe, non-inheritable PipeReader and PipeWriter types.
Repository Health
Technical Analysis
os_pipe is a small cross-platform Rust crate for opening OS-level pipes — the kind created by pipe/pipe2 on Linux and CreatePipe on Windows — without reaching for platform-specific FFI directly. It returns a PipeReader/PipeWriter pair backed by std::fs::File, both of which implement Into<Stdio> so they drop straight into std::process::Command’s stdin/stdout/stderr for wiring up child process I/O, including joining a child’s stdout and stderr into a single combined stream.
Beyond raw pipe creation, the crate exposes dup_stdin, dup_stdout, and dup_stderr helpers that duplicate the current process’s standard handles as owned pipe ends, useful when extra copies of the parent’s streams are needed without disturbing the originals. Every pipe it creates is non-inheritable by default (CLOEXEC on Unix, non-inheritable handles on Windows), preventing children from silently holding a copy of a pipe end they weren’t explicitly given. As of Rust 1.87, similar functionality is available via std::io::pipe, but os_pipe remains useful for supporting older compiler versions, and its README documents at length the deadlock pitfalls that come with pipe-based I/O.
What You Get
- Cross-platform pipe() - Opens a non-inheritable OS pipe on both Unix and Windows behind one function call, backed by
pipe2()/CreatePiperespectively. - Stdio-ready types -
PipeReaderandPipeWriterimplementInto<Stdio>, so they plug directly intoCommand::stdin/stdout/stderr. - Standard stream duplication -
dup_stdin,dup_stdout, anddup_stderrreturn owned duplicates of the current process’s standard handles. - try_clone support - Both pipe ends implement
try_clone, letting a single pipe be fanned out to multiple owners, e.g. combining a child’s stdout and stderr. - Deadlock-focused documentation - The README explains the two classic pipe deadlock patterns (unclosed writers, full buffers) with worked examples.
Common Use Cases
- Joining child stdout/stderr - Cloning a pipe writer and setting it as both
stdoutandstderron aCommandto interleave a child process’s output into one stream. - Custom child process I/O plumbing - Building tools that spawn subprocesses and need direct, low-level control over pipe lifetimes instead of relying on
Stdio::piped(). - Duplicating parent stdio for children - Passing
dup_stdout()/dup_stderr()results to a child so it shares a synchronized copy of the parent’s streams. - Supporting older Rust toolchains - Using os_pipe as a compatibility shim on projects that can’t yet require Rust 1.87’s
std::io::pipe.
Under The Hood
Architecture
The crate keeps a tiny, focused surface: lib.rs defines the public PipeReader/PipeWriter wrapper types (both thin newtypes over std::fs::File) and the pipe(), dup_stdin(), dup_stdout(), and dup_stderr() functions, all of which delegate to a sys module whose implementation is switched at compile time via #[cfg(windows)]/#[cfg(not(windows))] #[path] attributes pointing at unix.rs or windows.rs. This isolates every platform-specific syscall (pipe2/fcntl on Unix, CreatePipe on Windows) inside the sys module while the public API, trait implementations (io::Read/Write, Into<Stdio>, AsRawFd/AsRawHandle, AsFd/AsHandle), and documentation stay platform-agnostic; changing the core File-backed representation would touch both platform modules but not the public API shape.
Tech Stack
Built for Rust 2021 edition with an MSRV of 1.63, published to crates.io with docs hosted on docs.rs. It depends on libc (^0.2.62) on non-Windows targets for pipe2/fcntl, and on windows-sys (pinned to a wide >=0.28, <=0.61 range with the Win32_Foundation, Win32_System_Pipes, and Win32_Security features) on Windows for CreatePipe. There is no async runtime, no build script, and no other runtime dependency; a GitHub Actions workflow (referenced by the README’s badge) runs the test suite.
Code Quality
Tests live inline under #[cfg(test)] mod tests in lib.rs and exercise both the common path (writing and reading data of varying sizes, try_clone, Debug formatting) and cross-process edge cases (verifying pipes are genuinely non-inheritable, that parent-handle duplication doesn’t close the originals) by shelling out to small helper binaries in src/bin/ (cat, swap, cat_both) built just for the test run. All public functions return io::Result and propagate OS errors via io::Error::last_os_error() rather than panicking or swallowing failures; naming follows idiomatic Rust conventions throughout. No linter configuration file is present in the repo beyond standard rustfmt conventions, but CI is wired up via GitHub Actions.
What Makes It Unique
The crate itself is intentionally unglamorous — a faithful, correct cross-platform wrapper around two very different native pipe APIs — and its own README candidly notes that Rust 1.87’s std::io::pipe has since absorbed most of its core functionality, making os_pipe primarily a compatibility layer for older toolchains today. Its lasting value is in getting the non-inheritability and cross-process ownership semantics right on both platforms, and in documenting — clearly and at length — the two deadlock patterns (forgotten writers, full pipe buffers) that trip up nearly every developer who first works with OS pipes directly.