summaryrefslogtreecommitdiff
path: root/posts/First Impressions of the Rust Programming Language/first-impressions-of-the-rust-programming-language.org
blob: 1bb85d2b044008132276b430f1b3602f189ac239 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
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.