我可以将一个字节数组反序列化成一个结构体吗?

17

我正在从套接字读取一系列字节,我需要将每个长度为n字节的片段作为一个结构体的项。

use std::mem;

#[derive(Debug)]
struct Things {
    x: u8,
    y: u16,
}

fn main() {
    let array = [22 as u8, 76 as u8, 34 as u8];
    let foobar: Things;
    unsafe {
        foobar = mem::transmute::<[u8; 3], Things>(array);
    }

    println!("{:?}", foobar);

}

我收到了错误提示,说foobar是32位的,而array是24位的。那么foobar不应该是24位的吗(8+16=24)?

4个回答

18

问题在于y字段是16位对齐的。因此,您的内存布局实际上是这样的:

问题在于y字段是16位对齐的。因此,您的内存布局实际上是

x
padding
y
y
请注意,交换xy的顺序并没有帮助,因为Rust结构体的内存布局实际上是未定义的(因此仍然是32位,没有其他原因,只是为了编译器简单)。如果您依赖它,您将获得未定义的行为。
对齐的原因在Purpose of memory alignment中解释。
您可以通过添加属性repr(packed)来防止对齐发生,但您将失去性能和获取字段引用的能力:
#[repr(packed)]
struct Things {
    x: u8,
    y: u16,
}

最好不要使用 transmute,而是手动提取值,并希望优化器使其更快:

let foobar = Things {
    x: array[0],
    y: ((array[1] as u16) << 8) | (array[2] as u16),
};

byteorder 这样的板条箱可以简化从字节中读取不同大小和字节序的过程。


在一个结构体中,每个元素之间是否总是有8位的填充? - Fluffy
1
@Fluffy 不一定。如果 x 也是 u16,则不需要填充。如果 x 仍然是 u8,并且在 xy 之间有一个 x2 字段,该字段也是 u8,则仍然不需要填充。你明白为什么吗?(请注意,无论语言如何,您都可以查找结构体对齐方式 - 原因始终相同。) - Theodoros Chatzigiannakis
3
如果所有字段的大小相同,那么使用u8并不会更快。原因是u8的对齐方式是8位,而u16的对齐方式是16位。这样就在它们之间创建了必要的8位空间。您可以选择自己使用这个空间(通过将第一个字段扩展到16位或引入一个中间的8位字段),也可以将其保留未使用(称为填充),但无论哪种方式,编译器都将把8位空间放在那里,无论您是否打算将其命名为字段。(除非您明确要求打包结构体,如答案中所述。) - Theodoros Chatzigiannakis
@Fluffy 也许重新排列字段可能在某些情况下会给您不同的大小,但在这个特定情况下可能不会(因为即使您将它们交换,我仍然希望编译器在结尾引入填充,以确保您的结构体数组对齐)。我不知道是否有任何算法可以在您的头脑中手动压缩结构体而不损失速度。通常这是一个直觉过程,但有一些资源可以帮助您获得一个大致的想法。请参见http://www.catb.org/esr/structure-packing/ - Theodoros Chatzigiannakis
4
即使你设法解决了对齐问题(这是不太可能的),你仍然会遇到字节顺序问题,这在需要通过网络传输数据时非常重要。将字节数组重新解释为结构体本质上是不可移植的。这就是为什么有很多序列化格式可用的原因。 - Vladimir Matveev
显示剩余6条评论

4

bincode和serde可以非常简单地实现此功能。

use bincode::{deserialize};
use serde::{Deserialize};

#[derive(Deserialize, Debug)]
struct Things {
    x: u8,
    y: u16,
}

fn main() {
    let array = [22 as u8, 76 as u8, 34 as u8];
    let foobar: Things = deserialize(&array).unwrap();
    println!("{:?}", foobar);
}

这同样适用于将结构体序列化为字节的情况。
use bincode::{serialize};
use serde::{Serialize};

#[derive(Serialize, Debug)]
struct Things {
    x: u8,
    y: u16,
}

fn main() {
    let things = Things{
        x: 22,
        y: 8780,
    };
    let baz = serialize(&things).unwrap();
    println!("{:?}", baz);

}

3

在处理同时具有字符数组的结构体时,我使用byteorder crate遇到了问题。我无法通过编译器错误。最终我这样进行强制类型转换:

#[repr(packed)]
struct Things {
    x: u8,
    y: u16,    
}

fn main() {
    let data: [u8; 3] = [0x22, 0x76, 0x34];
    
    unsafe {
        let things_p: *const Things = data.as_ptr() as *const Things;
        let things: &Things = &*things_p;
        
        println!("{:x} {:x}", things.x, things.y);
    }
}

请注意,使用 packed 会出现以下警告:
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!

如果可以的话,将Things更改为类似于C结构体的行为:
#[repr(C)]
struct Things2 {
    x: u8,
    y: u16,    
}

然后像这样初始化data。请注意,为了对齐目的,多了一个字节。
let data: [u8; 4] = [0x22, 0, 0x76, 0x34];

-2
use std::mem;

fn main() {
    let bytes = vec!(0u8, 1u8,2u8, 3, 4, 5, 6, 7, 8, 9, 0xffu8, );


    let data_ptr: *const u64 = unsafe { mem::transmute(bytes[0..4].as_ptr()) };

    let data: u64 = unsafe { *data_ptr };

    println!("{:#x}", data);
}

请注意,这与 SO 上许多其他答案存在相同的问题:它没有考虑对齐、目标类型中的潜在填充(对于 u64 不是问题),或者字节序。 - Shepmaster
不需要在指针上使用transmute,只需将指针转换即可。这将防止指针和指向指针的指针之间的意外转换,因为强制转换在某种程度上是类型检查的。 - oli_obk
正如其他人所说,这并不被推荐,但这正是OP所要求的,并且在技术上是正确的。如果有人想要走这条路,还需要注意字节序和其他细节。 - dpc.pw

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接