Standard Input and Output of Child Processes
Every child process starts with three standard I/O descriptors—standard input (stdin), standard output (stdout), and standard error (stderr)—configured via std::process::Stdio. Connecting these streams to OS pipes, files, or null devices enables bidirectional communication and multi-stage process pipelines.
Parent Process Child Process
+-----------------------------+ +-------------------+
| child.stdin: ChildStdin --+---[ OS Pipe Buffer ]->| fd 0 (stdin) |
| | | |
| child.stdout: ChildStdout <-+---[ OS Pipe Buffer ]--| fd 1 (stdout) |
| | | |
| child.stderr: ChildStderr <-+---[ OS Pipe Buffer ]--| fd 2 (stderr) |
+-----------------------------+ +-------------------+
Stdio Configuration Options
| Constructor | Behavior | Handle on Child |
|---|---|---|
Stdio::inherit() |
Child shares the parent's stdin, stdout, or stderr descriptor directly. |
None |
Stdio::piped() |
Creates an OS pipe connecting the parent and child. | Some(ChildStdin) / Some(ChildStdout) / Some(ChildStderr) |
Stdio::null() |
Connects the stream to /dev/null (Unix) or NUL (Windows), discarding output or returning immediate EOF on read. |
None |
Stdio::from(file) |
Redirects the stream directly to or from an open std::fs::File, ChildStdout, ChildStderr, or OwnedFd. |
None |
Writing to ChildStdin and Reading from ChildStdout
When a child process reads from stdin until EOF before producing output (or while producing output), the parent must take and drop child.stdin after writing so the child receives an EOF signal. Furthermore, if a child writes more data to stdout than fits in the OS pipe buffer (typically 16–64 KB) while the parent is still blocked writing a large payload to stdin, both processes can deadlock unless stdin is written on a separate thread.
use std::io::{self, Write};
use std::process::{Command, Stdio};
use std::thread;
fn main() -> io::Result<()> {
// Compile any stdin source directly using `rustc - --crate-type lib`
let mut child = Command::new("rustc")
.args(["-", "--crate-type", "lib", "-Z", "unpretty=normal"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
// Take ownership of `ChildStdin` so it is closed (dropped) when the writer finishes
let mut stdin = child.stdin.take().expect("stdin was piped");
let writer = thread::spawn(move || -> io::Result<()> {
stdin.write_all(b"pub fn add(a: i32, b: i32) -> i32 { a + b }\n")?;
Ok(()) // `stdin` drops here, closing the pipe and sending EOF to child
});
// Wait for child and read all stdout/stderr concurrently
let _output = child.wait_with_output()?;
writer.join().expect("writer thread panicked")?;
println!("Pipe write and child wait completed without deadlock");
Ok(())
}
Pipe write and child wait completed without deadlock
Redirecting Child Output Directly to a File
Because Stdio implements From<File>, you can redirect a child's stdout or stderr straight to a file at the OS file-descriptor level without copying bytes through user-space buffers in the parent process:
use std::env;
use std::fs::{self, File};
use std::io;
use std::process::{Command, Stdio};
fn main() -> io::Result<()> {
let log_path = env::temp_dir().join("rust_ch51_rustc_ver.log");
let log_file = File::create(&log_path)?;
let status = Command::new("rustc")
.arg("--version")
.stdout(Stdio::from(log_file))
.status()?;
assert!(status.success());
let logged = fs::read_to_string(&log_path)?;
println!("File redirection captured: {}", logged.trim());
fs::remove_file(&log_path)?;
Ok(())
}
File redirection captured: rustc 1.85.0 (4d91de4e4 2025-02-17)
Chaining Two Child Processes into a Pipeline
ChildStdout also implements Into<Stdio>, allowing the stdout pipe of one child process to be wired directly into the stdin of a second child process (equivalent to cmd1 | cmd2 in a shell):
let mut first = Command::new("producer").stdout(Stdio::piped()).spawn()?;
let pipe_out: Stdio = first.stdout.take().unwrap().into();
let second = Command::new("consumer").stdin(pipe_out).output()?;
first.wait()?;