1. Rust: lscpu Implementation

Building mlscpu — Systems Introspection

This post is about building mlscpu, a small Rust CLI that provides lscpu‑style CPU introspection on macOS. It assumes basic Rust familiarity, but focuses less on syntax and more on how Rust shapes design decisions when you are forced to confront the limits of your platform.


Why mlscpu exists

On Linux, lscpu is deceptively good.

It answers hard questions — topology, cache hierarchy, virtualization, feature flags — in a single, structured output. It doesn’t guess. It doesn’t invent values. If something isn’t known, it says so.

On macOS, the equivalent information exists, but only in fragments:

  • sysctl exposes low‑level kernel values
  • uname tells you architecture
  • system_profiler provides high‑level summaries
  • Some values exist only on Intel Macs
  • Others silently disappear on Apple Silicon

There is no single command that gives you a coherent picture.

mlscpu exists to assemble only what the OS actually knows, and present it honestly — even when the answer is “N/A”.

That constraint turns out to be an excellent Rust exercise.


The first design rule: never lie

Early versions of mlscpu made the classic mistake:

  • Call sysctl
  • Parse the output
  • Print a number
  • Assume it means something

On Apple Silicon, this fails immediately.

Fields like:

  • machdep.cpu.vendor
  • machdep.cpu.model
  • machdep.cpu.stepping
  • hw.cpufrequency

either don’t exist or return empty values. Treating those as zero or empty strings produces output that looks valid but is wrong.

In Rust, this naturally pushes you toward Option<T>:

fn sysctl_n(key: &str) -> Option<String> {
    let out = Command::new("sysctl")
        .args(&["-n", key])
        .output()
        .ok()?;

    if !out.status.success() {
        return None;
    }

    let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
    if s.is_empty() { None } else { Some(s) }
}

This function refuses to pretend. If the value doesn’t exist, you must handle that fact.

There is no default value. No sentinel. No silent fallback.

Rust forces the caller to acknowledge absence.


Option as a design tool, not a nuisance

In scripting languages, missing data usually gets handled implicitly:

  • empty string
  • zero
  • null
  • hope

Rust doesn’t allow that kind of ambiguity. Every missing sysctl key becomes an explicit branch:

let vendor = sysctl_n("machdep.cpu.vendor")
    .unwrap_or_else(|| {
        if arch.contains("arm") {
            "Apple".to_string()
        } else {
            "N/A".to_string()
        }
    });

This is not verbosity for its own sake. It’s a forced decision point:

  • Do we know this value?
  • Is there a reasonable fallback?
  • Or should we admit we don’t know?

For systems tooling, that discipline matters more than elegance.


Formatting as a systems problem

At first glance, printing aligned output looks trivial.

It isn’t.

lscpu output has rules:

  • Keys align vertically
  • Nested fields are indented
  • Long values (like CPU flags) wrap cleanly
  • Wrapped lines align under the value column, not the key

This quickly turns into a formatting engine, not just println!.

In mlscpu, formatting is explicit and boring on purpose:

fn print_kv(indent: usize, key: &str, value: &str) {
    let pad = " ".repeat(indent);
    let k = format!("{key}:");
    println!("{pad}{k:<width$}{value}", width = KEY_WIDTH - indent);
}

Nothing clever. No macros. No dynamic width guessing.

Rust makes you encode the layout rules directly. That clarity pays off when output grows more complex.


Wrapping without losing alignment

Flags are the hardest part of lscpu‑style output. They are:

  • Long
  • Space‑separated
  • Required to wrap
  • Required to stay aligned

Instead of relying on terminal width or third‑party crates, mlscpu implements a simple word‑wrapping function that respects alignment:

wrap_aligned(&flags, &first_prefix, &cont_prefix, WRAP_WIDTH);

The key insight: formatting is data transformation. Treat it as such.

Rust’s string handling is strict enough to make this safe, but flexible enough to stay readable.


Apple Silicon: confronting platform reality

Apple Silicon exposes a hard truth:

Not all systems expose the same kind of CPU information.

Linux assumes:

  • NUMA
  • SMT
  • Vulnerability reporting
  • Fine‑grained cache topology

macOS does not.

Rather than emulating Linux output poorly, mlscpu does this instead:

NUMA:
  NUMA node(s):          N/A

Vulnerabilities:
  Status:                N/A (not reported like Linux lscpu)

This is a philosophical choice as much as a technical one.

Rust’s type system encourages this honesty. If the OS doesn’t provide the data, the program shouldn’t fabricate it.


Why Rust fits this problem well

This project could have been written in shell, Python, or Go.

Rust adds value in three specific ways:

1. Failure is explicit

Every missing sysctl, every parse failure, every unsupported feature is modeled in the type system.

You cannot “forget” to handle it.

2. Resource handling is boring

Process execution, string ownership, and lifetimes are handled predictably. There are no leaks, no hidden globals, no shared mutable state.

3. Refactoring stays safe

As output grows more structured, the compiler guards invariants. Adding a new section or field doesn’t silently break formatting elsewhere.

This matters when tools live longer than their original author expects.


What mlscpu is not trying to do

It is not:

  • A perfect clone of Linux lscpu
  • A benchmark tool
  • A hardware detection oracle

It is a reporting tool. It reports what macOS can actually tell you, no more and no less.

Rust’s insistence on correctness aligns naturally with that goal.


Lessons for Rust learners (especially SREs)

This project reinforces a few non‑obvious Rust lessons:

  • Option is a modeling tool, not boilerplate
  • Formatting is logic, not presentation
  • Being explicit about absence leads to better tools
  • Systems programming is mostly about constraints, not cleverness

Rust doesn’t make systems programming easy.

It makes it honest.


Closing thoughts

mlscpu is a small tool. Intentionally so.

But it exercises the same muscles required for larger systems:

  • dealing with partial truth
  • modeling uncertainty
  • refusing to guess
  • encoding rules directly instead of relying on convention

If Rust feels strict, this is why.

Reality is strict. Rust just refuses to pretend otherwise.


Source code available at:
https://github.com/silentoxygen/mlscpu

profile picture

SilentOxygen

Learning stuff.