← Back to list

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.

TheOpinionatedDev · 2025-10-10 16:48 · 39 claps · 4.2 min read paywalled
#libc #died #rust-programming-language #alloc-crates #programming
Open on Medium ↗
Wiki topics: 3D · Motion & 3D Design 💻 · Programming

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, Result
  • Iterator, 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>
  • String
  • Rc, 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, free
  • open, read, write
  • pthread_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:

  1. Your function calls Vec::new() → from alloc.
  2. alloc asks the global allocator to allocate memory.
  3. The global allocator (in std) calls **libc::malloc()**.
  4. libc makes 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