Back to JetBrains

Everything You Wanted to Ask About Rust

JetBrainsDecember 18, 20251h 4m
In a Nutshell

Rust expert Herbert Wolverson explains that Rust eliminates entire classes of bugs at compile time through its ownership and borrowing system—preventing data races, use-after-free errors, and memory leaks that C++ and Java can't catch until runtime. The language delivers C-level performance with zero-cost abstractions, meaning high-level features like iterators and traits add no runtime overhead. Beginners should start building real projects immediately rather than mastering memory concepts first, then gradually adopt patterns like cloning liberally during learning before optimizing with lifetimes and smart pointers later.

AI-Generated Notes

These notes were generated by AI and may contain inaccuracies.

Herbert Wolverson works as the Rust specialist at Arden Labs, collaborates with PragProg as publisher of the Rust expert, and serves as lead developer of LibreQoS, an open source project providing traffic shaping for ISPs worldwide.

The recently published book Advanced Hands-On Rust serves as a follow-up to the original Hands-On Rust. Combined, both books span approximately the size of a Droid Turbo. The author felt the first book ended too early for beginners transitioning to intermediate level, and this follow-up covers everything from beginner through intermediate concepts.

The book is available on Amazon, Barnes & Noble, Waterstones, and through the publisher's website where ebooks are available at a lower price.

Memory Management Differences

Java and C# operate as managed languages running inside the JVM and .NET runtime respectively. Objects are created with new and garbage collection automatically handles deletion. This creates uncertainty about when objects will be removed, which poses problems for high-performance applications like automotive braking systems where timing is critical.

C++ and Rust require explicit consideration of stack versus heap memory allocation. The stack typically provides 4-8 megabytes of fast memory close to CPU cache sizes, while the heap offers unlimited memory capacity. Both languages require decisions about where to place data for optimal performance.

RAII Pattern and Resource Management

The RAII (Resource Acquisition Is Initialization) pattern ensures resources are automatically cleaned up when objects go out of scope. In C++ and Rust, this eliminates the need for manual memory management in most cases. Rust implements this through a default drop trait that cleans up resources automatically when variables go out of scope.

Smart Pointers and Memory Safety

Modern C++ uses smart pointers with make_unique to manage heap allocation. Rust provides equivalent functionality through Box and unique pointers. Both approaches ensure memory is automatically freed when the pointer goes out of scope, eliminating memory leaks.

Concurrency and Data Race Prevention

A C++ program creating three threads that each count to 10,000 and add to a shared counter will produce different results on every run due to data races. C++, Java, C#, and Python all compile and run this code without warnings. Rust prevents compilation of such code, forcing developers to implement protection through atomics or mutexes.

Uber discovered 10,000 to 20,000 data race conditions in their production codebase using a data race detector, some of which could affect ride pricing. Rust's approach to concurrency eliminates these issues at compile time.

Performance Characteristics

Rust achieves performance equivalent to C with nearly identical generated code. While the JVM performs well in some scenarios, Rust typically uses significantly less memory than Java applications.

Senior Rust developers in California can earn $400,000 annually, though this is not achievable for junior developers. The high cost of living in California reduces the effective purchasing power of such salaries.

Rust's syntax 12 years ago required learning complex glyphs and symbols for memory management. Current Rust code resembles a 12-line web server using the Axum framework that handles thousands of requests per second while using 4-6 megabytes of RAM.

The learning approach focuses on building useful applications immediately without touching memory management concepts initially. This enables learners to encounter problems organically and seek solutions as needed.

Rust can serve as an effective first language without prior C knowledge. The language operates at a higher level than C while remaining in the same performance space. Learning multiple languages provides better perspective on choosing appropriate tools for different problems.

The Linux kernel has officially integrated Rust, ensuring its long-term presence in systems programming. Training demand spans defense aerospace, finance, and automotive industries. Most current Rust developers are transitioning from other languages rather than entering as juniors.

Blockchain companies at the Solana Breakpoint conference extensively use Rust across their projects.

Multiple resources support different learning styles:

  • Hands-On Rust teaches through game development
  • Rust for Rustaceans provides intermediate to advanced content
  • Rust in Action offers practical examples
  • The official Rust book and "Learn Rust by Example" are freely available from the Rust Foundation
  • Iron Labs provides video courses and on-site training

The most effective approach involves selecting a project goal and iteratively solving problems as they arise.

Ownership works like car ownership: when you own something, you control its lifecycle. Borrowing functions like lending a car - you temporarily give access while retaining ultimate ownership. Rust's borrow checker prevents scenarios where one part of a program could destroy data being used elsewhere.

Rust defaults to move semantics rather than copying. When passing values to functions without borrowing syntax, ownership transfers completely. Mutable borrows allow modification of borrowed data, while immutable borrows permit read-only access.

Borrowing and references serve the same fundamental purpose as in other languages, ensuring proper resource cleanup by maintaining clear ownership chains.

