Any way to create a zero-filled Box<[u8]> without through Vec or using unsafe?

Way through Vec:

fn alloc_bytes(len: usize) -> Box<[u8]> {
   vec![0; len].into_boxed_slice()
}

Way using unsafe (from my previous project):

use core::{alloc::Layout, ptr::NonNull};
use alloc::{alloc::{alloc_zeroed, handle_alloc_error}, boxed::Box};

// alloc::raw_vec::capacity_overflow
#[cfg(not(no_global_oom_handling))]
fn capacity_overflow() -> ! {
    panic!("capacity overflow");
}

fn alloc_bytes(len: usize) -> Box<[u8]> {
    let layout = Layout::array::<u8>(len).unwrap_or_else(|_| capacity_overflow());
    let raw_ptr = unsafe { alloc_zeroed(layout) };
    let ptr = NonNull::new(raw_ptr).unwrap_or_else(|| handle_alloc_error(layout));
    let mut slice_ptr = NonNull::slice_from_raw_parts(ptr, len);
    unsafe { Box::from_raw(slice_ptr.as_mut()) }
}

Well, it's possible to coerce Box<[u8; N]> to Box<[u8]>. But the Vec approach in most cases is probably the most efficient.

1 Like

but len needs to determine at runtime

Going through Vec is the right answer. It will compile down to the same thing as your unsafe code.

(Yes, the macro has a special case to use alloc_zeroed when the contents are just zeroes.)

10 Likes

You can collect into boxed slice. Though I do not think that for zeroes this is the best idea, this playground shows that optimizer does not use alloc_zeroed for core::iter::repeat(0).take(len).collect(), but does for vec![0; len].into_boxed_slice().

Interesting. There is no special case for alloc_zeroed directly in the macro, but if you go down to what it calls you will find it: alloc::vec::from_elem then alloc::vec::spec_from_elem::SpecFromElem::from_elem, specifically its specialization for u8 has the special case. It is also present for i8 and for a bunch of types implementing alloc::vec::is_zero::IsZero (which includes both i8 and u8, byte types just have extra optimization for non-zero case). And I thought it is from some of the optimization passes.

Going through Vec is the correct way.

Demo: https://rust.godbolt.org/z/GYK1oYMbj

You'll see that it's just a call to __rust_alloc_zeroed plus the error handling (for things like alloc_bytes(usize::MAX) -- anything more than isize::MAX bytes is always disallowed -- and the possibility of the allocator returning an error).

Or, for a more obvious demonstration that it's not adding overhead, try

#[no_mangle]
pub fn alloc_bytes(len: std::num::NonZeroU16) -> Box<[u8]> {
   vec![0; len.get().into()].into_boxed_slice()
}

so that the optimizer knows it doesn't need to worry about the "don't allocate for len == 0" case and the "what if the length overflows isize case", and you get the very simple https://rust.godbolt.org/z/rqse6Ezsj

alloc_bytes:
        push    rbx
        movzx   ebx, di
        mov     esi, 1
        mov     rdi, rbx
        call    qword ptr [rip + __rust_alloc_zeroed@GOTPCREL]
        test    rax, rax
        je      .LBB0_2
        mov     rdx, rbx
        pop     rbx
        ret
.LBB0_2:
        mov     edi, 1
        mov     rsi, rbx
        call    qword ptr [rip + alloc::raw_vec::handle_error@GOTPCREL]
2 Likes

Note that this is unsound. From the GlobalAlloc safety requirements,

This function is unsafe because undefined behavior can result if the caller does not ensure that layout has non-zero size.

2 Likes

Will making len non-zero fix it?