←back to thread

177 points signa11 | 1 comments | | HN request time: 0.251s | source
1. goku12 ◴[] No.42163599[source]
If anyone's just starting out with Rust and struggling with the borrow checker, here are some tips. I have been using Rust since 2013 and have tried these ideas myself and while training others. (C/C++ devs may be familiar with some of these and may skip them.)

1. Give priority to the fundamental semantics of code execution - things like C memory layout [1], function calls, stack, stack frames, frame invalidation, heap, static data, multi-threading/programming, locks, synchronization, etc. Also learn associated problems like double-free, use-after-free, invalidated references, data races, etc. These are easiest to understand if you learn an assembly language. However, if you don't have the time or patience for that, at least focus hard on the hardware and memory layout topics in Rust books. If you prefer instead to learn those by making mistakes without the borrow checker intervening, start with C. (Aside: This knowledge is needed for C and C++ as well, especially C. You can't write large code without it.)

2. DON'T try to memorize the borrow checker. Borrow checker is based on a few simple principles (which you should know), but they can manifest in very complex and surprising ways (same as memory safety bugs). It's not practical to learn all such cases. Instead, check the borrow checker error message and see if you can correlate it to any memory safety problems I mentioned above. While the borrow checker can seem very complicated and arbitrary, it's designed solely to prevent those memory safety bugs. Pretty soon, you'll be comfortable with correlating BC errors to such bugs, without having to worry about how the BC found them. Knowing the real problem will also make it easy to satisfy the BC and avoid fighting with it.

3. Understand the memory layout of data structure that you use. Ref: [2]. Borrow checker errors often require this knowledge to make sense. The same becomes crucial during debugging if you make such mistakes in C/C++. The BC wont even allow you to compile it if you make similar mistakes in Rust. You need it just to get the program to run.

4. Borrow checker wont solve every problem for you. It doesn't have the intelligence to reason it all. There are a few notable cases:

- Data structures with cycles (like closed graphs, dequeues, etc) and algorithms that deal with them. (BC prefers data structures in a tree hierarchy)

- Function calls across an FFI boundary (since the BC can't check that code)

- Valid cases according to borrow checker rules, but rejected anyway due to complicated lifetime analysis. These may eventually get resolved in a later Rust version. But such cases exist.

5. Most of the BC errors can be solved by simple code refactors. But in cases like above, you need to identify them (as a limitation of the BC) and look for an alternative solution. BC is a compile-time safety checker. The alternatives are:

- Runtime safety checks using concepts like Rc, Arc, Cell, RefCell, etc. They will pass the BC checks at compile time. But if memory safety is violated at runtime, it will simply panic and crash. It may also have a slight performance hit due to runtime checks. But this is most often very negligible, given the fact that most other languages are based entirely on such checks (GC, ARC, etc). You don't need to be too shy in resorting to these to get around BC complaints. Many Rust programmers do.

- Manual safety checks using unsafe. If the performance is absolutely important for you, you can use unsafe functions and blocks. Unsafe keyword activates some potentially memory-unsafe language features (like raw pointer de-referencing) that the BC doesn't vet (Note: BC is not deactivated). This is often what you need when you're trying to implement an unavailable data structure or algorithm. This is also the only choice for FFI calls. Rust will not check them at any time. But this usually isn't a problem. Unsafe blocks are often used only for very fundamental ideas (eg: self-referential structs) and consist of no more than 5% of a codebase. If a memory safety bug does occur, it will be in one of those blocks and will be easier to locate and correct. Moreover people convert them to libraries and publish them on crates.io. This improves the chance of finding any hidden bugs. If you need unsafe code, there's probably a library for it already.

To get a better intro, check out 'Learn Rust With Entirely Too Many Linked Lists' [3]

[1] https://www.scaler.com/topics/c/memory-layout-in-c/

[2] https://cheats.rs/#basic-types

[3] https://rust-unofficial.github.io/too-many-lists/