Can "const fn" concatenate byte slices?

Hi,

When I compile the following code snippet:

struct Packet([u8; 4]);

impl Packet {
    const fn from(labels: [&[u8; 2]; 2]) -> Packet {
        let mut bytes = [0; 4];
        bytes[..2].copy_from_slice(labels[0]);
        bytes[2..].copy_from_slice(labels[1]);
        Packet(bytes)
    }
}

const AA: &[u8; 2] = b"AA";
const BB: &[u8; 2] = b"BB";
const CC: &[u8; 2] = b"CC";

const AABB: Packet = Packet::from([AA, BB]);
const AACC: Packet = Packet::from([AA, CC]);

I get the following compiler error:

error[E0723]: mutable references in const fn are unstable
 --> src/main.rs:7:9
  |
7 |         bytes[..2].copy_from_slice(labels[0]);
  |         ^^^^^^^^^^
  |
  = note: see issue #57563 <https://github.com/rust-lang/rust/issues/57563> for more information
  = help: add `#![feature(const_fn)]` to the crate attributes to enable

The error is very clear: mutable references in const fn are not yet part of stable Rust. But maybe there's a way to achieve that in stable Rust without using mutable references?

I know I could do this instead:

const AABB: Packet = Packet(*b"AABB");
const AACC: Packet = Packet(*b"AACC");

But in this case, I'm not reusing the "AA" constant which is precisely what I'm trying to achieve.

Thanks for any help on this matter!

It maybe doesn't look as nice, and it'd get unwieldy pretty fast if you needed to increase the length of the Packet or the labels, but the following works and is pretty simple:

impl Packet {
    const fn from(labels: [&[u8; 2]; 2]) -> Packet {
        let bytes = [labels[0][0], labels[0][1], labels[1][0], labels[1][1]];
        Packet(bytes)
    }
}
1 Like

Can you steal inspiration from the const-concat crate?

Hi Michael-F-Bryan,

Yeah, the const-concat crate kind of does what I'm trying to achieve, but just tried it and it doesn't compile on stable Rust. Thanks though it's a good discovery!

Thank you ArifRoktim! It doesn't look nice, but it works!

But to follow up on your point about increasing the Packet length, if I wanted to have packets with 2 or 3 labels, I could do:

struct Packet2Labels([u8; 4]);
struct Packet3Labels([u8; 6]);

impl Packet2Labels {
    const fn from(labels: [&[u8; 2]; 2]) -> Packet2Labels {
        let bytes = [labels[0][0], labels[0][1], labels[1][0], labels[1][1]];
        Packet2Labels(bytes)
    }
}

impl Packet3Labels {
    const fn from(labels: [&[u8; 2]; 3]) -> Packet3Labels {
        let bytes = [labels[0][0], labels[0][1], labels[1][0], labels[1][1], labels[2][0], labels[2][1]];
        Packet3Labels(bytes)
    }
}

const AA: &[u8; 2] = b"AA";
const BB: &[u8; 2] = b"BB";
const CC: &[u8; 2] = b"CC";

const AABB: Packet2Labels = Packet2Labels::from([AA, BB]);
const AABBCC: Packet3Labels = Packet3Labels::from([AA, BB, CC]);

But this starts to get crazy if I want to have packets with 2, 3, 4, 5, or 6 labels (which is kind of what I need for my real use case). Ideally, I would like to use a single struct with a single constructor. The signature of the constructor would look like:

const fn from(labels: &[&[u8; 2]]) -> Packet

instead of:

const fn from(labels: [&[u8; 2]; 2]) -> Packet

That said, my Packet would need to be generic over the number of bytes:

struct Packet([u8; N]);

where N equals 2 * number of labels.

Is this something that will only be possible with constant generics? Maybe there's a way to circumvent this by using a struct like this:

