Photo by Zsolt Palatinus on Unsplash

Five Reasons I Love Rust

A Language That Addresses Pain Points of Modern Development

Herbert Wolverson
8 min readJan 19, 2022

--

📚 Connect with us. Want to hear what’s new at The Pragmatic Bookshelf? Sign up for our newsletter. You’ll be the first to know about author speaking engagements, books in beta, new books in print, and promo codes that give you discounts of up to 40 percent.

I began my Rust journey after spending years writing C++. After finishing a particularly difficult project, I went looking for a solution to that project’s pain points. I’d heard about Rust, and tried using it to make some simple games. Several open source projects, a tutorial and two books later — I haven’t looked back.

Rust is built by developers, for developers — and works hard to provide both a consistent, useful systems language and a development ecosystem that addresses the pain points of modern development.

One: Dependency Management

C and C++ have many solutions for managing dependencies — Vcpkg, Conan, Hunter, and so on. All have their perks — but mixing and matching between them becomes a chore. In particular, sharing a project with others can require that recipients be willing to take the time to set up your favorite package manager — or manually install dependencies.

Rust’s Crates system provides a unified package management system across platforms. You can add dependencies as lines in your project’s Cargo.toml file — and the system does the rest:

[dependencies]
wgpu = "0.12"

This line downloads wgpu, a full-featured graphics system that supports DirectX, Vulkan, Metal, and WebGPU. It works on Mac, Windows, Linux, and other platforms.

A few other things that can make Rust dependency management great:

  • Dependencies are statically linked — no DLL or library packages to ship/install with your project.
  • Crates (packages) can compile C/C++ libraries and make them available to your Rust program.
  • Semantic versioning is enforced, and you can easily pin your dependencies — and not worry about an update breaking your code.
  • Your dependencies don’t have to come from the Rust Crates ecosystem. Cargo integrates with filesystem paths, git repositories, vendored dependency libraries, and more.

Rust makes it easy to stop re-inventing the wheel and benefit from premade packages in your projects. Rust also makes it easy to keep your dependencies up-to-date.

Two: Easy and Consistent Build Tooling

Another common complaint among C++ developers is the lack of a standardized build system. Many projects use CMake — which is very powerful, and quite intimidating.

Other projects use autotools, make, ninja, msbuild, meson, and so on. C++ has a plethora of choices, and everyone has their favorite. The C++ ecosystem works but can make switching platforms or collaborating with other developers painful.

Rust’s Cargo provides a powerful, consistent build tool:

  • cargo new lets you create new application or library projects. It even defaults to setting up a git repository with a .gitignore file ready to use.
  • cargo build compiles your project. You can specify --release to enable optimizations.
  • cargo run runs your application. Again, --release enables optimizations.
  • cargo check performs a quick build, finding errors.

Cargo’s build system revolves around a Cargo.toml manifest for your project. This is the same file that handles dependencies — the build system is highly integrated. Here’s an example that includes the complete Bevy game engine:

[package]
name = "bevy_mesh_example"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy = "0.5"

Despite its simplicity, Cargo builds can be very finely controlled. You can gate dependencies behind feature flags, customize your build based on target platform, and patch dependencies to use source from Github or other repositories. Here’s an example of a dependency that compiles only if the target platform isn’t Web Assembly:

[target.'cfg(not(any(target_arch = "wasm32")))'.dependencies]
glutin = {version = "0.27.0", optional = true }

Coming from CMake, Cargo was a breath of fresh air. It’s easy to get started — and the features you need to ship a complex, cross-platform library are included out of the box. Cargo can even handle cross-compilation to other architectures.

Three: Safety First

