JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Cargo and How Rust Maps onto Your C Knowledge

You have met ownership, the borrow checker, and Option and Result. This last guide hands you the everyday tool that ties a Rust project together — Cargo — and then lays your hard-won C knowledge side by side with Rust, so the new language feels less like a foreign country and more like the same city with better road signs.

From hand-written Makefile to one tool

Two rungs ago you learned what it really takes to turn C source into a running program: a preprocessor, a compiler, an assembler, and a linker, orchestrated by a Makefile that spells out which file becomes which object file, in what order, with which flags, and how they link. And the moment you wanted someone else's library, the chore got worse: hunt down the source or a system package, install its headers and binaries, then tell the compiler an include path and the linker a library path so it could actually find them. It works — generations of software were built exactly this way — but every line of that build description is yours to write and maintain.

Rust folds that whole pipeline behind a single tool: Cargo. Cargo is the compiler driver, the build system, and the package manager rolled into one. The deep difference from Make is who describes the build. With Make you write down how to build — the rules, the order, the recipes. With Cargo you almost never describe how; Cargo already knows the conventions (your source lives in src/, the output lands in target/), works out the build graph and incremental rebuilds itself, and asks you for just one thing in return: a plain declaration of what your project is and what it depends on.

This is the same shift you saw when Make replaced typing gcc commands by hand, taken one step further. Make is declarative about files and rules; Cargo is declarative about packages and dependencies. You still get incremental rebuilds and a build graph underneath — Cargo just fills in the recipes you would otherwise have hand-written, using fixed conventions instead of asking you to invent them per project.

Cargo.toml, crates, and crates.io

Two words anchor the whole system. A crate is Rust's unit of compilation and sharing — roughly, one library or one program; it is the Rust analogue of a translation unit grown up into a whole reusable component. A package is a bundle of one or more crates, described by a small text file named Cargo.toml. That file lists the package's name, its version, and — this is the part that changes your life — its dependencies, each named with the version you want. Where C left dependency tracking to your memory and your filesystem, Rust writes it down in one file the tool reads.

A whole small project's build is just a few lines. Under a [package] header you write name = "hello", version = "0.1.0", and edition = "2021"; under a [dependencies] header you write rand = "0.8". That single dependency line replaces the Makefile plus the manual hunt for a library's headers and link flags. The handful of commands you will actually type are short and predictable: cargo new starts a fresh project with the right layout, cargo build compiles it, cargo run builds and runs it, and cargo test runs its tests.

And crates.io is the public registry — the shared warehouse of open-source crates the community has published. To pull one in, you add a line under [dependencies] and run cargo build; Cargo resolves the version, downloads that crate and the entire tree of crates it depends on, compiles the lot, and links them. The painful C patchwork of package managers, system paths, and version mismatches collapses into one declarative file and one command.

The translation table: your C model, relabelled

Here is the reassuring news to end this rung on. Learning C was not a detour — it built exactly the mental model Rust assumes. Rust is a systems language running on the same machine, so the hardware-level picture you now carry transfers almost one-for-one. What changes is not the hardware; it is who is responsible for getting the details right. Laying the pieces side by side is the fastest way to see what is genuinely new and what is just a new name for something you already understand.

  C                                  Rust
  ---------------------------------  -----------------------------------
  malloc() / free()                  Box<T>, String, Vec<T> / drop
  char *p   (might be NULL)           Option<T>     (no null in safe Rust)
  returns -1, sets errno             returns Result<T, E>
  pointer + length you track         &[T]   (a slice carries its length)
  you remember to call free()        drop runs at the end of the scope
  C pointer  (no rules enforced)     &T / &mut T  (always valid memory)
A rough map, not an equation. Each left-hand idea you already own; the right-hand column is mostly the same machine concept with the bookkeeping moved from your head into the compiler.

Walk the map row by row, because each row is a guide you have already climbed. The stack and the heap are the same two regions of process memory in both languages — locals on the stack, longer-lived data on the heap. In C you reach the heap with malloc() and reclaim it with free(); in Rust an owning type like Box<T>, String, or Vec<T> allocates for you and releases it automatically through drop, so you never write free at all and the leak of a forgotten free becomes much harder to commit. A C pointer becomes a Rust reference: &T is the shared read-only borrow, &mut T the exclusive writable one you met in the borrowing guide — and unlike a raw pointer, the borrow checker guarantees a reference always points at valid, still-living memory.

The last three rows are the previous two guides in disguise. Where C uses a possibly-NULL pointer to mean maybe nothing, Rust uses Option<T>, so the forgotten NULL check turns into a compile error. Where a C function returns -1 and stashes the real error in errno, a Rust function returns Result<T, E> and the error rides home inside the return value, with the ? operator propagating it cleanly. And where C describes a buffer as a bare pointer plus a length you must keep in sync by hand — the setup behind every buffer overrun — Rust uses a slice, &[T], a single bounds-checked value that carries both where the data starts and how long it is.

What is genuinely new, and what is not

The map is a guide, not an equation, and honesty about its limits is what keeps it useful. Box<T> is not literally a malloc() call, a reference is not literally a C pointer (it carries borrowing rules a raw pointer never had), and Rust's move semantics and the exact order in which values drop have details with no C equivalent at all. Use the table to get oriented on day one; then expect to learn, case by case, the places where Rust genuinely diverges rather than merely renames.

Step back and name the one thing that actually moved. The hardware reality is identical between the two languages — bytes, addresses, the stack growing down, the heap, the cost of a cache miss, the ABI when you call into a C library. So all of your low-level intuition is still good; none of it was wasted. What Rust adds is a compiler that tracks ownership, borrowing, and lifetimes, so the bookkeeping you used to do by hand and by comment in C — who frees this, who may write to that, how long this pointer stays valid — is now checked for you, before the program ever runs. That single relocation, from your head to the compiler, is the whole of what changed.

Where this leaves you

Hold on to the honest framing this whole rung was built on. The thing you can correctly say after these five guides is sharp and limited: in its safe subset, Rust eliminates a specific, costly family of bugs at compile time — use-after-free, double-free, dangling references, buffer overruns, and data races. Those are not minor; they are the source of a large share of the security vulnerabilities in real C and C++ codebases. Removing them by construction, before the program runs and with no garbage collector pausing it, is a genuine and important win. That is the promise from the very first guide in this rung, now made concrete.

But Rust is not a silver bullet, and selling it as one does the language a disservice. The borrow checker does nothing about a logic error, an off-by-one in your arithmetic, a deadlock between two threads that each wait on the other, or a memory leak you create on purpose. It costs a real learning curve — the checker will reject correct-looking code, and you will, for a while, feel like you are fighting it. And the unsafe escape hatch still exists for talking to hardware and C, where the compiler's guarantees stop and yours begin, with undefined behaviour waiting on the other side exactly as in C. Rust is a different set of tradeoffs, not the end of all bugs.

And that is exactly why your C is the right foundation for it. Everything that makes Rust feel safe — knowing what a dangling pointer is, why a double-free corrupts the heap, what a data race does to shared state, how a heap allocation without a matching free() leaks — you learned by meeting those failures honestly on the rungs below. Rust did not invent the ownership discipline; it encoded the discipline good C programmers already kept in their heads and their comments, then made the compiler enforce it for everyone. You climbed this on-ramp not by forgetting C, but by carrying it with you. The road signs are clearer now — and you already know how to read the road.