struct Packet(&'static [u8]);

But I don't know how to implement the constructor with an arbitrarily-length slice of labels:

const fn from(labels: &[&[u8; 2]]) -> Packet

Yeah, seems like you probably need const generics if you need to be generic over the number of bytes. I don't know much about the specifics of const generics so I can't help you too much here.

You could implement the constructor like so, but to actually use it with const generics, you'd probably have to change label's type from a slice to something like [&[u8; 2]; N]:

impl Packet {
    const fn from_slice(labels: &[&[u8; 2]]) -> Packet {
        // replace with whatever const generic syntax you need
        let mut bytes = [0; 4];
        
        let mut i = 0;
        let len = labels.len();
        // for loops/iterators not usable in const fns
        while i < len {
            bytes[2 * i] = labels[i][0];
            bytes[2 * i + 1] = labels[i][1];
            i += 1;
        }
        Packet(bytes)
    }
}

If you don't want to use const-generics, you could also use type-level numbers (typenum) with the generic_array crate.
(This is part of how a crate like nalgebra can in stable rust, have a VectorN type and then basically define Vector4 as VectorN<U4> and Vector6 as VectorN<U6>.)

But this doesn't solve the const variable problem. Instead, using your new Packet that's based on GenericArray, you can use either the lazy_static or once_cell crates to define some static variables:

use once_cell::sync::Lazy;

static AABB: Lazy<Packet> = Lazy::new(|| {
    // Call your non-const constructor here.
    // This block isn't required to be const:
    todo!("Panics aren't const yet this compiles")
});

Using a lazy type will initialize AABB when it is first accessed.

The closest I could come up with is storing an oversized array and length inside Packet:

#[derive(Debug)]
struct Packet([u8;8],usize);

impl Packet {
    const fn from(labels: &[&[u8]]) -> Packet {
        let mut bytes = [0; 8];
        let mut idx_out = 0;
        let mut idx_in = 0;
        let mut idx_lbl = 0;

        'outer: while idx_out < bytes.len() {
            while idx_in >= labels[idx_lbl].len() {
                idx_in = 0;
                idx_lbl += 1;
                if idx_lbl >= labels.len() {
                    break 'outer;
                }
            }
            bytes[idx_out] = labels[idx_lbl][idx_in];
            idx_in += 1;
            idx_out += 1;
        }
        Packet(bytes, idx_out)
    }
}

(Playground)

Yeah that's clever! I'm surprised that the following line is not considered a mutable reference to bytes, which is not allowed inside "const fn" in stable Rust.

bytes[idx_out] = labels[idx_lbl][idx_in];

In my head, the line above was equivalent to the line below which doesn't compile because of the mutable reference.

*bytes.get_mut(idx_out).unwrap() = labels[idx_lbl][idx_in];

I'll have to investigate why bytes[index] = value isn't considered a mutable reference. Thanks, that's an interesting solution and it made me learn stuff!

Yeah, I really want those packets to be generated at compile-time for const variables. I want to avoid going the lazy_static way for this program. I'm also trying to avoid heap allocations.

I guess I'll have to wait for const generics to get exactly what I want. But in the mean time, I can use the solution you proposed and just create different structs for different number of labels. Or I can go with @2e71828's solution.

Also, in your code sample, I'm surprised that the following line is not considered a mutable reference to bytes:

bytes[2 * i] = labels[i][0];

In my head, the line above was equivalent to the line below which doesn't compile because of the mutable reference.

*bytes.get_mut(2 * i).unwrap() = labels[i][0];

I'll have to investigate why bytes[index] = value isn't considered a mutable reference.

Thank you for both your answers, they've been very instructional!

In case you didn't know, GenericArray is stack allocated.
Still, I personally would use @2e71828's solution. Type level numbers and the generics/type names involving them seem pretty complicated (but also pretty cool). Their solution is much simpler, works without lazy types, and memory is cheap.


I did some digging for you and I think the answer is compiler magic. From here:

unsafe impl<T> SliceIndex<[T]> for usize {
    type Output = T;
// --snip--
    fn index_mut(self, slice: &mut [T]) -> &mut T {
        // N.B., use intrinsic indexing
        &mut (*slice)[self]
    }
}

Slices are a primitive type and so the compiler intrinsically knows how to index into them.
It's common for the primitive types to implement traits even though the compiler intrinsically knows how to perform that operation. This is so that you can still use the primitive types in situations with generic types that have trait bounds.

2 Likes