如何在Rust中从文件中读取一个结构体?

33

有没有办法在Rust中直接从文件中读取结构体?我的代码如下:

use std::fs::File;

struct Configuration {
    item1: u8,
    item2: u16,
    item3: i32,
    item4: [char; 8],
}

fn main() {
    let file = File::open("config_file").unwrap();

    let mut config: Configuration;
    // How to read struct from file?
}

我该如何将配置直接从文件中读取到config中?这是否可能?


1
你的文件是哪种格式?正确答案很大程度上取决于文件中实际数据的表示方式。 - Vladimir Matveev
3
二进制格式,我不希望从文件中读取并复制到我的结构体中;我想使用我的结构体作为缓冲区来读取文件。 - Jeroen
啊,我现在明白你需要什么了。你不能没有一些不安全的代码来完成它。我现在会尝试写一个概念证明。 - Vladimir Matveev
这个 crate 看起来正好符合你的需求:https://github.com/TyOverby/bincode - onionjake
3个回答

18

给你:

use std::io::Read;
use std::mem;
use std::slice;

#[repr(C, packed)]
#[derive(Debug, Copy, Clone)]
struct Configuration {
    item1: u8,
    item2: u16,
    item3: i32,
    item4: [char; 8],
}

const CONFIG_DATA: &[u8] = &[
    0xfd, // u8
    0xb4, 0x50, // u16
    0x45, 0xcd, 0x3c, 0x15, // i32
    0x71, 0x3c, 0x87, 0xff, // char
    0xe8, 0x5d, 0x20, 0xe7, // char
    0x5f, 0x38, 0x05, 0x4a, // char
    0xc4, 0x58, 0x8f, 0xdc, // char
    0x67, 0x1d, 0xb4, 0x64, // char
    0xf2, 0xc5, 0x2c, 0x15, // char
    0xd8, 0x9a, 0xae, 0x23, // char
    0x7d, 0xce, 0x4b, 0xeb, // char
];

fn main() {
    let mut buffer = CONFIG_DATA;

    let mut config: Configuration = unsafe { mem::zeroed() };

    let config_size = mem::size_of::<Configuration>();
    unsafe {
        let config_slice = slice::from_raw_parts_mut(&mut config as *mut _ as *mut u8, config_size);
        // `read_exact()` comes from `Read` impl for `&[u8]`
        buffer.read_exact(config_slice).unwrap();
    }

    println!("Read structure: {:#?}", config);
}

在这里试试(已更新至 Rust 1.38)

然而,需要注意的是,不安全的代码是不安全的。在 slice::from_raw_parts_mut() 调用后,同时存在两个可变句柄引用同一数据,这违反了 Rust 的别名规则。因此,您应该尽可能短地保留从结构创建的可变切片。我还假设您知道字节序问题 - 上面的代码绝不是可移植的,并且如果在不同类型的机器上编译和运行将返回不同的结果(例如 ARM vs x86)。

如果您可以选择格式并希望使用紧凑的二进制格式,请考虑使用bincode。否则,如果需要解析某些预定义的二进制结构,byteorder crate 是正确的方式。


是的,我知道字节序问题 - 但这只是一个快速工具,我正在编写它将在大约3台计算机上运行。 - Jeroen
1
@A.B.,我相信是这个。现在它的位置在这里 - Vladimir Matveev
1
我选择了 mem::uninitialized 而不是 mem::zeroed。如果内存最终会被覆盖,那么将内存初始化为0似乎没有什么意义。 - Jeroen
这给了我一个“警告,此警告将成为错误”的消息,https://github.com/rust-lang/rust/issues/46043 - don bright
有趣。我完全不记得我是怎么想出这些值的,但似乎不太可能是手写的... - Vladimir Matveev
显示剩余2条评论

12

Vladimir Matveev提到的,使用byteorder crate通常是最好的解决方案。这样做可以解决字节序问题,避免处理不安全的代码,也不用担心对齐或填充问题:

use byteorder::{LittleEndian, ReadBytesExt}; // 1.2.7
use std::{
    fs::File,
    io::{self, Read},
};

struct Configuration {
    item1: u8,
    item2: u16,
    item3: i32,
}