The Bevy game engine provides an effective platform for learning Rust through practical game development projects.

The phrase "zero-cost abstractions" originated in C++ and means that having nice features doesn't cost program performance unless you're actively using them. It also implies you can have source-only code that affects the compiler but never makes it into the final program. The borrow checker exemplifies this concept - every time you borrow something, a lifetime is attached and the compiler tracks resource duration and usage correctness, but this tracking never appears in compiled binaries.

Rust implements this through zero-sized types and phantom data. These types enforce logic during compilation but never exist in the final program. Type states make certain states unrepresentable, ensuring only sensible operations are possible, all without runtime cost.

The canonical example involves loops versus iterators. Whether you write a for loop or use the functional approach with iterators, filter, and sum, Rust guarantees no performance penalty for the functional approach. Sometimes the functional version performs slightly faster, but it's guaranteed not to be slower.

Traits also benefit from zero-cost abstractions through monomorphization. Without the dynamic keyword, Rust avoids expensive branching code from dynamic vtables, allowing nice features without slowing programs down.

When coming from functional programming backgrounds like Haskell, developers often wonder whether to use iterators whenever possible. The recommendation is to use whichever approach feels most comfortable. An older C programmer once called iterator syntax "horrible" and "unreadable," prompting the presenter to write the equivalent for loop instead.

However, some tools like Rayon force the iterator approach because they allow replacing iter with par_iter to automatically parallelize loops when possible.

Rust works well in multi-language polyglot programming where it takes on roles when computation and memory usage become critical. The right tool should be chosen for each task - Go, Java, and C# work well for quickly writing web servers, while Rust produces smaller, faster programs but requires more upfront effort and longer compile times.

Languages can be mixed effectively. One example involved linking LanceDB, a Rust vector database, into a C# system through a small FFI library.

Hands-on Rust and Advanced Hands-on Rust build on each other. Hands-on Rust starts with hello world, while Advanced Hands-on Rust immediately covers making crates and libraries, introducing generics, macros, and intermediate to early advanced topics. For beginners, Hands-on Rust serves as the better starting point. A second edition is currently being discussed because some issues mentioned in the first edition have since been fixed.

When developing real projects, synchronous Rust should be learned first before tackling async code. The Tokio crate appears frequently in crates.io downloads, indicating most applications use it for asynchronous code.

Async functions transform entire functions into state machines with pinning happening behind the scenes. Async Rust complications primarily involve references, so developers should become comfortable with Arc and avoid typing references when working with async code.

Rust lacks throwing and catching exceptions, which surprises developers coming from Java and C++. In old C, error codes were integers - negative numbers indicated errors, but these were easy to forget checking and provided little information.

C++ introduced exceptions with complete stack traces and unwinding, but many industries now compile with no-except flags due to memory overhead. Java made exception checking mandatory, leading to patterns where main functions wrap everything in try-catch blocks.

Rust uses Result types to force acknowledgment that something might go wrong. Results must be handled through unwrap, match, or if-let constructs. The panic system offers some unwinding capability that can be disabled. A proposal currently exists to add a throw keyword to Rust, though it likely won't implement traditional exceptions.

Option represents nullable values where it's acceptable for something not to exist. Looking up a user by name and finding no match isn't an error - you simply want to know the user doesn't exist because you might want to create them later.

Result carries error information when something actually went wrong. Database lookups need to distinguish between "database server is on fire" and "record doesn't exist." It's valid to have Result<Option<User>> when both concepts apply.

When teaching, the colored crate serves as an excellent first introduction because it's simple and visually engaging for classroom demonstrations. For practical use, Serde (serialization/deserialization) ranks as the most important crate to learn first.

Serde allows tagging structs with derive macros for serialize and deserialize, then specifying formats like JSON or YAML. In one line, almost everything can be serialized or deserialized, including standard containers like HashSets, HashMaps, and Vectors.

Structs function like records or classes in other languages - data grouped together in memory with optional methods attached. Enums represent sets of possible things, and unlike enumerations in many languages, Rust enums can attach data to each variant, creating tagged unions.

Traits work like interfaces in other languages, specifying functions that types must offer when the trait is implemented. Common traits include Debug (implemented almost everywhere with a derive macro) and Display. Traits allow specifying behavior for data stores or other abstractions.

Pattern matching proves essential in Rust because so much of the language is type-based. With Result and Option types containing values, pattern matching extracts data cleanly without deep nesting of if statements.

Match expressions are exhaustive - adding a new enum variant triggers compiler warnings for all existing matches that don't handle it. One-arm matches like if-let and let-else provide shorthand for single-pattern matching.

Early Rust required explicit lifetime labels everywhere, but the compiler now infers lifetimes often enough that needing to write lifetime annotations may indicate overly complicated code. Lifetimes represent how long allocated values remain valid until their last use or explicit deletion.

