Pipes

#049 Mar 2026

49. std::io::pipe — Anonymous Pipes in the Standard Library

Need to wire up stdout and stderr from a child process, or stream data between threads? Since Rust 1.87, std::io::pipe() gives you OS-backed anonymous pipes without reaching for external crates.

What’s an anonymous pipe?

A pipe is a one-way data channel: one end writes, the other reads. Before 1.87, you needed the os_pipe crate or platform-specific code to get one. Now it’s a single function call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    let (mut reader, mut writer) = io::pipe()?;

    writer.write_all(b"hello from the pipe")?;
    drop(writer); // close the write end so reads hit EOF

    let mut buf = String::new();
    reader.read_to_string(&mut buf)?;
    assert_eq!(buf, "hello from the pipe");

    println!("Received: {buf}");
    Ok(())
}

pipe() returns a (PipeReader, PipeWriter) pair. PipeReader implements Read, PipeWriter implements Write — they plug into any generic I/O code you already have.

Merge stdout and stderr from a child process

The killer use case: capture both output streams from a subprocess as a single interleaved stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::io::{self, Read};
use std::process::Command;

fn main() -> io::Result<()> {
    let (mut recv, send) = io::pipe()?;

    let mut child = Command::new("echo")
        .arg("hello world")
        .stdout(send.try_clone()?)
        .stderr(send)
        .spawn()?;

    child.wait()?;

    let mut output = String::new();
    recv.read_to_string(&mut output)?;
    assert!(output.contains("hello world"));

    println!("Combined output: {output}");
    Ok(())
}

The try_clone() on the writer lets both stdout and stderr write to the same pipe. When both copies of the PipeWriter are dropped (one moved into stdout, one into stderr), reads on the PipeReader return EOF.

Why not just use Command::output()?

Command::output() captures stdout and stderr separately into Vec<u8> — you get two blobs, no interleaving, and everything is buffered in memory. With pipes, you can stream the output as it arrives, merge the two streams, or fan data into multiple consumers. Pipes give you the plumbing; output() gives you the convenience.

Key behavior

A read on PipeReader blocks until data is available or all writers are closed. A write on PipeWriter blocks when the OS pipe buffer is full. This is the same behavior as Unix pipes under the hood — because that’s exactly what they are.