my personal adventures in rust — pistat

I’m not here to proselytize about Rust the language. There are too many out there already who will do that. I’m here to talk about how I’ve come to learn the language, what various tools and utilities I’ve written with it, and perhaps try to give an unbiased (or maybe even biased) opinion of the language. And I have example code to go along with this particular post.

Rust Background

Rust came into existence in 2006 as a Mozilla internal project. As the years went by it evolved to the point where it was self hosting and picked up a lot of features that are found in other languages. Because Rust was initially a part of Mozilla, that meant it was a victim of Mozilla’s various vicissitudes, include a big layoff at Mozilla in 2020 that had a negative impact on Rust. As a consequence the Rust Foundation was created in February 2021. One of the founding members of the Rust Foundation was Google, who announced at the time that Rust would be an alternative language for writing Android applications. The Rust Foundation itself went through some bumps as well, which you can read about elsewhere. All I can say at this point is that I hope that drama has passed.

Rust has been described as a C-style language targeted at “frustrated C++ developers,” that emphasizes features such as safety, control of memory layout, and concurrency. Because Rust started out as being written in OCaml, a functional programming language, there is a lot of that kind of influence in the design of the language. I have worked with ‘pure’ functional languages in decades past, so I can see some of that influence.

Personal Background

Let me take a moment to talk about “frustrated C++ developers.” I count myself as one of them; I know all too well what many of those issues are. I’ve had to deal with them since I learned C in 1982 with Lifeboat C on an IBM XT, through C++ (C with classes) via cfront on a DEC MicroVAX II and DEC Ultrix in 1986. I then switched to Borland C++ in 1990, and I’ve been heavily invested in that language ever since across multiple operating systems. That’s over 40 years of C and C++ development, many hundreds of thousands of lines of code scattered across dozens and dozens of for-pay application development as well as personal work. That didn’t stop me from learning other languages, such as Java (1995) and Python (1993). I’ve tinkered with additional C-style languages, most significantly Google Go, but I’ve always dropped back to C++ for my heavy coding lifting needs, regardless of the warts on C++. As they say, better the devil you know.

Through the years and decades I’ve tried to adhere to best C++ coding standards, and I’ve tried various code analysis tools to ferret out “code smells” that code reviews would miss (no human code review is ever perfect, and turns out to be a waste of human’s time; use automation and tools for consistency). I’ve tried to keep up with the latest official C++ coding standards, such as C++23, and use those compilers (such as g++ and clang) that implement those official coding standards. But it’s tough, and as the standards have been released, the language has just grown more complex.

The Application

First, an important warning to the reader: DO NOT take this as idiomatic Rust source code. Regardless, it does work.

I wrote a non-trivial (to me anyway) Rust-based utility on my Raspberry Pi 5 running Ubuntu 24.04.1 for the express purpose of producing a limited set of statistics about the running system. There’s plenty of utilities for that already, but they produce too much information for my needs and/or have too many bells and whistles I can do without. What follows is an example of the statistics output I wanted.

 Raspberry Pi model:   Raspberry Pi 5 Model B Rev 1.0 Core model / count:   Cortex-A76 / 4 Kernel version:   6.8.0-1010-raspi Operating system: Ubuntu 24.04.1 LTS Memory total / free:  7.75 Gi / 3.28 Gi Swap total / free:1.00 Gi / 1.00 Gi Filesystem capacity:  468.9 Gi Filesystem available: 432.1 Gi Uptime:   8 days, 8 hours

And here’s the listing that produced that output. There’s a lot of little (and some big) differences between Rust source code and equivalent C++. For example, there’s very little up-front boilerplate code. Note the lack of any #include statements or any using statements to shorten the use of C++ namespace prefixes. Note also how functions are defined, and how the return types are defined and returned from each function. There is no explicit return statement. If you’ve defined a return type, then the last statement in a Rust function is an implied value return, without the return statement or even a semicolon at the end of the statement.

