如果我将Vec :: from_raw_parts调用的容量小于指针的实际容量,会发生什么?

sgl*_*div 7 rust

我有一个u8我想要解释为矢量的矢量u32.假设字节顺序正确.我不想在转换后分配新内存和复制字节.我得到以下工作:

use std::mem;

fn reinterpret(mut v: Vec<u8>) -> Option<Vec<u32>> {
    let v_len = v.len();
    v.shrink_to_fit();
    if v_len % 4 != 0 {
        None
    } else {
        let v_cap = v.capacity();
        let v_ptr = v.as_mut_ptr();
        println!("{:?}|{:?}|{:?}", v_len, v_cap, v_ptr);
        let v_reinterpret = unsafe { Vec::from_raw_parts(v_ptr as *mut u32, v_len / 4, v_cap / 4) };
        println!("{:?}|{:?}|{:?}",
                 v_reinterpret.len(),
                 v_reinterpret.capacity(),
                 v_reinterpret.as_ptr());
        println!("{:?}", v_reinterpret);
        println!("{:?}", v); // v is still alive, but is same as rebuilt
        mem::forget(v);
        Some(v_reinterpret)
    }
}

fn main() {
    let mut v: Vec<u8> = vec![1, 1, 1, 1, 1, 1, 1, 1];
    let test = reinterpret(v);
    println!("{:?}", test);
}
Run Code Online (Sandbox Code Playgroud)

但是,这里有一个明显的问题.从shrink_to_fit文档:

它将尽可能接近长度下降,但分配器仍然可以通知向量有多个元素的空间.

这是否意味着我的容量可能仍然不是u32调用后的大小的倍数shrink_to_fit?如果from_raw_parts我设置能力v_len/4v.capacity()不是4的整数倍,我漏电,这些1-3个字节,或者他们将返回到由于内存池mem::forgetv

我在这里还有其他问题吗?

我认为v进行重新解释可以保证从那时起无法访问,因此从mem::forget(v)呼叫开始只有一个所有者.

dab*_*oss 4

这是一个老问题,评论中似乎有一个可行的解决方案。我刚刚写下了这里到底出了什么问题,以及人们可能在今天的 Rust 中创建/使用的一些解决方案。

这是未定义的行为

Vec::from_raw_parts是一个不安全的函数,因此您必须满足其不变量,否则您将调用未定义的行为

引用自文档Vec::from_raw_parts

  • ptr需要事先通过 String/Vec 分配(至少,如果不是的话,很可能是不正确的)。
  • T需要与分配的 ptr 具有相同的大小和对齐方式。(对齐不太严格是不够的,对齐确实需要等于满足释放内存的要求,即内存必须使用相同的布局进行分配和释放。)
  • 长度需要小于或等于容量。
  • 容量必须是分配给指针的容量。

所以,为了回答你的问题,如果capacity不等于原始 vec 的容量,那么你就打破了这个不变量。这会给你带来未定义的行为。

但请注意,这两者都没有要求size_of::<T>() * capacity,这让我们进入下一个主题。

我在这里还忽略了其他问题吗?

三件事。

首先,所编写的函数忽略了 的另一个要求from_raw_parts。具体来说,T必须具有与原件相同的对齐尺寸Tu32是 的四倍u8,所以这再次打破了这个要求。即使capacity*size保持不变,size也不是,也不capacity是。这个功能在实现时永远不会健全。

其次,即使上述所有内容都有效,您也忽略了对齐。u32必须与 4 字节边界对齐,而 aVec<u8>只能保证与 1 字节边界对齐。

OP 的评论提到:

我认为在 x86_64 上,未对齐会带来性能损失

值得注意的是,虽然这对于机器语言来说可能是正确的,但对于 Rust 来说却并非如此。rust 参考明确指出“对齐 n 的值只能存储在 n 的倍数的地址处”。这是一个硬性要求。

为什么有确切的类型要求?