The compiler maps lifetimes for every allocation to prevent use-after-free and unsafe usage. This checking contributes to slower compilation compared to other languages. Developers coming from C and C++ understand lifetimes implicitly through prior experience, while those from managed languages face steeper learning curves.

Visualization tools help significantly - JetBrains RustRover includes a lifetime visualizer, and drawing pictures to represent lifetimes aids understanding.

Rust was named after a fungus. Like all languages, it will eventually accumulate legacy issues, though its different system makes removing outdated features easier than languages stuck with 25-year-old bugs.

Box and Rc/Arc are owning pointers. Box works well when values are too large for the stack. Rc and Arc provide reference counting garbage collection - when no references remain, the value gets destroyed.

References pass values to functions for examination without transferring ownership. References and ownership operate orthogonally - references examine values while Box, stack allocation, or Rc/Arc handle ownership concerns.

Rust's syntax derives heavily from the ML family, particularly OCaml, making it a hybrid language supporting imperative, functional, and mixed programming styles.

Functional languages serve as laboratories for new language features that migrate to mainstream languages when proven effective. Lambda functions and pattern matching exemplify this pattern - both originated in functional languages and now appear across programming paradigms.

Clone performs deep copies of entire structures, including contents of vectors and other containers. Copy is poorly named - it actually means values can be moved while remaining valid, typically applying only to primitives handled by CPU registers.

When starting with Rust, developers should ignore Copy entirely as it causes confusion. Optimization work later may warrant examining Copy traits.

The terminology creates confusion because types like Arc and database connection pools implement Clone without performing deep copies - cloning Arc increments reference counters, and cloning connection pools increments pool references. A movement exists to introduce new terminology, possibly involving the word "handle," to distinguish these operations from expensive deep copies.

Adding clone everywhere until it works is a hallmark of learning Rust. Not something to be ashamed of. In the beginning, just don't think about it. Make it simpler. When you know what you are doing, when you are doing proper profiling, when you know that this clone is actually a bad thing, then you will be able to remove it.

Bjarne Stroustrup, C++ creator, once pointed out that Rust and C++ are the same language, we just have the opposite defaults. You run into C++ programmers who type const everywhere. You run into Rust programmers who let mut everywhere.

The preference for immutability makes it possible to bind a variable and keep that binding, or replace it with something even using the same variable name. This approach forces seeing all intermediate steps, which makes debugging easier.

The big thing is it stops shooting yourself in the foot by having a really big function and forgetting having an X that you've used throughout the function, and somewhere changing it, doing something stupid to it, and the function no longer works. If you need to start writing with let mut everything and just to get it to work, do so. The compiler will even tell you where to remove it.

Put similar things together. Use the module boundary either widget.rs or mod.rs as your index that links to all of the parts of that module, and exposes publicly what you want the API to be. Write defensively, stop people from reaching into your internals, changing things, force them to do things your way.

There are books on this topic. This is one that you really have to Google. There is an 8-hour class on this topic available.

The first thing you see when doing cargo new hello is println!, which is a macro. It has an exclamation mark because it's surprising. Hello world cannot be expressed in the standard Rust syntax, so a macro had to be used in the example.

There are two types of macros, with a third one coming because macros 2.0 is on its way. Logical macros are weird out syntax that looks like Lisp, allowing you to easily do something repeatedly, combine a bunch of operations together. Procedural macros read the AST, the compiled tokens of your program in the compile process, and let you change it.

When you see things like derive serialize, it's actually spitting out all new code on your behalf. You really want to be using either cargo expand or rust-analyzer's expand feature to find out what it's doing because otherwise you're shipping code and you're trusting that somebody else wrote that.

Macros are sometimes kind of terrifying. They were used extensively in the Advanced Rust book, and got some interesting email about it. One macro was there purely because Bevy changed its syntax for managing scheduling three times in the time it took to write the book. A macro was written and the back end changed so it didn't have to be changed five times in the book.

You can make macros look like anything you want. There was a terrifying demo at Rust Conf Albuquerque where somebody made XML compilable with macros. It's one of those things you can do, but don't.

Macros are a very big problem for IDEs. There was a talk by Lucas Virth at EuroRust about difficulties related to implementing macro support in IDEs and Rust analyzer. The Rust Rover team has a lot of issues with macros because for IDE implementers, it's an awful feature. People should be asked not to use them, but of course it's a nice feature of Rust.

The expansion of println! tells you absolutely nothing, because it promptly falls back to another macro. You need to expand to expand and to expand.

This was a live stream covering 19 questions. There is a link in the video description to the repo with all other questions. Reading through all the notes is recommended because there is much more information there.

The live stream mentioned not only Rust, but other functional programming languages including Haskell, Lisp, and OCaml. Polyglot programming was also discussed.

Keep JetBrains in your library

Save the videos and channels worth coming back to, and find them again in one place.