Standard library
The Rust standard library (std) is comparatively small but covers the substance of routine programming: collections, strings, I/O, threads, synchronisation, time, networking, file system, error handling, and the conventional traits. The library is layered: core admits no-allocation, no-OS dependencies (suitable for embedded no_std environments); alloc admits allocation (Vec, String, Box); std adds OS-dependent features (file I/O, threads, networking). The conventional Rust application uses the full std; embedded and constrained environments often use only core or core + alloc.
This tour points out the principal modules and their conventional uses.
std::collections
The standard collection types:
use std::collections::{
Vec, // (re-exported from prelude)
HashMap, BTreeMap,
HashSet, BTreeSet,
VecDeque, LinkedList,
BinaryHeap,
};
Treated in Data structures.
The conventional default is Vec<T> for sequences, HashMap<K, V> for key-value maps, HashSet<T> for sets. Tree-based versions (BTreeMap, BTreeSet) admit ordered iteration and range queries.
std::string and std::str
The string types: String (owned, growable, UTF-8) and str (the unsized slice type, conventionally seen as &str):
let s: String = String::from("hello");
let slice: &str = &s;
s.len();
s.is_empty();
s.contains("ell");
s.starts_with("he");
s.ends_with("lo");
s.replace("l", "r");
s.to_uppercase();
s.to_lowercase();
s.trim();
s.split(" ").collect::<Vec<&str>>();
s.chars().count(); // character count (not bytes)
Treated in Strings.
std::io
The I/O surface:
use std::io::{self, Read, Write, BufRead, BufWriter, BufReader};
// stdin/stdout/stderr:
let mut stdin = io::stdin();
let mut stdout = io::stdout();
let mut stderr = io::stderr();
// Read a line:
let mut line = String::new();
stdin.read_line(&mut line)?;
// Buffered I/O (much faster):
let stdin = io::stdin();
let reader = stdin.lock();
for line in reader.lines() {
println!("{}", line?);
}
Treated in I/O and serialisation.
std::fs
File-system operations:
use std::fs;
let contents: String = fs::read_to_string("file.txt")?;
let bytes: Vec<u8> = fs::read("file.bin")?;
fs::write("output.txt", "hello world")?;
fs::create_dir("dir")?;
fs::create_dir_all("a/b/c")?;
fs::remove_file("file.txt")?;
fs::remove_dir("dir")?;
fs::remove_dir_all("a")?;
fs::copy("from.txt", "to.txt")?;
fs::rename("old.txt", "new.txt")?;
let metadata = fs::metadata("file.txt")?;
println!("size: {}", metadata.len());
println!("modified: {:?}", metadata.modified()?);
for entry in fs::read_dir(".")? {
let entry = entry?;
println!("{}", entry.path().display());
}
Treated in I/O and serialisation.
std::path
Path manipulation:
use std::path::{Path, PathBuf};
let p = Path::new("/etc/hosts");
println!("{}", p.display()); // "/etc/hosts"
println!("{:?}", p.file_name()); // Some("hosts")
println!("{:?}", p.parent()); // Some("/etc")
println!("{:?}", p.extension()); // None
println!("{}", p.is_absolute()); // true
let mut buf = PathBuf::from("/etc");
buf.push("hosts"); // "/etc/hosts"
let combined = Path::new("/usr").join("local").join("bin");
The Path is the borrowed view; PathBuf is the owned, growable path. The relationship mirrors &str/String.
std::env
Environment and process arguments:
use std::env;
let args: Vec<String> = env::args().collect();
let exe = env::current_exe()?;
let cwd = env::current_dir()?;
let home = env::var("HOME")?;
let path = env::var("PATH").unwrap_or_default();
env::set_var("MY_VAR", "value");
for (key, value) in env::vars() {
println!("{}={}", key, value);
}
std::process
Process management:
use std::process::{Command, exit};
let output = Command::new("ls")
.args(&["-la", "/etc"])
.output()?;
println!("{}", String::from_utf8_lossy(&output.stdout));
println!("{}", String::from_utf8_lossy(&output.stderr));
println!("status: {}", output.status);
// Exit:
exit(1);
// Inherit stdout/stderr:
let status = Command::new("git")
.arg("status")
.status()?;
std::time
Time and duration:
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
let d = Duration::from_secs(5);
let d = Duration::from_millis(500);
let d = Duration::from_micros(100);
let d = Duration::from_nanos(1000);
// Monotonic time (for measurements):
let start = Instant::now();
do_work();
let elapsed = start.elapsed();
println!("took {:?}", elapsed);
// Wall-clock time:
let now = SystemTime::now();
let since_epoch = now.duration_since(UNIX_EPOCH)?;
println!("seconds since epoch: {}", since_epoch.as_secs());
// Sleep:
std::thread::sleep(Duration::from_secs(1));
For elaborate date and time handling (parsing, formatting, time zones), the third-party chrono and time crates are conventional.
std::thread
OS threads:
use std::thread;
use std::time::Duration;
let handle = thread::spawn(|| {
thread::sleep(Duration::from_secs(1));
42
});
let result = handle.join().unwrap();
let current = thread::current();
println!("thread name: {:?}", current.name());
let builder = thread::Builder::new()
.name("worker".to_string())
.stack_size(2 * 1024 * 1024);
let handle = builder.spawn(|| {
// ...
})?;
Treated in Concurrency.
std::sync
Synchronisation primitives:
use std::sync::{Arc, Mutex, RwLock, Barrier, OnceLock};
use std::sync::atomic::{AtomicI32, AtomicBool, Ordering};
use std::sync::mpsc;
Treated in Concurrency and Smart pointers.
std::net
Networking:
use std::net::{TcpStream, TcpListener, UdpSocket, SocketAddr, IpAddr, Ipv4Addr};
use std::io::{Read, Write};
// TCP client:
let mut stream = TcpStream::connect("example.com:80")?;
stream.write_all(b"GET / HTTP/1.0\r\n\r\n")?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
// TCP server:
let listener = TcpListener::bind("127.0.0.1:8080")?;
for stream in listener.incoming() {
let mut stream = stream?;
handle_connection(&mut stream);
}
// UDP:
let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.send_to(b"hello", "127.0.0.1:8080")?;
For elaborate networking (HTTP clients, TLS, protocol handling), third-party crates are conventional: reqwest for HTTP, tokio for async networking, tungstenite for WebSocket.
std::error
The Error trait:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct MyError(String);
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for MyError {}
Treated in Error handling.
std::fmt
Formatting:
use std::fmt::{self, Display, Debug};
struct Point { x: i32, y: i32 }
impl Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
let p = Point { x: 1, y: 2 };
println!("{}", p); // (1, 2)
println!("{:?}", p); // Point { x: 1, y: 2 }
The Debug is conventionally derived (#[derive(Debug)]); Display is implemented for user-facing output.
std::convert
Type conversion traits:
use std::convert::{From, Into, TryFrom, TryInto, AsRef, AsMut};
// From / Into are paired:
let n: i64 = 42i32.into(); // i32 → i64
let s: String = String::from("hello");
// TryFrom / TryInto for fallible conversions:
let n: i32 = 42i64.try_into()?;
let n = i32::try_from(42i64)?;
// AsRef / AsMut for cheap reference conversions:
fn process<T: AsRef<str>>(s: T) {
let s: &str = s.as_ref();
// ...
}
process("hello"); // works
process(String::from("hello")); // works
process(&String::from("hello")); // works
The conventional discipline is to take &str, &[T], or generic T: AsRef<...> parameters for substantial flexibility.
std::cmp
Comparison:
use std::cmp::{Ordering, min, max, Ord, PartialOrd, Eq, PartialEq, Reverse};
let a = 3;
let b = 5;
match a.cmp(&b) {
Ordering::Less => println!("a < b"),
Ordering::Equal => println!("a == b"),
Ordering::Greater => println!("a > b"),
}
let smaller = min(3, 5);
let larger = max(3, 5);
let mut v = vec![3, 1, 4, 1, 5];
v.sort(); // ascending
v.sort_by(|a, b| b.cmp(a)); // descending
v.sort_by_key(|x| Reverse(*x)); // descending via Reverse
std::iter
Iterator adapters and combinators:
use std::iter::{repeat, once, empty, from_fn};
let r = repeat(5).take(10); // 5, 5, 5, ...
let o = once(42);
let e: std::iter::Empty<i32> = empty();
let counter = (0..).take(10); // infinite 0.. truncated to 10
let mut counter = 0;
let f = from_fn(|| {
counter += 1;
if counter <= 5 { Some(counter) } else { None }
});
Treated in Iterators.
std::option and std::result
The Option and Result types and methods:
let opt: Option<i32> = Some(5);
let r: Result<i32, &str> = Ok(5);
opt.unwrap_or(0);
opt.map(|n| n * 2);
opt.and_then(|n| if n > 0 { Some(n) } else { None });
opt.ok_or("missing");
opt.is_some();
r.unwrap_or(0);
r.map(|n| n * 2);
r.map_err(|e| e.to_string());
r.ok();
r.is_ok();
Treated in Error handling.
std::mem
Memory operations:
use std::mem;
mem::size_of::<i32>(); // 4
mem::size_of_val(&5); // 4
mem::align_of::<i32>(); // 4
let mut a = 1;
let mut b = 2;
mem::swap(&mut a, &mut b); // a = 2, b = 1
let v = mem::take(&mut value); // replace with default
let v = mem::replace(&mut value, new_value); // replace with specified
mem::drop(value); // drop early
The mem::swap and mem::take are conventional for substituting values without move violations.
std::ops
Operator traits:
use std::ops::{Add, Sub, Mul, Neg, Deref, DerefMut, Index, IndexMut};
#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }
impl Add for Vec2 {
type Output = Vec2;
fn add(self, rhs: Self) -> Self::Output {
Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b; // Vec2 { x: 4.0, y: 6.0 }
Treated in Operators and Traits.
std::marker
Marker traits:
use std::marker::{Send, Sync, Copy, Sized, PhantomData};
The PhantomData<T> admits using a generic parameter without storing a value of T:
struct Tagged<T> {
value: i32,
_phantom: std::marker::PhantomData<T>,
}
The mechanism is conventional for type-level state machines and FFI wrappers.
std::any
Type introspection (rarely used):
use std::any::{Any, TypeId};
fn type_id<T: 'static>(_: &T) -> TypeId {
TypeId::of::<T>()
}
fn print_type<T: Any>(value: T) {
let any: Box<dyn Any> = Box::new(value);
if let Some(s) = any.downcast_ref::<String>() {
println!("string: {}", s);
} else if let Some(i) = any.downcast_ref::<i32>() {
println!("integer: {}", i);
}
}
The Any admits dynamic typing in narrow cases; the conventional Rust style avoids it for static-typed alternatives.
std::ffi
C interop:
use std::ffi::{CString, CStr, OsString, OsStr};
let s = CString::new("hello").unwrap();
let bytes: &[u8] = s.as_bytes_with_nul();
let path = std::env::var_os("PATH"); // OsString — platform-native string
The CString/CStr admit interoperability with C; OsString/OsStr admit handling platform-specific strings (which may not be valid UTF-8, e.g., on Windows).
core and alloc
The standard library’s layers:
core— fundamental types and traits, no allocation, no OS.Option,Result,Iterator, primitives, slice operations.
alloc— heap-allocated types.Box,Vec,String,Rc,Arc, collections.
std— OS-dependent features.- File I/O, threads, networking, time.
Embedded code conventionally uses #![no_std] and depends on core (and optionally alloc); std is unavailable on platforms without an OS.
A note on third-party crates
The Rust ecosystem treats many features as third-party rather than standard:
- Date/time —
chrono,time. - Random numbers —
rand. - Regular expressions —
regex. - Serialisation —
serde,serde_json,bincode. - Logging —
log(facade),env_logger/tracing. - HTTP client —
reqwest. - Async runtime —
tokio,async-std. - Error handling —
thiserror,anyhow. - CLI parsing —
clap. - Hashing —
sha2,blake3. - UUID —
uuid. - Web frameworks —
axum,rocket,actix-web. - Database access —
sqlx,diesel,sea-orm.
The conventional Rust application brings in several of these; the standard library focuses on substance and stability rather than feature completeness.
A note on the conventional discipline
The standard-library advice:
- Use
Vec<T>,String,HashMap<K, V>as the conventional defaults. - Use the
std::fsandstd::pathfor file-system operations. - Use
std::time::Durationfor time spans;Instantfor measurements. - Use
std::threadandstd::syncfor OS-thread concurrency. - Reach for crates.io for elaborate functionality (date/time, HTTP, regex, etc.).
- Use
std::process::Commandfor spawning subprocesses. - Use
std::envfor environment variables and process arguments. - Implement
Displayon types meant for user output;Debug(typically derived) for development.
The combination — substantial standard library covering routine programming, layered design admitting embedded use, conventions favouring third-party crates for elaborate functionality — is the substance of Rust’s standard library philosophy. The mechanism admits substantial reuse and a stable, well-tested foundation.