Is `generic-array` the solution to this problem?

I'm hoping someone can shed light on what's the best way to solve this problem.

Context:

  1. I have an i2c device that takes a command (via a i2c write call), performs some computation
  2. We then read from the device after a delay of a few milliseconds.

Problem:

  1. The size of the response can vary but the first byte contains the length in bytes
  2. I'm looking for a way to use the returned length-information contained in the first-byte to perform a second read and retrieve the rest of response.

But as I'm not using a heap and array lengths don't support generic parameters, I don't know of a way to achieve this other than - just define a big enough array (i.e. MAX) buffer for the response. I was looking at generic arrays and wondered whether my problem can be solved by it.

If no, is there some other way to achieve this.

Pseudocode for what I'm trying to achieve


//First read
let mut count_byte = [0; 1];   
self.i2c.read(self.dev_addr, &mut count_byte);

//Second Read
let mut remaining_bytes = [0; count_byte[0]]; // This doesn't work in Rust as array lengths can't be generic constants
self.i2c.read(self.dev_addr, &mut remaining_bytes);

Without heap allocation, just defining a large array is the only way.

Thank you for confirming. I worked around this problem by using length-information to build an array slice for the second read. So, I could allocate a big enough array and then take a slice that's equal to the length.

Looks something like this

        let mut count_byte = [0; 1];
        self.i2c.read(self.dev_addr, &mut count_byte);
        // Subsequent bytes 
        let mut resp = [0; (constants::CMD_SIZE_MAX) as usize];
        self.i2c.read(self.dev_addr, &mut resp[..count_byte[0] as usize]);

If you have a statically known upper bound on the length of the array, then you can use that upper bound, and afterwards slice it.

For instance, if by byte you mean u8, which thus cannot yield a length greater than 255, we got our upper bound:

// First read
let mut count_byte = [0_u8; 1];
self.i2c.read(self.dev_addr, &mut count_byte);

// Second Read
let remaining_bytes = &mut [0_u8; 255][.. usize::from(count_byte[0])];
self.i2c.read(self.dev_addr, remaining_bytes);

More generally, if you don't have an upper bound for 100% of the cases, but you still want to stack allocate for the cases where the length is small, you can use special wrapper types such as https://docs.rs/tinyvec, or use continuation-passing style to define a helper function that can stack-allocate or heap-allocate depending on the length provided:

fn with_buf<R> (len: usize, ret: impl FnOnce(&'_ mut [u8]) -> R)
  -> R
{
    const MAX_STACK_LEN: usize = 255;
    if len <= MAX_STACK_LEN {
        ret(&mut [0_u8; MAX_STACK_LEN][.. len])
    } else {
        ret(&mut vec![0_u8; len][..])
    }
}

and use it as follows:

// First read
let mut count_byte = [0; 1];   
self.i2c.read(self.dev_addr, &mut count_byte);

// Second Read
with_buf(count_byte[0] as usize, |remaining_bytes: &mut [u8]| {
    self.i2c.read(self.dev_addr, &mut remaining_bytes);
});
1 Like

I went with the first solution as I have a statically known upper bound.

And thanks for the second-option, it looks interesting (could come handy sometime). I'm working on an embedded driver. Consequently, I am actively trying to stay away from using the heap. (to avoid fragmentation and code-complexity)

1 Like

Another trick you can use when your input is too large for a stack-allocated array is to read the input in chunks (using an array on the stack), then invoke a callback to notify the caller as each chunk comes in.

I've used the callback-based approach in the past for situations where you've got a potentially unbounded amount of input (imagine reading from a serial port or something) and don't have access to a heap.

1 Like