Vec::from_raw_parts看起来好像很严格,这是有原因的。在 Rust 中,分配器 API 不仅对分配大小进行操作,还对 a 进行操作Layout,a 是大小、事物数量和各个元素对齐的组合。在 C 中memalloc,分配器可以依赖的只是大小相同,以及一些最小对齐。但在 Rust 中,允许依赖整个Layout,如果不依赖则调用未定义的行为。

因此,为了正确地释放内存,Vec需要知道分配内存的确切类型。通过将 a 转换Vec<u32>Vec<u8>,它不再知道此信息,因此它无法再正确地释放此内存。

替代方案 - 转换切片

Vec::from_raw_parts的严格性来自于它需要释放内存。相反,如果我们创建一个借用切片,&[u32]我们就不再需要处理它了!&[u8]将 a转为时没有能力&[u32],所以我们应该一切都好,对吧?

嗯,差不多了。您仍然需要处理对齐问题。基元通常与其大小对齐,因此 a[u8]只能保证与 1 字节边界对齐,而[u32]必须与 4 字节边界对齐。

不过,如果你想碰碰运气,并创建一个[u32] if possible,那么有一个函数可以实现这一点 - <[T]>::align_to

pub unsafe fn align_to<U>(&self) -> (&[T], &[U], &[T])
Run Code Online (Sandbox Code Playgroud)

这将修剪任何开始和结束未对齐的值,然后在新类型的中间给您一个切片。这是不安全的,但您需要满足的唯一不变量是中间切片中的元素有效。

将 4 个u8值重新解释为一个u32值是合理的,所以我们很好。

将它们放在一起,原始函数的声音版本将如下所示。这对借用的值而不是拥有的值进行操作,但考虑到重新解释拥有的值Vec在任何情况下都是即时未定义的行为,我认为可以肯定地说这是最接近的声音函数:

use std::mem;

fn reinterpret(v: &[u8]) -> Option<&[u32]> {
    let (trimmed_front, u32s, trimmed_back) = unsafe { v.align_to::<u32>() };
    if trimmed_front.is_empty() && trimmed_back.is_empty() {
        Some(u32s)
    } else {
        // either alignment % 4 != 0 or len % 4 != 0, so we can't do this op
        None
    }
}

fn main() {
    let mut v: Vec<u8> = vec![1, 1, 1, 1, 1, 1, 1, 1];
    let test = reinterpret(&v);
    println!("{:?}", test);
}
Run Code Online (Sandbox Code Playgroud)

请注意,这也可以使用std::slice::from_raw_parts而不是 来完成align_to。然而,这需要手动处理对齐,而它真正提供的只是我们需要确保我们做得正确的更多东西。好吧,这一点以及与旧编译器的兼容性 -align_to是在 2018 年的Rust 1.30.0中引入的,当提出这个问题时,它并不存在。

替代方案 - 复印

如果您确实需要Vec<u32>长期数据存储,我认为最好的选择是分配新内存。无论如何,旧的内存都分配给了u8s,并且不起作用。

通过一些函数式编程可以使这变得相当简单:

fn reinterpret(v: &[u8]) -> Option<Vec<u32>> {
    let v_len = v.len();
    if v_len % 4 != 0 {
        None
    } else {
        let result = v
            .chunks_exact(4)
            .map(|chunk: &[u8]| -> u32 {
                let chunk: [u8; 4] = chunk.try_into().unwrap();
                let value = u32::from_ne_bytes(chunk);
                value
            })
            .collect();
        Some(result)
    }
}
Run Code Online (Sandbox Code Playgroud)

首先,我们用来<[T]>::chunks_exact迭代4 u8s 块。接下来,try_into从 转换&[u8][u8; 4]. 保证长度&[u8]为 4,因此永远不会失败。

我们使用本机字节u32::from_ne_bytes序将字节转换为u32字节序。如果与网络协议或磁盘序列化交互,那么使用from_be_bytesfrom_le_bytes可能更合适。最后,我们collect将结果转回Vec<u32>.

最后一点,真正通用的解决方案可能会同时使用这两种技术。如果我们将返回类型更改为Cow<'_, [u32]>,我们可以返回对齐的借用数据(如果有效),如果无效则分配一个新数组!虽然不是两全其美,但也很接近。