extern crate fs4;use std::fs::File;use std::io::BufReader;use std::io::prelude::*;fn translate_uptime(line: String) -> String {let uptime_seconds_str = line.split(' ').collect::<Vec<_>>()[0].trim();let mut uptime_in_seconds:f64 = uptime_seconds_str.parse::<f64>().unwrap();let seconds_in_a_day = 86400f64;let seconds_in_an_hour = 3600f64;let days_uptime = (uptime_in_seconds / seconds_in_a_day).floor();uptime_in_seconds -= days_uptime * seconds_in_a_day;let hours_uptime = (uptime_in_seconds / seconds_in_an_hour).round();if days_uptime > 0f64 {format!("{days_uptime:} days, {hours_uptime:} hours")}else {format!("{hours_uptime:} hours")}}fn translate_meminfo(line: &str) -> f64 {let substring = line.split(':').collect::<Vec<_>>()[1].trim();let number = substring.split(' ').collect::<Vec<_>>()[0].trim();let value: f64 = number.parse::<f64>().unwrap();value / (1024.0f64 * 1024.0f64)}fn decode_cpu_part_number(line: &str) -> &str {let part_number = line.split(':').collect::<Vec<_>>()[1].trim();// Partial matching of ARMv8-A part numbers.// See https://en.wikipedia.org/wiki/Comparison_of_ARM_processors#ARMv8-A//match part_number.to_uppercase().as_str() {"0XD01" => "Cortex-A32","0XD02" => "Cortex-A34","0XD03" => "Cortex-A53","0XD04" => "Cortex-A35","0XD05" => "Cortex-A55","0XD06" => "Cortex-A65","0XD07" => "Cortex-A57","0XD08" => "Cortex-A72","0XD09" => "Cortex-A73","0XD0A" => "Cortex-A75","0XD0B" => "Cortex-A76","0XD0D" => "Cortex-A77","0XD0E" => "Cortex-A76AE",_ => "NO MATCH",}}fn get_filesystem_metrics() -> (f64, f64) {let mut total_space = fs4::total_space("/").unwrap() as f64;let mut available_space = fs4::available_space("/").unwrap() as f64;let devisor = 1024f64 * 1024f64 * 1024f64;total_space = total_space / devisor;available_space = available_space / devisor;(total_space, available_space)}fn main() -> std::io::Result<()> {let cpuinfo_file = File::open("/proc/cpuinfo")?;let mut buf_reader = BufReader::new(cpuinfo_file);let mut cpuinfo = String::new();buf_reader.read_to_string(&mut cpuinfo);let mut core_count = 0;let mut cpu_part = "";let mut model = "";for line in cpuinfo.lines() {if line.contains("CPU part") && cpu_part.is_empty() {cpu_part = decode_cpu_part_number(line);}if line.contains("processor") { core_count += 1; }if line.contains("Model") && model.is_empty() {model = line.split(':').collect::<Vec<_>>()[1].trim();}}let meminfo_file = File::open("/proc/meminfo")?;buf_reader = BufReader::new(meminfo_file);let mut meminfo = String::new();buf_reader.read_to_string(&mut meminfo);let mut memory_total = 0.0f64;let mut memory_free = 0.0f64;let mut swap_total = 0.0f64;let mut swap_free = 0.0f64;for line in meminfo.lines() {if line.contains("MemTotal:") {memory_total = translate_meminfo(line);}if line.contains("MemAvailable:") {memory_free = translate_meminfo(line);}if line.contains("SwapTotal:") {swap_total = translate_meminfo(line);}if line.contains("SwapFree:") {swap_free = translate_meminfo(line);}}let uptime_file = File::open("/proc/uptime")?;buf_reader = BufReader::new(uptime_file);let mut uptime = String::new();buf_reader.read_to_string(&mut uptime);let system_uptime = translate_uptime(uptime);let version_file = File::open("/proc/version")?;buf_reader = BufReader::new(version_file);let mut version = String::new();buf_reader.read_to_string(&mut version);let kernel_version = version.split(' ').collect::<Vec<_>>()[2].trim();let os_release_file = File::open("/etc/os-release")?;buf_reader = BufReader::new(os_release_file);let mut os_release = String::new();buf_reader.read_to_string(&mut os_release);let mut pretty_name = "";for line in os_release.lines() {if line.contains("PRETTY_NAME=") {let pretty_name_quoted = line.split('=').collect::<Vec<_>>()[1];pretty_name = &pretty_name_quoted[1..pretty_name_quoted.len()-1];}}let (total_space, available_space) = get_filesystem_metrics();println!(" Raspberry Pi model:   {}", model);println!(" Core model / count:   {} / {}", cpu_part, core_count);println!(" Kernel version:   {}", kernel_version);println!(" Operating system: {}", pretty_name);println!(" Memory total / free:  {:.2} Gi / {:.2} Gi", memory_total, memory_free);println!(" Swap total / free:{:.2} Gi / {:.2} Gi", swap_total, swap_free);println!(" Filesystem capacity:  {:.1} Gi", total_space);println!(" Filesystem available: {:.1} Gi", available_space);println!(" Uptime:   {}", system_uptime);Ok(())}

One of my favorite Rust discoveries has been the match expression in the function decode_cpu_part_number starting on line 31. There is no switch in Rust, only match, and from what I’ve been able to ascertain match is far superior. My biggest complaint about the C++ switch statement is that it will only work with native types, such as int. Match will work with anything. In the function decode_cpu_part_number starting on line 37 I lay out clearly and cleanly the match between a given CPU value and what CPU type that value maps to, without having to go through the extra step of creating an explicit map. Rust allows me to implement a clean and easily readable decoder. I say easily readable because I have no doubt that sometime in the future I might have to go back in and add something, so it’s nice there is no ambiguity in that function.

Another Rust code feature I appreciate is println! and format!. They behave an awful lot like how Python’s print and string formatting features work. Once again C++ came up with std::cout and all its supporting functions, and while I learned it well enough to do sophisticated work, it would produce a long line of code I just didn’t care for. And it goes on.

At a total of 138 lines, I don’t consider the total source code length to be too long. It’s more than reasonable. I’d also like to point out that I know this is very specific to Linux, and in particular Linux on the Raspberry Pi 5, and at this point I don’t really care. I’m working to make it operate correctly in this environment rather than think about optimizations or portability to other systems. To quote Donald Knuth:

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

Or as a wizened software engineer once told me decades ago, make it right before you make it fancy.

And before I forget, here’s the Cargo.toml file for the source:

[package]name = "pistats"version = "0.5.0"edition = "2021"# See more keys and their definitions at# https://doc.rust-lang.org/cargo/reference/manifest.html[lints.rust]#non_snake_case = "allow"#unused_imports = "allow"unused_must_use = "allow"#unused_variables = "allow"[dependencies]fs4 = "0.9.1"

The toml file sets up the build environment. I also like the linting section, and I use that to turn off build warnings. The lint settings are counter-intuitive. For example, the one uncommented line (line 12) is telling rustc (the Rust compiler) to not emit any lines of code that isn’t doing anything with the return types from function calls. For example, I’m ignoring all returns from buf_reader.read_to_string(…) (source lines 68, 87, 194, 110, and 116). If that line were commented out, then I’d get five very verbose warnings about my ignoring those returns when I built the application with cargo. There are a lot more lint controls, which I strongly recommend you read about.

Links

Rust — https://en.wikipedia.org/wiki/Rust_(programming_language)

Go — https://en.wikipedia.org/wiki/Go_(programming_language)

Ultrix — https://en.wikipedia.org/wiki/Ultrix

Cfront — https://en.wikipedia.org/wiki/Cfront

virtualizing rhel 9.4 on an apple m1 max macbook pro

I’ve had to create a Red Hat 9.4 virtual machine on my M1 Max MacBook using UTM ( https://mac.getutm.app ). Because this is an ARM architecture machine, I used UTM because it would use Apple Silicon’s native virtualization support, and I installed the aarch64 version of Red Hat Enterprise Linux 9.4 so that it would be truly virtualized, not emulated.

So far the virtualization works flawlessly. Unfortunately for me and my work, the reason for installing it, to provide a RHEL environment for some work I’m doing, is a failure because the software I need to run under RHEL is only available for x86-64, not aarch64. Not everyone is rushing to embrace the future, especially with regards to processor architectures. It now appears that far too many vendors are refusing to support ARM systems, which is a sad state of affairs considering everyone is trying to install as much ARM-based silicon in their clouds; consider for example that AWS released Graviton 4, a 96 core beast that consumes far less power than Intel or AMD for the same workloads.

Anyway, this modest post documents how to set up UTM to install and then run aarch64 RHEL 9.4  on Apple Silicon M1 Max and a MacBook Pro. I’m going to leave the acquisition and installation of UTM as an exercise to the reader. It is open source, so you can either download a free copy from UTM’s website, or else use the App Store to purchase a permanent copy for $9.99. Using the App Store helps the UTM developers to help keep the lights on, and provides you, the user, a way to get automatic updates through the App Store.

After acquiring and installing UTM, you also need to get a copy of Red Hat’s Enterprise Linux to install. You can also do that for free by registering as a developer. You can download and install up to six copies for personal use. I’ve installed three so far, one under UTM and the other two using Parallels on another MacBook Pro running with an Intel i9. I wrote about that machine earlier, and called it the Beast.

With all the bits in place, it’s time to configure UTM and get RHEL installed on it. Again, installing the RHEL ISO isn’t that difficult, but the configuration can be somewhat perplexing. These settings work fine for me, and might work fine for you. But at least they’re a starting point.

I enabled everything on the virtualization tab except Rosetta on Linux. Remember this RHEL is aarch64. I have no idea how RHEL under x86_64 emulation might operate, and I don’t ever intend to find out. Also note that keyboard and pointer support are Mac keyboard and trackpad. The defaults are generic, and they don’t quite work as well.

I had absolutely no idea where to start with the display, so I had to play around with this for awhile before I found the value shown above. I can now shut down the VM window, and when I start it back up it will be at the correct size and resolution. Getting to this point was aggravating.

Finally we come to the networking. I found these settings on a forum posting, and discovered they work well for my setup. For one thing network traffic between my native MacBook Pro and the RHEL VM stay on my machine.

One nice note: power consumption at idle, especially inside the RHEL VM, is at an absolute minimum.

I’m still amazed at the power of Apple Silicon. With UTM installed I will probably start looking at other versions of Linux compiled natively for aarch64 under UTM.