Working with arrays in the Rust language

I’ve recently gotten back into writing more projects/code in rust. I initially got into rust after reading “The Rust Programming Language(2018)” book from No Starch Press. I was intrigued by the memory guarantees and the build system being part of the language. There are some places where I feel the language becomes hostile to it’s users, arrays being one of them.

Arrays as a afterthought

I can’t help but feel that as a programmer using rust that you aren’t encouraged to use arrays. Most the example code and documentation you find makes heavy use of the standard libraries built in collections, vectors in this case.

Vectors are great for many things but having a small size and being allocated onto the stack isn’t one of them.

Often if I have a chunk of information that has a known size at compile time I would prefer to reach for a C-style array; often this array will be populated during runtime when as a system collects information or data.

This can act as a real issue when trying to squeeze out both performance and size from the use of things like lookup-tables (LUT)s.

Example

As they say, “Show me the code!”

#include <stdio.h>
...SNIP...


...CONT...
#define LUT_WIDTH   255
#define LUT_HEIGHT  255

//Creating structure matrix addressible by uint8_t
typdef struct {
    uint8_t data[LUT_WIDTH][LUT_HEIGHT];
    uint8_t id;
    uint16_t save_address;
}mini_matrix;


int main() {
    
    //instance of the mini_matrix
    mini_matrix default_fuel_map;
    default_fuel_map.id = 0;
    default_fuel_map.save_address = 0x06

    load_fuel_map(&default_fuel_map);

    //afr mapped to 0-255
    uint8 afr;

    while(1) {
        //high preformance polling loop
        ...SNIP...
        
        ...CONT...
        
        afr = lookup_air_fuel_ratio(&default_fuel_map);
        update_injector_pwm(&afr);
    }
    
    
    return 0;
}

The simplified code above is for a imaginary ECU where fuel maps are stored into a 2D array acting as a lookup table. We use this method because it means that we can pre-calculate the correct fuel map and use it to apply pwm settings to the injectors.

If we manually calculated based off of sensor data for a continuous stream of data we would probably be better off using a dedicated controller for DSP.

Either way loading a lookup table into the stack works well to reduce the number of cycles we have to use; it also removes the need to address memory on the heap which can burn cycles.

Example in Rust

//Attempting simalar things in rust.

#[derive(Debug)]
struct MiniMatrix {
    id: u8,
    save_address: u16,
    data: [[u8; 255]; 255],
}

fn main() {
     //instance of the mini_matrix
    let mut default_fuel_map: mini_matrix;
    default_fuel_map.id = 0;
    default_fuel_map.save_address = 6;
    load_fuel_map(&mut default_fuel_map);
    
    
    //afr mapped to 0-255
    let afr: u8 = 0;

    while true {
        //high preformance polling loop
        ...SNIP...
        
        ...CONT...
        
        afr = lookup_air_fuel_ratio(&default_fuel_map);
        update_injector_pwm(&mut afr);
    }
       
}

Did you notice the issue?

Probably not at first unless you have spent a lot of time using rust. This code will cause some warnings and other issues because of the MiniMatrix structure.

When an instance of the struct is created in the main loop we don’t initialize the data field(2d array). The rust compiler really doesn’t like that so it smacks the developer’s hand informing them they really should initialize it.

Problem with this is, it can be an absolute hassle for more complex arrays of data. If we use the Option and make a default() impl for the struct then we have now introduced more overhead, but if we don’t then compiler issues will plague us.

The same problem happens when we want to make the values inside the array something more complicated like another struct.

We know as users it doesn’t make sense to waste time initializing the elements of the 2d array that will overwritten shortly afterwards. We could solve this by simply using vectors in rust, but the cost in terms of space and memory access on the heap makes that unattractive.

We also can’t initialize the 2d array all at one time due to memory constraints and the data being streamed from an external flash chip, giving us access to data over serial as it comes into the usi(universal serial interface) buffer.

Solutions

So what could we do to solve the problem?

  1. change the load_fuel_map() to return a initialized structure.
  2. use a rust crate that implements this functionality.
  3. Write our own module to handle arrays
  4. use “pointers” in rust which will end up on the heap again… :(
  5. Write stuff in C

For the most part I think that when writing lower level and very high performance code C is the better option. It doesn’t get in the way of using simpler data structures allocated on the stack.

It’s totally possible to deal with this problem just in rust and I think given enough time and use of the “unsafe” keyword we could even get the same performance as the C equivalent.

The issue is how much more time it would take to get there, and the added amount of potential bugs that hide in that extra code.

creating a smaller program in C with less code that we unit test and lint, is often a better solution for in these situations for now.

Other areas of interest

I think next time I might explore more on unit testing for embedded systems as well as the ZIG programming language in the future.