The Day libc Died: How Rust’s Core and Alloc Crates Work Together
When you strip Rust down to its bare metal, what’s left isn’t chaos — it’s a masterpiece of modular design.
The Day libc Died: How Rust’s Core and Alloc Crates Work Together
When you strip Rust down to its bare metal, what’s left isn’t chaos — it’s a masterpiece of modular design.
If you’ve ever tried compiling Rust code for an embedded board, a bare-metal kernel, or even a toy OS, you’ve probably hit this cryptic error:
error[E0463]: can't find crate for `std`
That moment feels like stepping off a cliff. Suddenly, your beautiful, safe Rust world — with println!, threads, and files — vanishes.
Welcome to the no_std world.
But here’s the twist: Rust doesn’t need libc or even std to function.
Underneath the surface, it’s powered by a surprisingly elegant trio:
core, alloc, and std.
Each of these crates represents a layer of abstraction — a deliberate separation between language logic and platform glue. And this separation is the reason Rust can run on everything from an STM32 microcontroller to a Linux kernel module.
Let’s peel back that onion.
The Core of Everything: core
At the heart of Rust’s universe lies the **core crate — the smallest possible subset of the standard library that requires no OS, no heap, and no libc**.
Think of it as Rust’s spinal cord — it handles the absolute essentials of the language itself.
What’s inside core
Option,ResultIterator,Clone,Copy- Primitive traits (
Add,PartialEq,Ord, etc.) panic!support (without actually printing anything)- Basic pointer types and intrinsics
There’s no heap, no files, and definitely no system calls. Everything is defined purely in terms of what the compiler itself can understand.
Example:
#![no_std]
use core::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
// Won't compile: `println!` is in std
// println!("{}", Point { x: 1, y: 2 });
}
You can define traits, structs, generics — but without a runtime or heap allocator, you’re building in an oxygen-free environment.
And yet, core is enough to build most of Rust’s language guarantees.
That’s the real magic: Rust’s safety model doesn’t depend on the OS.
The Second Layer: alloc
Once you have the bare language working, the next logical question is:
“Where do I get my heap from?”
Enter the **alloc crate**.
It introduces heap-based types like:
Box<T>Vec<T>StringRc,Arc
But here’s the catch:
alloc doesn’t know how to allocate memory — it just defines what an allocator should look like.
That’s where you step in.
Example: Providing a custom allocator
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
struct DummyAllocator;
unsafe impl GlobalAlloc for DummyAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
0x1000 as *mut u8 // pretend this is valid memory
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {}
}
#[global_allocator]
static GLOBAL: DummyAllocator = DummyAllocator;
fn test_alloc() {
let v = Vec::<u8>::with_capacity(10);
}
This works on bare metal because you define your own heap — maybe a static memory pool, a buddy allocator, or a fixed-size arena.
The alloc crate simply plugs into whatever allocator you provide.
Think of it as:
core = foundation
alloc = the heap layer
std = the world outside
The Final Layer: std — and the Fall of libc
Now comes std, the standard library we all know — and its infamous dependency: **libc**.
Traditionally, libc acted as the glue between language runtimes and OS kernels. It provides syscalls like:
malloc,freeopen,read,writepthread_create, etc.
But std is just another wrapper layer — one that adds:
- OS abstractions (threads, I/O)
- Error handling around syscalls
- Platform-specific modules (
std::os::unix,std::os::windows)
When you compile with #![no_std], you’re simply skipping this layer.
That’s why core and alloc let Rust escape libc entirely — you no longer need malloc() or printf() from C.
You can write your own allocator, panic handler, and startup code.
It’s a quiet revolution:
“The day libc died wasn’t the end of portability — it was the beginning of control.”
Architecture Diagram
Here’s how it all fits together:
┌───────────────────────────┐
│ std │
│ OS APIs, Files, Threads │
└────────────▲──────────────┘
│
┌────────────┴──────────────┐
│ alloc │
│ Heap types (Vec, Box) │
│ Needs custom allocator │
└────────────▲──────────────┘
│
┌────────────┴──────────────┐
│ core │
│ Traits, Types, Intrinsics│
│ No heap, no OS, no libc │
└───────────────────────────┘
That’s Rust’s true elegance: a modular, stackable standard library.
Code Flow: From core to OS
When your Rust program runs on Linux (with std), this is roughly what happens:
- Your function calls
Vec::new()→ fromalloc. allocasks the global allocator to allocate memory.- The global allocator (in
std) calls**libc::malloc()**. libcmakes a syscall to the kernel.
In a no_std environment, steps 3–4 disappear.
You’re the allocator now.
That’s how Rust quietly detached itself from the C world — layer by layer.
Real Reason Behind the Design
Why did Rust split it up like this? Because portability was non-negotiable.
The Rust team wanted one language that could:
- Run on a Cortex-M microcontroller
- Build an OS kernel
- Power a Linux web server
Without changing syntax or semantics.
So instead of making the standard library monolithic, they made it modular — turning Rust into a language that could scale down to metal or up to the cloud.
This wasn’t an accident. It was architecture-level foresight.
The Emotional Bit
The first time I wrote a #![no_std] kernel in Rust, it felt like stepping into a post-apocalyptic wasteland.
No println!, no heap, no panic messages.
Just me, a bootloader, and a blinking LED.
But after a few weeks, something clicked.
Rust didn’t abandon me — it just stripped away the training wheels.
Underneath the convenience of std lies a design so clean it can live anywhere — from an Arduino to a space probe.
And that’s the real story:
Rust didn’t kill libc out of rebellion. It did it out of necessity — to reclaim control from decades of OS-level assumptions.
Key Takeaways
core= Rust’s foundation. No heap, no OS, no libc.alloc= Adds heap-based types, needs a custom allocator.std= Adds OS-level features using libc (if available).- You can mix and match them for your target environment.
In short:
The day libc died wasn’t a funeral. It was Rust’s declaration of independence — a reminder that true safety comes not from C’s conventions, but from clarity of design.
메타데이터
- post_id
- 23d10deeb2c3
- slug
- the-day-libc-died-how-rusts-core-and-alloc-crates-work-together-23d10deeb2c3
- url
- https://medium.com/@theopinionatedev/the-day-libc-died-how-rusts-core-and-alloc-crates-work-together-23d10deeb2c3
- canonical_url
- https://medium.com/@theopinionatedev/the-day-libc-died-how-rusts-core-and-alloc-crates-work-together-23d10deeb2c3
- author_url
- https://medium.com/@theopinionatedev
- status
- ok
- fetched_at
- 2026-06-25 07:00:49