rust for the regular human being

some time ago I wrote about the poor run-time performance of python. I recently wanted to use that same piece of code and thought it might be a good exercise to rewrite that program in rust.

rust is a new system programing language focused on safety without sacrificing performance.
So I could have the advanced features I easily implemented on python with the speed of C, and figure out what’s behind the hype over rust.

My code is an AFSK modem, which takes an input string and encodes it into a wav file. for the sake of this article I’ll discuss just a simplified version of it, if you want do see the actual code, dig right in.

I’ve started with the original C code as I thought that the procedural style would be a better fit to rust. and I actually almost got away with it. I was very happy to avoid bit-banging the WAV file format and simply use the hound crate (this is how rust calls a library) and many of the modern features of the language such as polymorph variables (known as enums)

fn write_byte<W:io::Write + io::Seek>(writer: &mut hound::WavWriter<W> , ch:u8, bitStuffing:bool){
  for i in 0..8 {
		write_bit(writer, ch & 1, bitStuffing);
		ch >>= 1;
	}
}

fn write_frame<W:io::Write + io::Seek>(writer: &mut hound::WavWriter<W> , msg:&str){
	
	  // Create and write actual data
	  for c in msg.chars()
	  {
		  write_byte(writer, c, true);
	  }
}

fn main() {
  let spec = hound::WavSpec {
      channels: 1,
      sample_rate: 44100,
      bits_per_sample: 16,
      sample_format: hound::SampleFormat::Int,
  };
  let mut writer = hound::WavWriter::create("sine.wav", spec).unwrap();
  write_frame(&mut writer, &text)
}

then I hit a snag. the modem uses bit coding that does not allow more than 5 consecutive ‘1’ bits. so I needed to maintain a bit counter. The original C code used a static variable, or I could use a global variable. But rust’s rigid memory management frowns upon both. I could pass variables between functions as parameters, but it started to accumulate and felt wrong to me.

So, then I thought I’ll go the Object-Oriented route. you’ll never find ‘class’ or ‘object’ in rust’s keywords, but under the hood, OO is alive and kicking. You can define a struct and then implement functions to it. A bit old fashioned but sounds familiar enough.

Most of rust documentation is very “academic” so I had to do a lot of trial and error alongside heavy googling and stack-overflow reading. and there a few quirks like having to define the “new” method.

I ended up with something like this:

extern crate clap;

use clap::{Arg, App};

struct Modem {
	bitcount: i32,
	writer: hound::WavWriter<std::io::BufWriter<std::fs::File>>,
	src: String,
	dest: String,
	isHigh:bool,
}

impl Modem {
    pub fn new(filename:&str)-> Modem {
		Modem{
			bitcount:0,
			is_high:false,
			phase:0.0,
			writer:hound::WavWriter::create(filename, spec).unwrap(),
		}
	}
    fn write_freq(&mut self , freq:f32, duration:f32){
    ... snip ...
   }
}

fn main() {

  let mut encoder = Modem::new("sine.wav", src, dest);
  encoder.write_frame(text)

}

And it actually worked !

Encouraged by my success, I’ve moved on to the next task of splitting the long code into serval files. A task that might seem as trivial, but as everything else in rust it’s anything but. Rust features a module system that was designed to include every bell and whistle. but like they say, “the road to hell is paved with good intentions”. The documentation is filled with complex examples for many sophisticated cases. I on the other hand just wanted to put each class in it’s own file. I’ve ended up with creating a “module” named “aprs” which contains the two classes “ax25” and “modem”. The folder structure is as follows

A screenshot of a cell phone

Description automatically generated

Ax25.rs & modem.rs holds the respected structs. for example:

    pub struct AX25 {
        src_callsign : String,
        dest_callsign : String,
        encoded: Vec<u8>,
    }
    
    impl AX25 {
        pub fn new(src:&str, dest:&str)->AX25{
            AX25{
                src_callsign : src.to_string(),
                dest_callsign : dest.to_string(),
                encoded: Vec::new(),
            }
        }

        fn mark_end_of_callsigns(&mut self)
        {
            let v = self.encoded.pop().unwrap();
            self.encoded.push(v | 0x01);
        }
  ...snip...

mod.rs holds the module definition, somewhat like __init__.py in python

pub mod ax25;
pub mod modem;

and then in main.rs we declare the module and import the classes

mod aprs;
use aprs::ax25;
use aprs::modem;

not sure how rustian it is, but it was the closest I got to a code I can consider “normal”

One thought on “rust for the regular human being

Comments are closed.