Safe conversion from [Option<Box<T>>; 2] to &[Box<T>]

This was a little puzzle for myself that I wasn't able to solve. I was wondering whether anyone would have any solutions.

Here's some unsafe code to convert [Option<Box<i32>>; 2] to &[Box<i32>] given the layout guarantees of Option and slices:

type Foo = [Option<Box<i32>>; 2];

fn foo_as_slice(foo: &Foo) -> &[Box<i32>] {
    match foo {
        // Updated per Cerber-Ursi comment below:
        // [Some(_), Some(_)] => unsafe { std::mem::transmute(foo.as_slice()) }
        [Some(_), Some(_)] => unsafe {
            std::slice::from_raw_parts(foo.as_ptr() as _, foo.len())
        }
        [Some(b), None] | [None, Some(b)] => std::slice::from_ref(b),
        [None, None] => &[],
    }
}

fn main() {
    println!("size_of<Foo>: {}", std::mem::size_of::<Foo>());
    println!("Foo1: {:?}", foo_as_slice(&[Some(Box::new(1)), Some(Box::new(2))]));
    println!("Foo2: {:?}", foo_as_slice(&[Some(Box::new(1)), None]));
    println!("Foo3: {:?}", foo_as_slice(&[None, Some(Box::new(2))]));
    println!("Foo4: {:?}", foo_as_slice(&[None, None]));
}

This successfully prints

size_of<Foo>: 16
Foo1: [1, 2]
Foo2: [1]
Foo3: [2]
Foo4: []

I was wondering whether there was a completely safe analog for this that would also have size_of<>()=16. The naive enum variant version has size_of<>()=24:

enum Bar {
    Zero,
    OneA(Box<i32>),
    OneB(Box<i32>),
    Two([Box<i32>; 2]),
}

fn bar_as_slice(bar: &Bar) -> &[Box<i32>] {
    match bar {
        Bar::Zero => &[],
        Bar::OneA(b) | Bar::OneB(b) => std::slice::from_ref(b),
        Bar::Two(b) => b,
    }
}

fn main() {
    println!("size_of<Bar>: {}", std::mem::size_of::<Bar>());
    println!("Bar::Zero: {:?}", bar_as_slice(&Bar::Zero));
    println!("Bar::OneA: {:?}", bar_as_slice(&Bar::OneA(Box::new(1))));
    println!("Bar::OneB: {:?}", bar_as_slice(&Bar::OneB(Box::new(2))));
    println!("Bar::Two: {:?}", bar_as_slice(&Bar::Two([Box::new(1), Box::new(2)])));
}

Output:

size_of<Bar>: 24
Bar::Zero: []
Bar::OneA: [1]
Bar::OneB: [2]
Bar::Two: [1, 2]

I've not managed to figure out a nice alternative. I guess the Foo version just happen to use the only two niche bits available - one at each entry - and there's no straightforward way to get that in safe rust? But I'm just throwing this puzzle out into the void at the mercy of y'all creativity in case you have an interesting solution.

Playground

As far as I know, strictly speaking, this is UB (although unlikely to cause anything in practice) - to convert between slices of different types, one is expected to use from_raw_parts.

Foo version simply uses the fact that Option<Box<T>> is the same layout as Box<T>.

Ah, okay. Running Miri on the playground didn't result in any complaints. But sure, fixed and updated the playground, thanks!

What I mean is that in the Foo case, the distinction between the 4 cases (empty slice, slice of first element, slice of second element, and slice of both elements) is handled by the Some/None that are distinguished using the niches within each Box, as embodied in the match statement in foo_as_slice. I was wondering whether there was any way to distinguish the 4 cases in safe rust with size_of<>() == 16. For example, the following simpler case does manage to give size_of<Baz>() == 16:

struct Baz {
   One(Box<i32>),
   Two([Box<i32>; 2]),
}

fn baz_as_slice(baz: &Baz) -> &[Box<i32>] {
    match baz {
        Baz::One(b) => std::slice::from_ref(b),
        Baz::Two(b) => b,
    }
}

It's unsound as wide pointers don't guarantee their order is the same across types. AFAIK that's not currently exploited (by PGO or whatever) but could be in the future. Miri only detects UB, not unsoundness.

1 Like

i do not believe there is a safe way to do what the post asks. this is something that would be really cool to see from bytemuck or a similar crate maybe one day.

also, very dumb idea, don't do this, but i would prob do

type Foo = [Option<Box<i32>>; 2];

fn foo_as_slice(foo: &Foo) -> &[Box<i32>] {
    match foo {
        // SAFETY : see https://doc.rust-lang.org/std/option/#representation
       // given they are both Some it is sound to reinterpret the [Option<Box<i32>>; 2] as  [Box<i32>; 2]
        [Some(_), Some(_)] => unsafe {
            std::slice::from_raw_parts(foo.as_ptr() as _, foo.len())
        }
        [None, Some(b)] => std::slice::from_ref(b),
        [opt, None] => opt.as_slice(),
    }
}

fn main() {
    let mut x = [Some(Box::new(1)), Some(Box::new(2))];
    println!("size_of<Foo>: {}", std::mem::size_of::<Foo>());
    println!("Foo1: {s:?} ({s:p})", s = foo_as_slice(&x));
    x = [Some(Box::new(1)), None];
    println!("Foo1: {s:?} ({s:p})", s = foo_as_slice(&x));
    x = [None, Some(Box::new(2))];
    println!("Foo1: {s:?} ({s:p})", s = foo_as_slice(&x));
    x = [None, None];
    println!("Foo1: {s:?} ({s:p})", s = foo_as_slice(&x)); // address is preserved  in the empty case
}

because it preserves the address (empty points to the start of the Foo), which is a dumb feature that will never be useful but is funny.

3 Likes