7.3 KiB
pstree in Rust
This repository contains a simple implementation of the Unix pstree program in Rust. This is mostly done as a learning exercise and port of a C implementation I wrote as an exercise in the Excellent book The Linux Programming Interface (TLPI).
The implementation here does not and is not intended to fully match the implementation of pstree that you might find on your computer and is not intended to replace it.
Build and Run It
You will need to install the latest version of rust. When I originally wrote this code, v0.12 of Rust had just recently been released.
$ rustc pstree.rs
$ ./pstree
Notes From Implementing
As noted, implementing this program in Rust was very much a learning exercise and a somewhat painful one at that. Here's some useful things I picked up along the way that might be helpful for other aspiring Rustafarians.
The Compiler is Probably Right
There are things you can get away with in languages like C that are either unsafe or not provably safe. Rust is a safe language (by default) and it is useful to try to think in terms of the compiler being able to reason about your code.
With rust-pstree, one thing that I found I was not thinking deep enough was ownership of data associated with each process. Rust is not a managed language, so it must be clear who owns and what the lifetime of each piece of memory is for your code to compile. This takes some getting used to but starts to make sense over time.
As my work progressed, I started to be able to ask the right questions. This yielded much better results when asking for help on IRC. For instance, consider the following function:
fn populate_node(node : &mut ProcessTreeNode, records: &Vec<ProcessRecord>) {
// populate the node by finding its children... recursively
let pid = node.record.pid; // avoid binding node as immutable in closure
for record in records.iter().filter(|record| record.ppid == pid) {
let mut child = ProcessTreeNode::new(record);
populate_node(&mut child, records);
node.children.push(child);
}
}
At first, a lot of things look weird here. Now, I can look at this and read through the code making the following obervations:
-
There is a function called
populate_node
that takes a mutable borrowed reference to aProcessTreeNode
and an immutable borrowed reference to aVec
ofProccesRecord
s and returns nothing. From this I know that the provided node may change but nothing else will including elements within the records vector. -
First, there is an odd line with a comment. It says
avoid binding node as immutable in closure
. This change was made because we needed to make reference to the node'spid
within the for loop but the compiler was preventing us from doing so through the node within the closure for thefilter
expression.Without this change, we receive the following error:
pstree.rs:121:9: 121:22 error: cannot borrow `node.children` as mutable because `node` is also borrowed as immutable pstree.rs:121 node.children.push(child); ^~~~~~~~~~~~~ pstree.rs:118:41: 118:80 note: previous borrow of `node` occurs here due to use in closure; the immutable borrow prevents subsequent moves or mutable borrows of `node` until the borrow ends pstree.rs:118 for record in records.iter().filter(|record| record.ppid == node.record.pid) { ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ pstree.rs:123:2: 123:2 note: previous borrow ends here pstree.rs:118 for record in records.iter().filter(|record| record.ppid == node.record.pid) { pstree.rs:119 let mut child = ProcessTreeNode::new(record); pstree.rs:120 populate_node(&mut child, records); pstree.rs:121 node.children.push(child); pstree.rs:122 } pstree.rs:123 } ^
Well, that looks pretty confusing. The 3rd line of the error, however, gives us a key insight. Node is first borrowed as immutable implicitly by the closure and Rust's lexical scoping rules. The life of that closure will be to the end of the for loop block. This would usually not be a problem, but accessing the record of the node involves borrowing a reference to the node children. Since
node
is mutable in this function, we are trying to obtain a mutable borrow within a closure where node is borrowed as immutable. As this violates the immutability constraint, the compiler balks.We fix by storing an immutable copy of the pid prior to the creation of the closure. Problem. solved. All I can say, is that you get much better at reading the compiler's errors and fixing them quickly as you go on.
-
The rest of the function (in the body of the for loop), reads pretty clearly. We have filtered all the records so that we are iterating over those records who are children of the node we are populating. We create a new node for each of these children and populate it (recursive call). Finally, we push the child into our list, taking ownership of it (e.g. The vec owns the child, the Node owns the vec, the tree owns the root node, and some scope (stack) owns the Tree. It is pretty much what you would do in C/C++, but the compiler checks much more for you, ensuring greater safety.
The Best way to Learn is by Reading Other Code
At several points while implementing, I got stuck. I found that
reading other code in the Rust standard library and in the community
often led to breakthroughs in my thinking. Some of this was just
noticing a standard library function I had missed before (Oh, there is
an iter_mut()
as well as an iter()
on Vec
? Other times, it was
just picking up the 'rusty' way of structuring a program.
I can't claim to be an expert or to have generated anything good, but reading is learning. Also, viewing the change for some files over time and the related PRs/discussion can be very interesting and enlightening.
Use IRC
I got good help when really stuck on the IRC channel. Just have a clear question ready to ask and code snippets ready to go. The community there wants to see you succeed.
Final Thoughts
This first experience with Rust has been somewhat frustrating but also
very enlightening. My optimism that Rust could represent a real
threat to entrenched systems programming languages is not crushed.
The next few months as Rust moves toward 1.0 will be very
interesting. I hope that the number of symbols in the language does
not increase much more. I already find the two meanings for the &
operator to be confusing at times. Explicit is better than implicit
in most cases.
Rust is not an 'easy' language to learn and I don't think it could be while still doing what it needs to do. Manual memory management is not easy, but I believe that Rust provides a real service to programmers and users by providing a language that greatly increases the chances that a program that runs is actually safe. I, for one, have already spent more than enough time debugging race conditions and resources leaks... Good riddance!