diff options
| author | Jakob L. Kreuze | 2019-05-26 10:53:19 -0400 |
|---|---|---|
| committer | Jakob L. Kreuze | 2019-05-26 10:53:19 -0400 |
| commit | 59203e1d25971e31fdd109b72a8be80cab48d2c0 (patch) | |
| tree | 68b93722abb0b9d024e8ffe729e767026023818c /org/First Impressions of the Rust Programming Language | |
| parent | bd8019400aba4e28e93c6044640f8cd11c880453 (diff) | |
Rename posts/ to org/
Diffstat (limited to 'org/First Impressions of the Rust Programming Language')
| -rw-r--r-- | org/First Impressions of the Rust Programming Language/first-impressions-of-the-rust-programming-language.org | 445 |
1 files changed, 445 insertions, 0 deletions
diff --git a/org/First Impressions of the Rust Programming Language/first-impressions-of-the-rust-programming-language.org b/org/First Impressions of the Rust Programming Language/first-impressions-of-the-rust-programming-language.org new file mode 100644 index 0000000..1bb85d2 --- /dev/null +++ b/org/First Impressions of the Rust Programming Language/first-impressions-of-the-rust-programming-language.org @@ -0,0 +1,445 @@ +#+TITLE: First Impressions of the Rust Programming Language +#+DATE: <2018-06-08 Fri 13:02> +#+TAGS: opinion, programming, rust + +C is almost 50 years old, and C++ is almost 40 years old. While age is usually +indicative of mature implementations with decades of optimization under their +belts, it also means that the language's feature set is mostly devoid of modern +advancements in programming language design. For that reason, you see a great +deal of encouragement nowadays to move to newer languages - they're designed +with contemporary platforms in mind, rather than working within the limitations +of platforms like the PDP-11. Among said "new languages" are Zig, Myrddin, Go, +Nim, D, Rust... even languages like Java and Elixir that run on a virtual +machine are occasionally suggested as alternatives to the AOT-compiled C and +C++. + +I have plans to look into the characteristics that distinguish each and every +one of these new programming languages, learning them and documenting my first +impressions in the form of blog posts. This post is the beginning of that +adventure: my first impressions of Rust. I chose to evaluate Rust first rather +than one of the other aforementioned contenders for a few reasons. For one, it's +backed by some big names like Mozilla, so I'm expecting it to have more polished +documentation than its independently developed counterparts - we might as well +step off with a language that I can learn without needing to read the compiler's +source code. Also, I've been fairly critical of Rust in the past because that +view was in-line with the opinions of my friends, but now that I've decided to +go out of my way to learn a new programming language, I might as well use this +as an opportunity to see if my criticisms were unfounded. + +Learning these new programming languages is certainly going to be an +undertaking. Because Python and C were the first languages I was introduced to, +I was able to simply buckle down, learn them, and apply them to pretty much +everything I was doing at the time. When I tried to learn other languages later +on, though, I had a hard time gauging whether or not I was making progress. I +think that this is because I wasn't engaged with what I was learning; I was, at +most, writing trivial programs with the language I was learning, and defaulting +to C or Python whenever I needed to work on a "real" project. My goal is to +learn these new languages to the extent that I can meaningfully evaluate them, +so I've looked back on my past attempts and come to the conclusion that I either +need to use them to develop something nontrivial, or make contributions to a +free software project written in the language, as suggested by [[https://hackernoon.com/unconventional-way-of-learning-a-new-programming-language-e4d1f600342c][several]] [[https://codewithoutrules.com/2017/09/09/learn-a-new-programming-language/][articles]]. +In the case of this post, it will be the former, as I've actually come to like +Rust enough to use it for my [[https://github.com/TsarFox/rebuild][reimplementation of Ken Silverman's BUILD engine]]. + +With my introduction for this series out of the way, we can get into my first +impressions of Rust. The first step was diving into the documentation to learn +it, so I guess it would make sense to begin with that. Simply put, there is no +shortage of high-quality learning material for Rust. [[https://doc.rust-lang.org/book/second-edition/index.html]["The Rust Programming +Language,"]] the equivalent of TCPL for Rust, is surprisingly well-written. Even +if you're familiar with a systems programming language like C, I would still +recommend reading it cover-to-cover. I had initially started off with the "Rust +for C++ Programmers" and the "Learn X in Y Minutes" tutorial for Rust, but until +I read TRPL, there was a lot that didn't make sense, and I was completely lost +when it came to using the standard library. The book is friendly, encouraging, +and full of great examples that outline common patterns in the standard library +and various third party crates. My only real complaint with TRPL is that some of +the the analogies step foot into the territory of [[https://www.hillelwayne.com/post/monad-tutorials/][monad tutorials]]. Some +exceptional examples are comparing a [[https://doc.rust-lang.org/book/second-edition/ch15-04-rc.html][reference-counting pointer to the TV in a +family room]], or comparing [[https://doc.rust-lang.org/book/second-edition/ch04-01-what-is-ownership.html][references to tables at a restaurant]]. They aren't all +bad, and there are a few that I actually really enjoy, like the comparison of +[[https://doc.rust-lang.org/book/second-edition/ch16-02-message-passing.html][message passing concurrency to a river]], but most of them try too hard to relate +the concept to something in the real world that it ends up being unhelpful. +Fortunately, the book is on GitHub and accepts pull requests, so I have plans to +send in suggestions for some alternatives. + +Despite the presence of great documentation, I predict that most people are +still going to have a hard time learning Rust. It brings some concepts that you +probably haven't seen before. As far as I'm aware, this is the first programming +language to offer compile-time memory management. (C++ has smart pointers which +are definitely similar, but those rules are enforced at runtime. Rust tightly +integrates its concepts of ownership and lifetimes into the compiler.) TRPL does +a good job of introducing the concepts for compile-time memory management, but I +feel that that it only really scratches the surface. For that reason, I'd like +to point anyone learning Rust to a great supplementary resource on the +memory-model: [[http://cglab.ca/~abeinges/blah/too-many-lists/book/]["Learning Rust With Entirely Too Many Linked Lists"]]. It's +hands-on, and just about as approachable as TRPL. [[http://softwaremaniacs.org/blog/2016/02/12/ownership-borrowing-hard/en/][This post]] might also help if +you're having trouble grasping the general concept. + +That brings me to another point - the features that Rust brings to the table +might be difficult to learn, but learning to use them pays off in the end. +Compile-time memory management requires designing your programs in a way you +might not be used to, but it definitely beats manual memory management, or +letting a runtime take care of garbage collection. + +C's memory model, for example, is manually managed. Heap allocations are +performed via =malloc(3)= and =calloc(3)=, and those allocations exist until +=free(3)= is called. Take this trivial piece of code for making a heap +allocation containing a string: + +#+BEGIN_SRC c :hl_lines 0 +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +int main(int argc, char **argv) { + char *buf; + + // Make a heap allocation of 14 bytes. + buf = calloc(14, 1); + + // calloc(3) CAN return a null pointer. + if (buf == NULL) { + return 1; + } + + // Fill the allocated buffer with a string, and print it. + strcpy(buf, "Hello, world!"); + puts(buf); + + // Free the heap allocation, since we're done with it. + // This won't always be at the end of the function, but it usually will be. + free(buf); + + return 0; +} +#+END_SRC + +This model requires keeping track of the allocations you make and ensuring that +they're freed when they aren't needed anymore - we easily could've forgotten +that call to =free(3)=. In this really trivial example, it doesn't matter +because the process exits and the operating system reclaims the heap page, but +if the program kept running after printing that string, we'd be dealing with a +memory leak. Anyway, C's manual memory management is explicit enough that you +can more or less predict what this will compile down to. GCC 6.4.0 emits +following amd64 code: + +#+BEGIN_SRC asm :hl_lines 0 + # Prelude. +55 pushq %rbp +4889e5 movq %rsp, %rbp +4883ec20 subq $0x20, %rsp +897dec movl %edi, -0x14(%rbp) +488975e0 movq %rsi, -0x20(%rbp) + + # calloc(14, 1), store pointer on the stack. +be01000000 movl $1, %esi +bf0e000000 movl $0xe, %edi +e892feffff callq sym.imp.calloc +488945f8 movq %rax, -8(%rbp) + + # Check for null pointer. +48837df800 cmpq $0, -8(%rbp) +7507 jne 0x750 +b801000000 movl $1, %eax +eb3b jmp 0x78b + + # (Really optimized) call to strcpy. +488b45f8 movq -8(%rbp), %rax +48ba48656c6c. movabsq $0x77202c6f6c6c6548, %rdx +488910 movq %rdx, 0(%rax) +c740086f726c. movl $0x646c726f, 8(%rax) +66c7400c2100 movw $0x21, 0xc(%rax) + + # puts(buf) +488b45f8 movq -8(%rbp), %rax +4889c7 movq %rax, %rdi +e846feffff callq sym.imp.puts + + # free(buf) +488b45f8 movq -8(%rbp), %rax +4889c7 movq %rax, %rdi +e82afeffff callq sym.imp.free + + # Teardown. +b800000000 movl $0, %eax +c9 leave +c3 retq +0f1f00 nopl 0(%rax) +#+END_SRC + +The equivalent in Rust is similar, but as you'll see, we don't need to +explicitly free the heap allocation. + +#+BEGIN_SRC rust :hl_lines 0 +use std::io; +use std::io::Write; + +fn main() { + let buf = Box::new(b"Hello, world!\n"); + io::stdout().write(*buf); +} +#+END_SRC + +rustc 1.25 compiles this down into the following amd64 code[fn:1]: + +#+BEGIN_SRC asm :hl_lines 0 + # Prelude. +4883ec48 subq $0x48, %rsp + + # Heap allocation, made by the 'std::boxed::Box' smart pointer. +b808000000 movl $8, %eax +89c1 movl %eax, %ecx +4889cf movq %rcx, %rdi +4889ce movq %rcx, %rsi +e8caedffff callq sym.alloc::heap::exchange_malloc::h42fa40019bea1ed3 + + # We actually end up storing a reference to the bytestring, rather than copying the individual bytes into the box. + # Regardless, I think this should still illustrate heap allocation fairly well, and I'm trying to keep the example somewhat simple so we'll roll with it. +488d0de3e705. leaq str.Hello__world, %rcx +4889c6 movq %rax, %rsi +488908 movq %rcx, 0(%rax) +4889742410 movq %rsi, 0x10(%rsp) + + # Get the handle to stdout. +e855590000 callq sym.std::io::stdio::stdout::h537f6f9874379378 +4889442408 movq %rax, 8(%rsp) +488b442408 movq 8(%rsp), %rax +4889442430 movq %rax, 0x30(%rsp) + + # stdout.write(*buf); +488b4c2410 movq 0x10(%rsp), %rcx +488b11 movq 0(%rcx), %rdx +be0e000000 movl $0xe, %esi +89f1 movl %esi, %ecx +488d7c2418 leaq 0x18(%rsp), %rdi +488d742430 leaq 0x30(%rsp), %rsi +e8965a0000 callq sym._std::io::stdio::Stdout_as_std::io::Write_::write::h12094683b11bc5a8 + + # Free the 'std::io::Result' that's returned by 'write'. + # We didn't check its, which is considered bad form, but this is just a simple example. +488d7c2418 leaq 0x18(%rsp), %rdi +e8fef4ffff callq sym.core::ptr::drop_in_place::h72bdea260ebb17c9 + + # Free the stdout handle. +488d7c2430 leaq 0x30(%rsp), %rdi +e8a6f4ffff callq sym.core::ptr::drop_in_place::h55479d5b85e18c56 + + # Finally, free the heap allocation we made. +488d7c2410 leaq 0x10(%rsp), %rdi +e8faf5ffff callq sym.core::ptr::drop_in_place::ha5ac9a364139ad29 + + # Teardown. +4883c448 addq $0x48, %rsp +c3 retq +#+END_SRC + +Besides needing to allocate a handle to interact with stdout, rustc's emitted +assembly does pretty much the same thing as that of GCC - allocate a buffer, +fill it, then free it when we're done using it. Rust just façades this process +with a friendlier abstraction. + +Another feature I've come to really enjoy is that there are no more NULL +pointers - they've been replaced by a strict type system à la Haskell. In the C +example above, we saw that =calloc(3)= can return =NULL= if glibc isn't able to +allocate enough memory. We easily could've forgotten to put in the check to make +sure the it isn't =NULL=, in which case we would get a segmentation fault. +Preventing this sort of thing is what people are talking about when they say +"memory safety." For a segmentation fault, the operating system has to jump in +because we're doing something we shouldn't - dereferencing a =NULL= pointer. +There are plenty of other naughty things we can do in C, like freeing a heap +allocation twice, or even worse, writing outside the bounds of a buffer. Rust +aims to have the compiler step in when we do something dumb, rather than leaving +that to the operating system or exploit mitigation systems. To do this for +=NULL=-able references, Rust provides an =Option= type (and the =Result= type) +that can represent either something or nothing. You see it used extensively in +the standard library. Consider the =find= method of =std::string::String=, a +method for finding the index of a substring in a string. There's the possibility +that the substring exists in the string, in which case we'd just return that +index, but what if it doesn't exist? In the case of C, we might return some +silly value like '-1', but in Rust, we return an =Option<usize>= - either some +=usize= value, or nothing. And the compiler makes sure we understand the +implications of this. + +#+BEGIN_SRC rust :hl_lines 0 +fn main() { + let to_search = String::from("I may contain foo."); + let index = to_search.find("foo"); + println!("index - 5: {}", index - 5); +} +#+END_SRC + +This is a pretty inane example, but please bear with me. If we try to compile +this, rustc errors out, because we're trying to treat a variable that might +represent nothing as if it were guaranteed to be something. + +#+BEGIN_SRC +error[E0369]: binary operation `-` cannot be applied to type `std::option::Option<usize>` + --> test.rs:4:31 + | + 4 | println!("index - 5: {}", index - 5); + | ^^^^^^^^^ + | + = note: an implementation of `std::ops::Sub` might be missing for `std::option::Option<usize>` +#+END_SRC + +This would be fixed by inspecting the Option, ensuring that it *is* something, +rather than nothing. It's an algebraic data type, so we can destructure it and +work with the index if =find= returned something. + +#+BEGIN_SRC rust :hl_lines 0 +fn main() { + let to_search = String::from("I may contain foo."); + if let Some(index) = to_search.find("foo") { + println!("index - 5: {}", index - 5); + } +} +#+END_SRC + +=if let= is a syntax construct that I don't think any other language has, so I +should probably give a brief explanation. That =if= block will run if and only +if =find= returned an instance of =Option= that was =Some=, rather than =None=. +If an instance of =Some= is returned, it contains our index, so we can +destructure it and set that value to the variable, =index=, which we go on to +use. + +You might expect this strictness to bring frustration, but the compiler emits +errors worded simply enough that a layman could understand them, and often makes +suggestions for fixing the code in question. The above isn't a great example, +here's a better one: + +#+BEGIN_SRC rust :hl_lines 0 +fn tabulate_slice(slice: &[u8]) { + for elem in slice.iter() { + println!("{}", elem); + } +} + +fn main() { + let vec = vec![1, 2, 3]; + tabulate_slice(vec); +} +#+END_SRC + +#+BEGIN_SRC +error[E0308]: mismatched types + --> test.rs:9:20 + | + 9 | tabulate_slice(vec); + | ^^^ + | | + | expected &[u8], found struct `std::vec::Vec` + | help: consider borrowing here: `&vec` +#+END_SRC + +Rust has a great deal of functionality that makes it feel like your typical +high-level Ruby or Python, despite being a compiled language. And it isn't +limited to what I described above - here are a few of the other features I +was really impressed with: + +* Conditionals are Expressions + +#+BEGIN_SRC rust :hl_lines 0 +let var = if true { + 1 +} else { + 2 +}; +#+END_SRC + +* No parentheses for the expression part of if/while/for + +Heh, I bet you've seen enough of that already. + +* Semantics for Infinite Loops + +#+BEGIN_SRC rust :hl_lines 0 +loop { + break; +} +#+END_SRC + +* Semantics for Unused Variables/Parameters + +#+BEGIN_SRC rust :hl_lines 0 +for _ in 0..5 { + println!("I'm printed 5 times!"); +} +#+END_SRC + +* Range Notation, Type Inference, and Iterators + +Again, you've seen these already. + +* Tuples, Destructuring, and Pattern Matching via =match= and =if let= Expressions + +#+BEGIN_SRC rust :hl_lines 0 +match to_search.find("foo") { + Some(index) => println!("Foo at {}", index), + None => println!("No foo :("), +} + +// Or, more idiomatically: + +if let Some(index) = to_search.find("foo") { + println!("Foo at {}", index); +} else { + println!("No foo :("); +} +#+END_SRC + +* Automated Testing is Integrated Into the Build System + +#+BEGIN_SRC rust :hl_lines 0 +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} +#+END_SRC + +This will be run upon invocation of =cargo test=. + +* Isolation of Unsafe Code + +There's a set of [[https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html][rules]] to ensure that the implications of working with unsafe +code are properly contained, but the gist of it is that unsafe code is isolated +by the scoping system. Mostly, I'm glad that the language allows you to work +with unsafe code at all. + +#+BEGIN_SRC rust :hl_lines 0 +fn main() { + unsafe { + asm!("INT3"); + } +} +#+END_SRC + +--- + +That's my opinion on the language design aspect, but the community and ecosystem +are important as well. My experience with the Rust community is limited, but +from what little I have seen, those in the community are friendly and rational. +I submitted [[https://github.com/mattnenterprise/rust-imap/issues/67][a few issues to rust-imap]] and received prompt and helpful responses. +I can also confidently say that the Rust ecosystem a pleasure to work with. It +obviously isn't as mature as some other language ecosystems, but adding a +"crate" dependency to your projects is as easy as adding a line to your +'Cargo.toml'. It's equally easy to publish the code and documentation for crates +you've made yourself. I threw together [[https://github.com/TsarFox/wildmidi][a library for interacting with WildMIDI]], +and a [[https://docs.rs/][docs.rs]] page popped up without any intervention from me. Painless. + +The process of linking those crates into the executable is relatively primitive, +and there are a few complaints in that respect. It's mostly static linking, so +the argument is "you get outdated copies of several libraries on your computer." +However, the benefits of dynamic linking as the alternative is a [[http://harmful.cat-v.org/software/dynamic-linking/][debate I don't +want to get into in this post]]. Right now I'll leave it as, "it's not an option +in the current implementation, and that's a disadvantage," even if I'm +blissfully ignorant of the size of my Rust binaries and /might/ have some +complaints about dynamic linking. + +All in all, I'm very happy with Rust. Maybe it isn't "there" yet as a viable +replacement to C, but it's promising and I have a feeling that, with time, it +will fit nicely into GNU/Linux ecosystem. + +[fn:1] A previous version of this post included *all* of the assembly emitted by the compiler, but in this revision, I've chosen to remove Rust's error/panic handling code because I believe that it actually detracts from the concept I'm trying to show. |