8
votes

I know Rust doesn't support variable-length arrays, but that leaves me wondering what to replace them with, given that:

  • I don't want to allocate and deallocate a tiny Vec in a loop
  • The borrow checker doesn't let me move the code outside the loop
  • There are many limitations on fixed-size arrays, so I can't figure out how to use them

The C code I'm converting processes an image by calling a callback on each line, passing a small array of pointers:

float *tmp[img->channels]; // Small, up to 4 elements
for(int y = 0; y < height; y++) {
    for(int ch = 0; ch < img->channels; ch++) {
        tmp[ch] = &img->channel[ch]->pixels[width * y];
    }
    callback(tmp, img->channels);
}

My Rust attempt (example in playpen):

for y in 0..height {
    let tmp = &img.channel.iter().map(|channel| {
        &mut channel.pixels.as_ref().unwrap()[width * y .. width * (y+1)]
    }).collect();
    callback(tmp);
}

But it's rejected:

a collection of type [&mut [f32]] cannot be built from an iterator over elements of type &mut [f32]

Sadly, that sounds exactly like what I was trying to do!

I've tried using a fixed-size array, but Rust doesn't support generics on them, so I can't populate it from an iterator, and I can't populate them in a C-like loop, because references taken in the loop don't outlive it.

the trait core::iter::FromIterator<&mut [f32]> is not implemented for the type [&mut [f32]; 4]


Another approach with taking slice of memory from a fixed-size array, also fails:

let mut row_tmp: [&mut [f32]; 4] = unsafe{mem::zeroed()};
for y in 0..height {
    row_tmp[0..channels].iter_mut().zip(img.chan.iter_mut()).map(|(t, chan)| {
        *t = &mut chan.img.as_ref().unwrap()[(width * y) as usize .. (width * (y+1)) as usize]
    });
    cb(&row_tmp[0..channels], y, width, image_data);
}

error: cannot borrow img.chan as mutable more than once at a time

1

1 Answers

6
votes

arrayvec is a library that does what you’re looking for. (Also, you probably want iter_mut and as_mut instead of iter and as_ref.)

for y in 0..height {
    let tmp: ArrayVec<[_; 4]> = img.channel.iter_mut().map(|channel| {
        &mut channel.pixels.as_mut().unwrap()[width * y .. width * (y+1)]
    }).collect();
    callback(&tmp);
}

It allocates a fixed amount of storage (here 4 items) on the stack, and behaves like a Vec whose size is bounded (up to the capacity specified at compile time) but variable.

Most of the complexity in arrayvec is to deal with running destructors for a variable number of items. But since &mut _ doesn’t have a destructor, you can also get a way with just a fixed-size array. But you have to use unsafe code and be careful not to read un-initialized items. (Fixed-size arrays don’t implement FromIterator, which is what Iterator::collect uses.)

(Playpen)

let n_channels = img.channel.len();
for y in 0..height {
    let tmp: [_; 4] = unsafe { mem::uninitialized() }
    for (i, channel) in img.channel.iter_mut().enumerate() {
        tmp[i] = &mut channel.pixels.as_mut().unwrap()[width * y .. width * (y+1)];
    }
    // Careful to only touch initialized items...
    callback(&tmp[..n_channels]);
}

Edit: The unsafe code can be replaced with:

let mut tmp: [&mut [_]; 4] = [&mut [], &mut [], &mut [], &mut []];

The shorter [&mut []; 4] initializer syntax does not apply here because &mut [_] is not implicitly copyable. The type annotation is necessary so you don’t get [&mut [_; 0]; 4].