Trying out Rust

29 April 2025
At first I thought that Rust just like Go, Elixir, and Kotlin was yet another language defined around 15 years ago which passed me by as — somewhat unfairly — being a bit faddish and never gave them much of a look beyond an unfavourable first impression. However for reasons I have now forgotten decided to give Rust a proper try-out by reimplementing an old program of mine which in the end left me with a net-positive view of the programming language. While I am doubtful of its practical usefulness in the near future it was an interesting experience and I can foresee using it again.

Language structure

Even though Rust is clearly at least ancestrally based on C at first it left the feeling that some of the deviations in syntax from the overall family of C-like languages are for the sake of being different, and while most made sense in the end it is steeper than say moving between C, C++, Java, and Javascript. However having sat down and reimplemented a non-trivial program in Rust there is very little I would now complain about even if some of them are very annoying at first. Below are some remarks on select languages particulars that I think are notable.

Bracing

Rust enforces the use of braces in program flow control even for single statements which overall I consider a good thing as allowing then to be omitted is a source of problems such as the dangling else and it makes it a lot clearer where a new level of scope opens and closes. With the conditonal book-ended by the if keyword and the required open brace Rust's designers took the opportunity to omit brackets around the conditional.

if index == 1 { println!("One"); } else { println!("Not one"); }

While bracing style is not strictly enforced the linting function rustfmt clearly demonstrates that the Rust community has a strong preference for K&R style bracing which I have developer a tolerance for but still much prefer Whitesmiths style. In the past I was dead against style guides but in more recent years have come to understand the need for them.

Memory and variable scope

Rather than any sort of reference counting as in the likes of Java objects in Rust have a single owner and the general idea is when the variable that currently holds that reference goes out of scope the object is immediately destroyed. As a result functions need to specify whether or they take ownership or borrow the object and this affects how it is supposed to be passed in by the caller, and this leads to this sort of trap where the second foo() call is an error:

fn foo(val : String) { println!("Value: {}", val); } fn main() { let x : String = String::from(""); foo(x); foo(x); }

This takes some getting used to and in practice means fiddling around with the presence/absence of the & operator until error and warnings are all gone. The correct version of the code being the snippet below.

fn foo(val : &String) { println!("Value: {}", val); } fn main() { let x : String = String::from(""); foo(&x); foo(&x); }

Rust has the copy trait for some objects which seems not that far from the C++ copy constructor and in effect makes such variables pass-by-value.

Mutability

Unlike C where a variable is marked as const if it does not change in Rust variables are immutable by default, so if they do change they need to be marked with the mut keyword. This is why there is the at-first confusing &mut since it is common for something to be both mutable and borrowed. There are limits to how good compilers are at optimising and a lot of programmers would not think about what could be made constant in cases where mutability is the default.

