如何构造大小仅在运行时已知的 DST 结构实例?

问题描述 投票:0回答:1

我有一个可以以固定大小或不固定大小的方式使用的结构,其中最后一个元素是一个由

u8
项组成的数组。我们可以编写以下结构体定义:

struct MyStructImpl<E: ?Sized> {
    field: char,
    extendable: E,
}
type MyStructFixed<const N: usize> = MyStructImpl<[u8; N]>;
type MyStructUnsized = MyStructImpl<[u8]>;

如果我们在编译时知道大小,就可以获取该结构的未调整大小版本的句柄。例如:

impl MyStructUnsized {
    fn new_from_fixed_array<const N: usize>(field: char, arr: [u8; N]) -> Box<Self> {
        Box::new(MyStructFixed {
            field,
            extendable: arr,
        })
    }
}

但是,如果您只知道运行时的大小,我找不到构造结构的方法。例如:

impl MyStruct<[u8]> {
    fn new_from_slice(field: char, arr: &[u8]) -> Box<Self> {
        todo!(); // help!
    }
}

理论上这应该是可以做到的:我们只需要分配一些正确大小的空间,然后在其中构造结构体,最后将

u8
复制过来。
Box<T>
应该知道如何释放未调整大小的
T
,所以这应该不是问题。

然而,实际上,这在 Rust 中实现起来并不简单。 (这在 C 或 C++ 中非常简单,因为这些语言中的结构布局定义非常明确。)

std::boxed::Box
from_raw()
,它获取
*mut T
的所有权并将其放入 Box 中。可以通过调用
*mut T
来获取已分配的
std::alloc::alloc()
。然而,
std::alloc::alloc()
需要一个
std::alloc::Layout
,它本质上是一个大小和一个对齐方式,但是如果
MyStructFixed<N>
仅在运行时已知,那么似乎没有办法计算
N
的大小和对齐方式,因为常规
#[repr(Rust)]
类型的布局未指定。理论上,编译器应该知道它,因为它需要为 MyStructUnsized 的任何成员函数发出代码,因此具有以下签名的编译器内部函数的存在似乎是合理的:

fn get_layout<T: ?Sized>(len: usize) -> Layout;

但是我找不到这样的东西。

我认为这样的事情可能会起作用:

fn guess_required_layout(len: usize) -> Layout {
    Layout::from_size_align(
        std::mem::size_of::<MyStructFixed<0>>() + len * std::mem::size_of::<u8>(),
        std::mem::align_of::<MyStructFixed<0>>(),
    )
    .unwrap()
}

...但这对可扩展结构的布局做出了假设,这似乎并不能得到保证。

构造一个未调整大小的结构体的 Box 的惯用方法是什么,其大小仅在运行时才知道,最好使用最新的稳定 Rust(撰写本文时为 1.74.0)?

rust struct unsafe variable-length-array dynamically-sized-type
1个回答
0
投票

自定义 DST 是一个不成熟的功能。

在稳定版上,据我所知,唯一的方法是标记结构

#[repr(C)]
。然后你可以手动计算布局,如下所示:

use std::alloc::Layout;
use std::ptr::addr_of_mut;

#[repr(C)]
struct MyStructImpl<E: ?Sized> {
    field: char,
    extendable: E,
}
type MyStructFixed<const N: usize> = MyStructImpl<[u8; N]>;
type MyStructUnsized = MyStructImpl<[u8]>;

impl MyStructUnsized {
    fn new_from_iterator(field: char, arr: &[u8]) -> Box<Self> {
        let layout = Layout::new::<char>()
            .extend(Layout::for_value(arr))
            .unwrap()
            .0
            .pad_to_align();

        unsafe {
            let data = std::alloc::alloc(layout);
            if data.is_null() {
                std::alloc::handle_alloc_error(layout);
            }

            let data =
                std::ptr::slice_from_raw_parts_mut::<u8>(data, arr.len()) as *mut MyStructUnsized;
            addr_of_mut!((*data).field).write(field);
            addr_of_mut!((*data).extendable)
                .cast::<u8>()
                .copy_from_nonoverlapping(arr.as_ptr(), arr.len());

            Box::from_raw(data)
        }
    }
}

每晚,理论上你可以用

Layout::for_value_raw()
做得稍微好一点,但是,它的安全前提之一是整个东西的大小适合
isize
,并计算出它的大小以了解DST 的前缀,它是字段(已知,
char
的大小),也是填充,您可以通过整个结构的大小计算填充,但然后我们回到第一点,或者您可以计算它通过
offset_of!()
extendable
实现,但是
offset_of!()
不稳定,而 有板条箱可以在稳定版上实现它,这意味着引入依赖项......所以总之,我不确定这些麻烦是否值得它。

© www.soinside.com 2019 - 2024. All rights reserved.