Epilogue the Eternal Basis of Rust Learning How to Continuously Improve

Conclusion: The Basis of Perpetuity—How to Sustain and Advance in Rust Learning? #

Hello, I’m Chen Tian.

First of all, congratulations on completing this course!

At the end of June, I confirmed that I would be launching this Rust column on Geek Time.

To be honest, I wasn’t that keen on such paid courses before because I had been used to doing things spontaneously. I wrote articles for the public freely, writing whenever I wanted and stopping at will. If I got bored with a topic, I could switch to another one with no reason needed. But once you sign a contract for a paid column, it means you can no longer do things as you please in terms of taste, quality, the content, and the pace of updates; you have to adhere to specific requirements.

The most critical aspect was the pace of updates—I had never been a full-time writer before. Like a software developer, starting some pioneering work greatly relies on inspiration, very much dependent on those moments of serendipity. This kind of unstable output mode, when faced with the pressure of progress, can be torturous. So, I’ve turned down many opportunities in the past.

But after some thought, I still took up the challenge of Rust’s first lesson.

The main reason is that my fondness for Rust has grown, and I want more people to fall in love with it too. Therefore, I’ve done quite a bit of output on public accounts and Bilibili, but this was scattered, lacking a complete system. So, when this opportunity came up to build a Rust learning system based on my personal summary, I thought it could be of great help to everyone’s learning.

Another part of it was my selfish desire. Since my book “TuKe Quan Venture Chronicles” was published in 2016, I had not published anything formally. Many topics I had verbally agreed to or even signed contracts were terminated or postponed due to various reasons. I really wanted to know whether I could still pick up the pen and write serious text that could reach wider and last longer.

However—is it possible for a text introducing a programming language to have a lasting life?

You must have this question in mind.

To write about a programming language and hope for it to have a long-lasting life seems like a foolish dream. The pace of evolution of modern programming languages compared to twenty years ago is astonishingly fast. Take Rust, for example, with a stable release every six weeks and a new edition every three years. It’s not just about having a few years of lifespan; even during the months of serialization of a column, it goes through two or three versions, meaning that plenty of fresh features are added to the language.

Fortunately, Rust pays extreme attention to backwards compatibility, which means that the code I’m introducing now, as long as it involves stable content from Rust language or the standard library, should still be effective for years to come (hopefully). Rust’s iterative but backwards-compatible approach gives it some teaching advantages over other languages. Therefore, writing about Rust may have a bit more longevity.

Of course, this is still not enough. The way to make a programming language’s introduction last longer is to start with the basics, helping everyone understand the ideas or mechanisms behind the superficial concepts of the language. This is also the most core design principle of this column.

It’s been almost seventy years since universal computing machines were born, and the von Neumann architecture of the time is still effective. From the birth of the C language to now, it’s been almost fifty years - the method of programming languages dealing with memory is still the heap and the stack, and the commonly used algorithms and data structures are still those. While programming languages are constantly evolving, the main means of solving problems are still roughly the same.

For example, reference counting—if you’ve grasped it in any language you’ve learned before, understanding Rust’s Rc/Arc is within reach. As long as we solidify our fundamental knowledge, many seemingly complicated issues are just the same essence disguised in confusing wraps.

So, how to clear the fog and reach the essence of things? My method has two aspects: first is to ask, and second is to cut. Yes, it’s the last two words of the traditional Chinese medicine approach: “look, smell, ask, and feel the pulse.”

Asking means digging down to the root, posing crucial questions based on existing knowledge, thus opening the door for subsequent exploration (cut). For instance, you know the conventional implementation of reference counting. You also understand Rust’s unique ownership mechanism, which ties the lifecycle of heap memory to stack memory; when the stack exists, so does the value, and when the stack perishes, so does the value.

If you ponder this, you’ll inevitably question: How do Rc/Arc break the unique ownership mechanism to allow heap memory to escape the constraints of stack memory? This question gives you the opportunity to “cut” further.

What does “cut” mean? It means to delve into the source code and trace the context to find the answer to the problem. Beginners often do not look at the standard library’s source code, but in reality, reading source code is the most helpful for your growth. Whether learning a language or studying the Linux kernel or something else, the source code is the primary resource. No matter how well others’ analysis might be, it is chewed-over food; limited by their understanding and expression abilities, this chewed-over meal may not go down as smoothly as if you tried it out yourself.

For instance, to understand the issue with Rc/Arc above, you naturally need to look at the source code implementation of Rc::new:

pub fn new(value: T) -> Rc<T> {
    // There is an implicit weak pointer owned by all the strong
    // pointers, which ensures that the weak destructor never frees
    // the allocation while the strong destructor is running, even
    // if the weak pointer is stored inside the strong one.
    Self::from_inner(
        Box::leak(box RcBox { strong: Cell::new(1), weak: Cell::new(1), value }).into(),
    )
}

Without looking, you might not know; once you look, you’ll be startled. The suspicious Box::leak appears before us. What is this Box::leak for? Following this clue, we discover a valuable gold mine (you can review the section on lifetimes).

Thought #

On top of tracing back to the basics, we must learn the correct methods of analyzing and solving problems. I think learning a programming language should not be limited to syntax itself but should also enhance our abilities to learn knowledge and solve problems during this process.

If you still remember the lecture on HashMap, we first introduced the main strategies for resolving hash collisions, which are the core algorithms for constructing hash tables; then we used transmute to understand the structure of Rust’s HashMap, inspecting the memory layout with gdb, and combining it with the code to find the specific strategy for HashMap construction and expansion.