fn bar(str : &mut String) { str.push_str("!"); } fn main() { let mut string = String::from("Rewritable"); bar( &mut string ); println!("Str {}", string);

Nevertheless some dogma is likely involved in this decision which is carried over from functional languages like Haskell where variables are always immutable, and for some it will be annoying at first.

Structure definitions

Structure definitions have variables terminated with commas rather than semi-colons which is consistent with other parts of Rust coming from the C/C++/Javascript world seems odd. It feels very much like a Python dictionary and unlike most other descendents of C puts the type after the variable, which I suspect was done because in many cases the type is optional.

struct Client { tcp : net::TcpStream, pos : usize, out : Vec<u8>, buf : Vec<u8>, }

Object-orientation

In Rust class functions for a structure are defined in a separate implementation block, unlike Java where they are within the class definition and also unlike C++ where they can individually be prefixed with the class name.

struct Server { list_clients : Vec<Client> } impl Server { fn new(addr : &String) -> Server { Server { list_clients : Vec::<Client>::new() } } fn old(addr : &String) { // ... } fn cycle(&mut self) -> bool { // ... } }

I have always thought that what C misses is not classes but namespaces and it appears that Rust may have initially gone down that route. The appearance is not vastly different from namespaces in C++ and Java although unlike in the latter they cannot be nested, and more notably two function calls below are identical.

Server::cycle(&mut srv); srv.cycle();

One thing in favour of this hypothesis is the way the constructor uses the first unbound function call form:

let mut srv = Server::new( &str_addr );

Variable wrappers

Rust allows the wrapping up of variables in a way not that far from Haskell's monads that allows a way to signal errors without using special values such as negative numbers for a return value that is a count. One of these is Option() which has to forms: something and nothing which is demonstrated below, and there is also Result() where the error case also has a value.

fn oops(val : i32) -> Option<i32> { //return None; return Some(val); } fn main() { let result = match oops(32) { Some(value) => value, None => "Not found", }; }

The variables need to be unwrapped before they can be used but there are short-cut functions that do this in the happy path but cause a fatal error otherwise, which is for those cases where full error handling is not needed or desired. These functions are unwrap() and expect() where the difference is the latter takes a custom error message to display. There is also the possible use of ? which replaces .unwrap() including the dot in its entirity.

println!("{}", oops(16).unwrap()); println!("{}", oops(32).expect("Error!!!"));

What really mattered: Bytestrings

While Unicode is a good thing overall I remember how versions of Python prior to v3.5 provided no way to directly format byte-strings which basically made it useless for the type of lower-level protocol development I often did, so one thing that would make and break Rust for me is whether or not Rust also had the same dogma or not. Fortunately there is bstr which although notionally third-party is written by someone who is a core contributor to Rust and using it to write the snippet below sealed the deal in having a favourable view of the language.

use bstr::{B,BStr,BString,ByteVec}; fn main() { let text = B("Remy"); println!("Text: {} {} {} {}", text[0],text[1],text[2],text[3]); let mut bytes : Vec<u8> = Vec::new(); bytes.push_byte( 82 ); bytes.push_byte( 101 ); bytes.push_byte( 109 ); bytes.push_byte( 121 ); println!("Bytes: {:?}", bytes); let bstr = BStr::new( &bytes ); let mut bstring = BString::new( bytes.clone() ); bstring.push_byte( b'!' ); let array = BStr::new( &bstring ); println!("BString: {:?}", bstring); println!("BStr : {:?}", bstr); println!("Conv : {:?}", array); let arr : [u8; 4] = [82, 101, 109, 121]; let bstring = BString::new( arr.to_vec() ); let bstr = BStr::new( &arr ); let slice = BStr::new( &arr[1..3] ); println!("Array: {:?}",arr); println!("BString: {:?}",bstring); println!("BStr: {:?}",bstr); println!("slice: {:?}",slice); if bstring == b"Remy" { println!("Equals"); } }

The program I reimplemented as a learning exercise was the core of an event-based RTSP server so these are what would be used to write the underlying RFC2616 packet handling, and while figuring out all the borrows and mutables was a bit harder than it should have been it is exactly what is needed for my type of work.

Comparison with C++ and Go

One thought in the back of my mind was looking at Rust is not whether I would use it in preference to C or Python, but instead whether it would be a good investment compared to C++ and in this run-off Rust seems by far the much better bet. This sprang to mind because I came across something that suggested Rust's borrowing system was at least indirectly based on C++ RAII, and regardless of whether this is true or not I did consider looking again at C++ but almost immediately decided against it. C++ is a horrific mess which seems to mostly used within the computer gaming and financial sectors so I even deleted all references to it from my CV long ago. Rust looks like it has wider appeal as well as being a lot saner.

Doing a proper try-out of Rust has made me think about also trying out Go which is of very similar vintage and would be interesting to see how it does things differently, but it comes down to the familiar decider: Is it worth the investment? The prima facie is that professionally it is less likely to be useful and having just about got Rust under my belt I would be more inclined to go for depth rather than breadth and consolidate my Rust experience rather than start anew with Go. Having said that from a fresh glance over Go's examples and unix bindings it may well have been a better choice for the task I chose as a learning exercise.

Remarks

It required perseverance which was not helped by using the nix::sys::select wrapper which unlike sys::io::selecting really dumped me in the deep end but ultimately it was worth it. If I was starting out afresh and having to choose between Rust and Python I would most likely lean towards Rust. However for the foreseeable future the combination of C & Python covers practically all my professional and personal use-cases so as-is it is hard to justify investing much more time in learning the finer details of Rust, because for getting things done fifteen or so years experience is what calls it.