Rust, a multi-paradigm, high-level, general-purpose programming language computer programming language, has become increasingly popular in recent years. According to the annual developer survey report of Stack Overflow, Rust has been ranked as the "most beloved programming language" for multiple years.
Several significant corporations, including Google, Microsoft, and Tencent, have adopted Rust into their diverse projects. In spite of its popularity, many junior programmers and students still find it challenging to grasp. Our team has dedicated itself to creating an open-source database written in Rust. We aspire to assist more individuals in understanding the value of this effective and secure programming language. This article aims to delve into the origins and advantages of Rust and provide valuable resources for those interested in learning more about this programming language.
What is Rust Language
Origin
Rust was originally developed by Graydon Hoare, an employee of Mozilla, and made its debut in 2010. With the help of Mozilla, the Rust development team continuously improved the design and implementation of the language until the official release of Rust 1.0 in 2015. Rust's design is inspired by a variety of programming languages, such as C++, C#, Haskell, and Erlang, and has some unique features that are absent in other languages. As a modern systems programming language, its goal is to provide memory safety, high performance, and reliability.
Rust Syntax
Rust's syntax is similar to C++, incorporating some features of modern programming languages such as pattern matching, closures, and iterators, while also inheriting C++'s syntax and low-level systems programming capabilities. In order to address memory safety issues and concurrency problems present in C++, Rust introduces a strict ownership model and type system that can detect memory errors and data race issues during compilation, ensuring the safety and stability of the program.
Advantage of Rust
High Performance
The high performance of Rust refers to its ability to provide performance close to that of C++ while maintaining memory safety and a rich set of language features. Rust's high performance mainly benefits from its optimization in memory allocation, multi-threading processing, and zero-cost abstraction.
If we compare Rust, C++, and Golang as three athletes participating in a running race, then C++ is undoubtedly an experienced contestant with high speed and flexibility. However, C++ needs to take care of many things, such as "adjusting breathing and heart rate" (memory management) by itself, which makes it prone to mistakes in the race, resulting in slowdowns or falls (memory leaks, dangling pointers, etc.). The Golang athlete is a new and promising contestant, with the advantage of having an intelligent coach (garbage collector) adjusting his breathing and heart rate during the race. Although helpful in preventing errors, consistent communication with the coach may impede his speed.
As a well-rounded athlete, Rust combines the speed and agility of a C++ athlete with the strategic planning ability of a Golang athlete (utilizing ownership, borrowing, and lifetime mechanisms). This unique combination allows Rust to maintain high speed while minimizing errors.
The above picture shows the load testing conducted on Discord. It is apparent that when using Golang, there is a load peak every so often due to the garbage collector. However, after switching to Rust implementation, the system runs much smoother overall. Additionally, in the Rust version, the latency, CPU, and memory are all improved.
Reliability
Imagine Rust and other programming languages (such as C++) as two restaurants, how does their reliability reflect in the process of food preparation?
The process of food preparation in the C++ restaurant is relatively free. Chefs can freely adjust ingredients, cooking methods, and processing methods. Although this flexibility may bring a lot of innovation, in this process, food is prone to contamination. Chefs might use a knife for raw meat when slicing sashimi, which may lead to food poisoning. This is similar to C++ dealing with memory and resources, where its flexibility may cause problems such as memory leaks, null pointer dereferences, and data races.
In contrast, the Rust restaurant is more strict and standardized. In the process of making food, the Rust restaurant will strictly supervise the sources of ingredients, the chefs' operations, and cooking methods. For example, it requires chefs to change to a clean knife before slicing sashimi. The strict supervision of the Rust restaurant ensures the quality and safety of the food, reducing problems caused by negligence.
Rust performs memory safety checks through the Borrow Checker during compilation, ensuring strict compliance with the specifications during memory operations, thus improving overall code quality and security.
- Memory Safety
The advantage of Rust's memory safety is that these mechanisms check the correctness of a program's memory management during the compilation stage, preventing programmers from making mistakes such as memory leaks, double frees, and dangling pointers while writing code.
This is like a restaurant manager pre-planning the cooking and cleaning process in advance, ensuring that the restaurant remains hygienic and efficient. Therefore, compared to other programming languages, Rust is better at ensuring memory safety and program stability.
Below shows an incorrect example:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
In the code snippet above, we defined r
in the outer scope. In the inner scope, we defined x
and assigned the reference of x
to r
. Finally, we accessed r
in the outer scope.
When we attempt to compile the above code, the Rust compiler will provide a detailed error message as follows:
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {}", r);
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `safe_memory` (bin "safe_memory") due to previous error
Then why is it wrong?
The error message above is quite straightforward. The variable x
does not "exist long enough". When x
reaches line 7, it leaves the scope and gets dropped. At this point, the outer scope's reference r
becomes a dangling reference, so accessing r
on line 9 will result in an error.
If line 9 is removed, the code will compile successfully. This is because the compiler knows that there is no way to access the dangling reference
r
afterx
has been dropped.
In summary, an important feature of Rust is memory safety. The Rust compiler checks for memory safety issues during compilation and tries to expose these issues at compile time to prevent memory errors from occurring at runtime.
We can see that Rust's memory safety features are mainly reflected in lifetimes and references. Lifetime parameters and reference rules help the compiler check memory safety at compile time, ensuring that there are no dangling pointers or other memory errors at runtime, thus improving the stability and security of the program.
- Concurrency Safety
The concurrency safety of Rust refers to its ability to ensure thread safety in a multi-threaded environment, avoiding issues such as data races and deadlocks.
Rust's concurrency safety mainly relies on its ownership, borrowing mechanism, and lifetimes. When a value is referenced by multiple threads, Rust prevents these threads from simultaneously accessing the mutable reference of this value, thus avoiding data race problems.
use std::sync::{
atomic::{AtomicI32, Ordering},
Arc,
};
fn main() {
let counter = Arc::new(AtomicI32::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = counter.clone();
let handle = std::thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
// the result should be 10*1000=10000
println!("Counter value: {}", counter.load(Ordering::Relaxed));
}
Rust Ecosystem
Though not as established as Java or C++, Rust's ecosystem has been gradually growing in recent years, with an abundance of libraries and frameworks in place. The Rust ecosystem comprises several significant components, including:
- Package Manager: Cargo is the official package manager for Rust, used for building, testing, and publishing Rust applications and libraries.
- Web Development: Actix and Rocket are the most popular web frameworks in Rust, providing the functionality needed to build high-performance, secure web applications.
- Asynchronous Programming: Tokio and async-std are two main libraries in Rust for asynchronous programming, offering features such as asynchronous I/O, timers, and task scheduling.
- Operating System Development: Rust has significant potential in the development of operating systems. Redox is a microkernel operating system written in Rust, representing Rust's strength in system programming.
This is merely a segment of the Rust ecosystem. With the constant expansion of the Rust community, we'll be sure to witness the emergence of more frameworks and libraries.
Rust learning
The following books cover basic to advanced knowledge of Rust and are suitable for readers of all levels.
Beginner:
- The Rust Programming Language
- Programming Rust
Intermediate:
- Rust for Rustaceans
- Rust Atomics and Locks
- The Rust Reference
Advanced:
- The Rustonomicon
Of course, you can also watch videos from Rust experts, such as
- Jon Gjengset, a blogger who explains the Rust language and is also a professor at MIT, and his videos are more advanced and suitable for an audience that already has some basic knowledge of the Rust language.
After getting started, you can further learn and use Rust through some hands-on projects, such as:
- Tokio, an asynchronous runtime for writing high-performance network applications
- Serde, a library for serializing and deserializing Rust data structures.
- Diesel, a Rust ORM (Object-Relational Mapping) library for mapping Rust data structures to relational databases.
These projects can all help you improve your usage of Rust. Of course, you can also choose our GreptimeDB project for practice, which is an open-source time-series database project written in Rust. We provide a series of good first issues suitable for beginners. Find us on GitHub and give it a try.
Conclusion
Prior to the release of its 1.0 version, Rust migrated from a "green" threading model toward a native threading model. This solidified the language's position as a language with no runtime and low abstraction levels. Even after the standard library introduced the asynchronous Future API, this characteristic remained unchanged.
The finer and stronger control that comes with low abstraction levels empowers Rust developers to manage resources (such as threads and memory) more precisely. As a result, Rust is particularly well-suited for developing applications in domains like servers and databases, putting it in direct competition with C++'s traditional stronghold.
However, the flip side of this control is high risk, which has been a longstanding criticism of C++. This is also what prompted Mozilla to explore new languages. Unlike programming languages like Java and Go, which prioritized risk reduction at the expense of control in the past two decades, Rust enforces strict constraints during compilation. With features like its well-known borrow checker and lifetime annotations, Rust ensures both high performance and safety during runtime.
The cost for Rust's balanced performance is a relatively steep learning curve and longer compilation times. In this regard, Rust established a tooling team early on and has continuously worked to mitigate its negative impact with better development tools.