impl Configuration {
    fn from_reader(mut rdr: impl Read) -> io::Result<Self> {
        let item1 = rdr.read_u8()?;
        let item2 = rdr.read_u16::<LittleEndian>()?;
        let item3 = rdr.read_i32::<LittleEndian>()?;

        Ok(Configuration {
            item1,
            item2,
            item3,
        })
    }
}

fn main() {
    let file = File::open("/dev/random").unwrap();

    let config = Configuration::from_reader(file);
    // How to read struct from file?
}

出于以下几个原因,我忽略了[char; 8]

  1. Rust的char是32位类型,而您的文件是否具有实际Unicode代码点或C风格的8位值不清楚。
  2. 您无法轻松地使用byteorder解析数组,您必须先解析N个值,然后自己构建数组。

我猜这些 read_u8 和其他的 read_X 调用可能会调用系统调用。所以它可能不是非常高效。我们能否按照某种字节顺序读取整个结构,而不是读取整数类型的小部分? - Victor Polevoy
1
@VictorPolevoy 这是缓冲读取器的工作,需要修复。请参见在Rust 1.x中读写文件的事实标准方式是什么?,从“缓冲I/O”开始。但是,是的,您可以不安全地将任何随机字节块转换为任何给定类型。这就是其他两个答案的重点。 - Shepmaster
如果我想读取10GB的文件怎么办?性能会受到很大影响。在我看来,使用from_raw_parts是唯一的方法。 - mishmashru
2
@mishmashru,我并没有立即看出为什么这比from_raw_parts的性能要差。这不是你需要有意见的事情。编写两者并进行基准测试 - 然后你就会确切地知道。 - Shepmaster

6
以下代码没有考虑任何字节序或填充问题,旨在与POD类型一起使用。在这种情况下,结构体Configuration应该是安全的。请参见endiannesspadding以了解更多信息。POD types
这是一个可以从文件中读取POD类型结构体的函数: ```C++ ```
use std::io::{self, Read};
use std::slice;

fn read_struct<T, R: Read>(mut read: R) -> io::Result<T> {
    let num_bytes = ::std::mem::size_of::<T>();
    unsafe {
        let mut s = ::std::mem::uninitialized();
        let buffer = slice::from_raw_parts_mut(&mut s as *mut T as *mut u8, num_bytes);
        match read.read_exact(buffer) {
            Ok(()) => Ok(s),
            Err(e) => {
                ::std::mem::forget(s);
                Err(e)
            }
        }
    }
}

// use
// read_struct::<Configuration>(reader)

如果您想从文件中读取一系列结构体,可以多次执行read_struct或一次性读取整个文件:
use std::fs::{self, File};
use std::io::BufReader;
use std::path::Path;

fn read_structs<T, P: AsRef<Path>>(path: P) -> io::Result<Vec<T>> {
    let path = path.as_ref();
    let struct_size = ::std::mem::size_of::<T>();
    let num_bytes = fs::metadata(path)?.len() as usize;
    let num_structs = num_bytes / struct_size;
    let mut reader = BufReader::new(File::open(path)?);
    let mut r = Vec::<T>::with_capacity(num_structs);
    unsafe {
        let buffer = slice::from_raw_parts_mut(r.as_mut_ptr() as *mut u8, num_bytes);
        reader.read_exact(buffer)?;
        r.set_len(num_structs);
    }
    Ok(r)
}

// use
// read_structs::<StructName, _>("path/to/file"))

为什么使用 ::std::mem... 而不是 std::mem?它们有区别吗? - wingerse
4
:: 开头的路径是绝对路径。使用绝对路径可以确保代码在将函数放入模块时能够编译通过。在 https://doc.rust-lang.org/book/crates-and-modules.html 中搜索“absolute”以了解更多信息。 - malbarbo
1
谢谢malbarbo。 - wingerse
1
@Knight 为了防止析构函数在s(未初始化的)上运行。这是forget文档中描述的一个用例。 - malbarbo
1
虽然这个答案提到了潜在的问题,但它不当地使用了不安全的 Rust。所提出的函数可能会在安全的 Rust 代码中引入内存不安全性。一个例子显示它导致了段错误。这段代码不应该被使用。 - Shepmaster
显示剩余2条评论

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