By peeling layers, exploring, and summarizing, we gained a solid grasp of Rust’s hash tables. This level of mastery will remain with you even if you don’t touch Rust for ten years; ten years later, if someone asks you how Rust’s hash tables work, you will be able to answer correctly.

I hope you can master this method of learning, which is beneficial for a lifetime. In 2006, when I was working at Juniper, I used a similar method to summarize the data plane processing flow of ScreenOS, and many details are still vivid in my memory.

Many times during interviews, I ask candidates about projects they designed and implemented three to five years ago, and they often can’t answer, commonly responding, “This project is too long ago, I can’t remember very clearly,” which I find peculiar. To me, any project I’ve worked on or code I’ve read, no matter how long, I can recall a lot of details as if they were a part of me.

Despite nearly 20 years of not touching it, I still remember some details of the OSPFv2 and IGMPv3 protocols from my first job, know how netlink works, and have a basic impression of the Linux VMM’s management process. Now, when I think about it, it might just be that I have mastered the correct method of learning.

So, in this course introducing the language, I have also smuggled in many methodological ideas. Most are scattered in various corners of the articles, from the methods of analyzing/solving problems mentioned just now to code reading methods, architectural design methods, interface writing and iteration methods, testing methods, code refactoring methods, and so on. I hope these “private goods” resonate with you and, combined with the methods you summarize in your career, serve your learning and work better.

Reading #

In the process of writing this column, I’ve referenced quite a few books. For example, “Programming Rust,” “Designing Data-intensive Applications,” and “Fundamentals of Software Architecture.” It’s a pity that Jon Gjengset’s [“Rust for Rustaceans”] was late; otherwise, the standard of this column could have gone up a notch.

We software developers seem to read less and less as we age, which is not good. Picasso said, “good artists copy; great artists steal.” When you learn from one person, you are imitating; when you learn from a crowd, you slowly integrate and become a master.

So, don’t think that after learning this first Rust course, you are done. This course is only a stepping stone that introduces you to the world of Rust; you still need to further learn and solidify more knowledge from various aspects.

Just as I answered a reader’s question: Often, what we lack is not the understanding of Rust knowledge, but a broader understanding of software development knowledge. Thus, don’t restrict yourself to Rust alone; read widely and think deeply about what interests you and the scenarios you will encounter in the future.

Action #

Alongside learning, reading, and thinking, we also need to practice extensively. Do not ask for help at every issue; think about whether you can construct a simple enough code to solve the problem.

For example, someone might ask: How does HTTP/2 work? Such a question can be approached not only by looking at RFCs and reading others’ summaries of experience but also by getting your hands dirty with a few lines of code to gain lots of information. For instance:

use tracing::info;
use tracing_subscriber::EnvFilter;

fn main() {
    tracing_subscriber::fmt::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    let url = "https://www.rust-lang.org/";

    let _body = reqwest::blocking::get(url).unwrap().text().unwrap();
    info!("Fetching url: {}", url);
}

This snippet of code shouldn’t be hard for you to write, but have you tried using RUST_LOG=debug or even RUST_LOG=trace to see the logs? Have you tried following the log’s trail to analyze the involved libraries?

Below is the output of the above few lines of code with RUST_LOG=debug, which allows you to see the basic workings of HTTP/2. I suggest you try RUST_LOG=trace (the content is too much to paste here); if you can make sense of the output, then you’ll have a fair understanding of the main process of handling HTTP/2 in Rust with hyper.

❯ RUST_LOG=debug cargo run --quiet
2021-12-12T21:28:00.612897Z DEBUG reqwest::connect: starting new connection: https://www.rust-lang.org/    
2021-12-12T21:28:00.613124Z DEBUG hyper::client::connect::dns: resolving host="www.rust-lang.org"
# ...(additional log lines demonstrating the HTTP/2 connection and request process)...
2021-12-12T21:28:01.018665Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1) }
2021-12-12T21:28:01.018885Z DEBUG Connection{peer=Client}: h2::codec::framed_read: received frame=Data { stream_id: StreamId(1), flags: (0x1: END_STREAM) }
2021-12-12T21:28:01.020158Z  INFO http2: Fetching url: https://www.rust-lang.org/

So, oftentimes, the knowledge is right around us; writing some code can get us there.

In this process, the exploratory code you write after your own thinking, the thinking you invest when analyzing the output, the in-depth reading, and finally the summarizing make the knowledge firmly your own.

Lastly, let’s talk about coding.

For learning any language, the most important step is to use the acquired knowledge to solve real problems. Can Rust handle the various tasks you need to complete? Most likely, yes. But can you complete these tasks using Rust? Not necessarily. Every person with ten fingers can learn to play the piano, but not everyone who learns to play the piano can reach grade ten. The vast gap between reality and the ideal is “deliberate practice.”

To become a Rust expert and make Rust a significant skill in your career, deliberate practice is essential, involving continuous coding. Indeed, learning and using Rust’s ownership and lifetimes can be difficult to understand; ownership and lifetimes combined with the type system (including generics and traits) and asynchronous development present even greater challenges. However, through continuous learning and practice, you will find that they are just small hills you’ll cross on your great journey.

Finally, I know many of you have struggled silently and learned in solitude, and as this column comes to an end today, I welcome you to comment in the comment section. I really hope to hear your voice, to hear about your feelings and what you’ve gained from learning this column and to see you there. Click here to provide feedback and suggestions on the course.

Thank you for choosing my first Rust lesson. Thank you for joining us all the way here. From now on, it’s up to you.

“Go where you must go, and hope!”— Gandalf