如何将“struct”转换为“&[u8]”?

57

我希望通过 TcpStream 发送我的结构体。我可以发送 Stringu8,但我无法发送任意结构体。例如:

struct MyStruct {
    id: u8,
    data: [u8; 1024],
}

let my_struct = MyStruct { id: 0, data: [1; 1024] };
let bytes: &[u8] = convert_struct(my_struct); // how??
tcp_stream.write(bytes);

收到数据后,我想将&[u8]转换回MyStruct。我该怎么在这两种表示之间进行转换呢?

我知道Rust有一个JSON模块可以用于序列化数据,但我不想使用JSON,因为我希望尽可能快地发送数据并保持小的开销。

3个回答

63

可以使用stdlib和通用函数来创建正确大小的结构体作为零拷贝字节。

下面的示例中有一个可重用的函数any_as_u8_slice,而不是convert_struct,因为这是一个实用程序,用于包装强制转换和切片创建。

请注意,问题询问的是转换,此示例创建了一个只读切片,因此具有不需要复制内存的优点。

以下是基于问题的工作示例:

unsafe fn any_as_u8_slice<T: Sized>(p: &T) -> &[u8] {
    ::core::slice::from_raw_parts(
        (p as *const T) as *const u8,
        ::core::mem::size_of::<T>(),
    )
}

fn main() {
    struct MyStruct {
        id: u8,
        data: [u8; 1024],
    }
    let my_struct = MyStruct { id: 0, data: [1; 1024] };
    let bytes: &[u8] = unsafe { any_as_u8_slice(&my_struct) };
    // tcp_stream.write(bytes);
    println!("{:?}", bytes);
}

注意1) 尽管在某些情况下第三方箱可能更好,但这是一种如此原始的操作,了解如何在Rust中执行此操作非常有用。

注意2) 在编写本文时(Rust 1.15),没有对const函数的支持。一旦支持,就可以将其转换为固定大小的数组而不是切片。

注意3) any_as_u8_slice函数被标记为unsafe,因为struct中的任何填充字节都可能是未初始化的内存(导致未定义的行为)。 如果有办法确保输入参数仅使用#[repr(packed)]的结构体,则可以使其安全。

否则,该函数是相当安全的,因为它防止缓冲区溢出,因为输出是只读的、固定数量的字节,并且其生命周期绑定到输入。
如果您想要返回一个&mut [u8]的版本,那么这将非常危险,因为修改可能会轻松地创建不一致/损坏的数据。


1
如答案所述:“它可以防止缓冲区溢出,因为输出是只读的、固定数量的字节,并且其生命周期绑定到输入。” - ideasman42
18
有没有一种方法可以反向操作,即将字节转换回结构体? - Lev
4
let s: MyStruct = unsafe { std::mem::transmute(*bytes) }; - d9ngle
非常棒的答案,谢谢!只有一个关于注释3的评论:该函数是不安全的,因为您正在使用任意内存片段来构建切片(这就是为什么from_raw_parts是不安全的),而不是因为填充字节。当您拥有字节数组时,填充字节只是普通字节,当您拥有结构体时,它们根本无法访问。 - ASLLOP
let s: MyStruct = unsafe { std::mem::transmute(*bytes) }; 是可行的,但我认为 let p: *const [u8; std::mem::size_of::<MyStruct>()] = bytes as *const [u8; std::mem::size_of::<MyStruct>()]; let s: MyStruct = unsafe { std::mem::transmute(*p) }; 也是不错的选择。 - lechat
显示剩余2条评论

30

(这是从类似问题的评论中 Shamelessly stolen and adapted from Renato Zannon 的评论 )

也许像bincode这样的解决方案适合你的情况? 这里是一个可行的摘录:

Cargo.toml

[package]
name = "foo"
version = "0.1.0"
authors = ["An Devloper <an.devloper@example.com>"]
edition = "2018"

[dependencies]
bincode = "1.0"
serde = { version = "1.0", features = ["derive"] }

main.rs

use serde::{Deserialize, Serialize};
use std::fs::File;

#[derive(Serialize, Deserialize)]
struct A {
    id: i8,
    key: i16,
    name: String,
    values: Vec<String>,
}

fn main() {
    let a = A {
        id: 42,
        key: 1337,
        name: "Hello world".to_string(),
        values: vec!["alpha".to_string(), "beta".to_string()],
    };

    // Encode to something implementing `Write`
    let mut f = File::create("/tmp/output.bin").unwrap();
    bincode::serialize_into(&mut f, &a).unwrap();

    // Or just to a buffer
    let bytes = bincode::serialize(&a).unwrap();
    println!("{:?}", bytes);
}

您随后就可以将字节发送到任何地方。我假设您已经意识到了朴素地发送字节时可能存在的问题(例如潜在的字节序或版本问题),但为了避免遗漏,我还是提一下吧 ^_^。


10
值得注意的是,这不是直接转换。虽然编码/解码使用了二进制格式,但这并不仅仅是访问结构体内存的操作(这可能被视为好事或坏事),这取决于你想要的内容,它执行了一些转换。例如,Bincode还执行了字节序转换。 - ideasman42
2
当您不关心性能时,请使用bincode。https://www.reddit.com/r/rust/comments/eg9cfm/it_seems_bincode_is_surprisingly_slow/ - محمد جعفر نعمة
这需要你的结构体派生出Serialize trait,所以对于未在你的crate中定义的结构体来说,并非总是可行的。 - jaques-sam

5
你可以使用 bytemuck crate 来安全地执行此操作:
#[derive(bytemuck::NoUninit, Clone, Copy)]
#[repr(C)]
struct MyStruct {
    id: u8,
    data: [u8; 1024],
}

let my_struct = MyStruct { id: 0, data: [1; 1024] };
let bytes: &[u8] = bytemuck::bytes_of(&my_struct);
tcp_stream.write(bytes);

请注意,这要求该结构体必须是Copy类型,并且要使用#[repr(C)]#[repr(transparent)]属性。


1
你可以使用bytemuck::bytes_of(..)来代替bytemuck::cast_slice(std::slice::from_ref(..))吗? - Johannes
@Johannes 你是对的。已编辑。 - Chayim Friedman

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