Rust turns several classes of C/C++ bugs into compilation errors:

  • Rust doesn’t have null pointers. You can wrap values in Option types if they may or may not exist, but if a variable promises to point somewhere — it will. Say goodbye to null-pointer exceptions/crashes.
  • Rust tracks ownership of variables. Use after move bugs are impossible in safe Rust. Dangling pointers — where you’ve handed out a reference to an object and later deleted it — are also compilation errors.
  • Rust protects against data races by enforcing synchronization across threads.
  • Rust enforces RAII (Resource Acquisition is Initialization) automatically. A structure that creates data will free it when it is destroyed — unless you explicitly say otherwise. Memory leaks aren’t impossible (there are even commands for creating them, should you need to release memory/resources without deallocating them) — but the default is safety.
  • Rust can be used to create potentially-dangerous code. This possibility is inevitable: you may need to interact directly with some hardware, may need to work with APIs from a less-safe language, or may need to use a mechanism that isn’t provably safe. Rust code can avoid many of the safety checks when wrapped in unsafe{ .. } tags. This tag is a good thing: you aren’t sacrificing potential usability, but you can isolate the potentially risky code to self-contained — and well-labeled — areas of code.

Coming from C++, I spent my first week with Rust cursing the borrow checker — the mechanism that enforces many of these safety features. After getting used to arranging my code to be borrow-checker friendly — I realized that the code was often clearer for it. Now, I instinctively find myself writing C++ with Rust idioms. Learning Rust has made my C++ safer!

Four: Fearless Concurrency

Rust promises fearless concurrency, promising to help you unleash the power of your CPU. Part of this capability comes from data-race protection: Rust enforces synchronization (and provides great synchronization primitives to use), making the most common threading bugs into compiler errors. For example, multiple threads that write to a shared variable are possible in both C++ and Rust. Rust will fail to compile if you don’t protect the shared object with an appropriate locking mechanism.

Rust also provides a great ecosystem for concurrent programming:

  • The Rayon crate provides a work-stealing thread pool with support for job-based scheduling. Rayon offers great support for parallel iterators that can turn normal iterator-based code into multi-threaded performance monsters.
  • Tokio and Async-Std provide very high-performance promise/future based asynchronous support for powerful servers.
  • Many game libraries, such as Legion and Bevy, integrate Rust’s first-class multi-threading into their Entity-Component Systems architecture. I wrote my first Rust game without realizing that I’d enabled multi-threading support!

🔮 Rust concurrency is a large topic — I’ll publish an article examining Rust’s fearless concurrency claims in the near future.

Five: First-Class Iterators

Iterators are Rust’s great, unsung hero.

Similar to LINQ in C#, or the new C++ Ranges, iterators make it easy to ingest large amount of data — filter, map, and process it — and output it in the form you need.

Unlike iterators in some other languages, Rust’s iterators are deeply embedded into the language. The following code snippets do the same thing:

// Use a for loop
for i in my_collection.iter() { .. }
// Use for_each
my_collection.iter().for_each(|i| .. }

Add Rayon to the mix, and you can automatically add parallel processing (and benefit from the compiler telling you if you forgot to lock a shared variable):

my_collection.par_iter().for_each(|i| .. )

Iterators are designed to be chained together. For example:

let bug_summaries : Vec<BugSummary> = all_bug_reports.iter()
.filter(|i| i.type == BugReport)
.map(|i| BugSummary(i))
.collect();

There are a lot of iterator functions available out of the box — enough to handle the majority of processing needs. Count, sum, map, fold, zip iterators (combining two data streams), reverse iterators —they’re all there. If you need more, Rust makes it easy to create your own iterators.

Wrap-Up

Rust is a very productive language. The Rust ecosystem makes it easy to work with dependencies and consistently build projects across platforms. The language protects you from the most common types of memory bugs and makes it easy to unleash the full power of your multi-core CPU. Iterators make crunching large amounts of data a breeze. Rust deserves StackOverflow users’ votes for “Most Loved” language.

6️⃣ Bonus perk six: the Rust community — they are a helpful, welcoming bunch. I was overwhelmed by their willingness to assist me while I made my first steps into Rust-land.

📣 Add your voice to the story. Tell everyone what you love about Rust in the comments.

--

--