更 Rustic 的方式摄取二进制文件的数据类型描述,然后指导如何将解析的 &[u8] 转换为 Rust 可以使用的类型?



我认为我的解决方案比它应该的更复杂,但我对Rust还不够熟悉,不知道更好的方法。以下是我经过多次尝试和错误后得到的唯一解决方案。

Rust Playground


上下文

  1. 从描述二进制文件的记录结构的XML文件中读取。在下面的工作代码中跳过了这一步骤,并对其进行了简单的模拟。请参阅代码块中的注释
  2. 每个记录提供的字段之一是CCD_ 1描述,其被存储为&str或String,具体取决于保存记录的Rust结构体的定义方式
  3. 使用XML描述信息从二进制文件中读取记录,以自动化所得到的解析&将[u8]强制转换为,以便以后可以使用。由于解析二进制文件不是重点,因此在下面的代码中也模拟了这些数据

简化的XML描述文件示例

假设更多的记录和更多的数据类型。这不是工作代码中使用的

use quick_xml;
let xml_desc = r#""
<Records>
<RecordDesc>
<name>Single</name>
<number>1</number>
<location unit="byte">0</location>
<data_type>IEEE754LSBSingle</data_type>
<length> unit="byte">4</length>
</RecordDesc>
<RecordDesc>
<name>Double</name>
<number>1</number>
<location unit="byte">5</location>
<data_type>IEEE754LSBDouble</data_type>
<length> unit="byte">8</length>
</RecordDesc>
</Records>
""#;
// quick_xml/serde used to get that string of xml into the rust records

#[derive(Serialize, Deserialize, Debug)]
pub struct Records {
pub records: Vec<Record>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Record {
...,
pub data_type: &str, //could also be String, but this example doesn't use this struct
...,
}

// For each record use data_type to cast into type rust can use

工作代码

第一个宏

创建将&mut&[u8]转换为特定的Rust等效类型。示例输出如first_example所示。

macro_rules! type_cast_function {
($func_name:ident, $endian:ident, $output_type:ty ) => {
fn $func_name(input: &mut &[u8]) -> $output_type {
let (int_bytes, _) = input.split_at(std::mem::size_of::<$output_type>());
<$output_type>::$endian(int_bytes.try_into().unwrap())
}
};
}

第二个宏

创建impl块,用于从DataTypes中的变量中解开每个特定值。示例输出如first_example所示。

macro_rules! create_unwrap_impl_for_type {
($unwrap_name:ident, $variant:path, $output_type:ty) => {
impl DataTypes { 
pub fn $unwrap_name(self) -> $output_type {
match self {
$variant(val) => val,
_ => panic!(),
}
}
}
};
}

为各种数据类型创建枚举

注:案例反映了xml_desc中的案例

#[derive(Debug)]
pub enum DataTypes {
// 4 Bytes
IEEE754LSBSingle(f32),
// 8 Bytes
IEEE754LSBDouble(f64),
}

第一个示例

匹配data_type: &str描述,并生成相关函数和impl块,用于展开要在其他地方使用的每个匹配的值。

fn first_example(){
// Simulated Data that would come from parsing the binary file
let mut data: &[u8] = &[172, 152, 111, 195];
let mut data2: &[u8] = &[172, 152, 111, 195, 117, 93, 133, 192];

// Simulated looping through records with different types
for dtype in ["IEEE754LSBSingle", "IEEE754LSBDouble"] {
match dtype {
"IEEE754LSBSingle" => {
create_unwrap_impl_for_type!(unwrap_le_f32,DataTypes::IEEE754LSBSingle,f32);
/* 
outputs:
impl DataTypes { 
pub fn unwrap_le_f32(self) -> f32 {
match self {
DataTypes::IEEE754LSBSingle(val) => val,
_ => panic!(),
}
}
}
*/
type_cast_function!(read_le_f32, from_le_bytes, f32);
/* 
outputs:
fn read_le_f32(input: &mut &[u8]) -> f32 {
let (int_bytes, _) = input.split_at(std::mem::size_of::<f32>());
f32::from_le_bytes(int_bytes.try_into().unwrap())
}
*/
let single = DataTypes::IEEE754LSBSingle(read_le_f32(&mut data)).unwrap_le_f32();
println!("First ExampletIEEE754LSBSingle {:?}",single);    
},
"IEEE754LSBDouble" => {
create_unwrap_impl_for_type!(unwrap_le_f64,DataTypes::IEEE754LSBDouble,f64);
/* 
outputs:
impl DataTypes { 
pub fn unwrap_le_f64(self) -> f64 {
match self {
DataTypes::IEEE754LSBDouble(val) => val,
_ => panic!(),
}
}
}
*/
type_cast_function!(read_le_f64, from_le_bytes, f64);
/* 
outputs:
fn read_le_f64(input: &mut &[u8]) -> f64 {
let (int_bytes, _) = input.split_at(std::mem::size_of::<f64>());
f64::from_le_bytes(int_bytes.try_into().unwrap())
}
*/
let double = DataTypes::IEEE754LSBDouble(read_le_f64(&mut data2)).unwrap_le_f64();
println!("First ExampletIEEE754LSBDouble {:?}",double);
},
_ => panic!(),
};


}
}

一个宏来管理所有宏

一个宏用于从其他宏创建函数和impl块。使上面的first_example和下面的second_example之间存在差异

macro_rules! generate_casting_extraction_functions {
($func_name:ident, $endian:ident, $unwrap_name:ident, $variant:path, $output_type:ty) => {
create_unwrap_impl_for_type!($unwrap_name, $variant, $output_type);
type_cast_function!($func_name, $endian, $output_type);
}
}

第二个示例

匹配data_type: &str描述,并生成相关函数和impl块,用于展开要在其他地方使用的每个匹配的值。

fn second_example(){
// Simulated Data that would come from parsing the binary file
let mut data: &[u8] = &[172, 152, 111, 195];
let mut data2: &[u8] = &[172, 152, 111, 195, 117, 93, 133, 192];

// Simulated looping through records with different types
for dtype in ["IEEE754LSBSingle", "IEEE754LSBDouble"] {
match dtype {
"IEEE754LSBSingle" => {
// Same output as first_example
generate_casting_extraction_functions!(read_le_f32_2, from_le_bytes,unwrap_le_f32_2,DataTypes::IEEE754LSBSingle,f32);
let single = DataTypes::IEEE754LSBSingle(read_le_f32_2(&mut data)).unwrap_le_f32_2();
println!("Second ExampletIEEE754LSBSingle {:?}",single);    
},
"IEEE754LSBDouble" => {
// Same output as first_example
generate_casting_extraction_functions!(read_le_f64_2, from_le_bytes,unwrap_le_f64_2,DataTypes::IEEE754LSBDouble,f64);
let double = DataTypes::IEEE754LSBDouble(read_le_f64_2(&mut data2)).unwrap_le_f64_2();
println!("Second ExampletIEEE754LSBDouble {:?}",double);
},
_ => panic!(),
};


}
}
fn main() {
first_example();
second_example();
}

下面是我要做的。

首先,您需要两个枚举:

  • 一个只有单元变体的字符串,它只能更好地表示data_type字符串,我将其称为data_type0
#[derive(Clone, Copy, Debug)]
enum DataKind {
// 4 bytes
IEEE754LSBSingle,
// 8 bytes
IEEE754LSBDouble,
...etc
}
  • 和一个将保存您从二进制文件解析的数据的文件
#[derive(Debug)]
enum DataTypes {
// 4 bytes
IEEE754LSBSingle(f32),
// 8 bytes
IEEE754LSBDouble(f64),
...etc
}

然后,您需要一个函数来解释目标类型所需的输入字节数,并将结果存储在相应的DataTypes值中:

impl DataKind {
fn parse(self, input: &mut &[u8]) -> DataTypes {
match self {
DataKind::IEEE754LSBSingle => DataTypes::IEEE754LSBSingle({
let (bytes, _) = input.split_at(std::mem::size_of::<f32>());
f32::from_le_bytes(bytes.try_into().unwrap())
}),
DataKind::IEEE754LSBDouble => DataTypes::IEEE754LSBDouble({
let (bytes, _) = input.split_at(std::mem::size_of::<f64>());
f64::from_le_bytes(bytes.try_into().unwrap())
}),
...etc
}
}
}

值得庆幸的是,使用宏可以很容易地同时生成所有这些:

macro_rules! generate_datatypes_parsing {
[$( $name:ident($target_type:ty => $conversion:ident) ),+ $(,)*] => {
#[derive(Clone, Copy, Debug)]
pub enum DataKind {
$( $name, )*
}

#[derive(Debug)]
pub enum DataTypes {
$( $name($target_type), )*
}

impl DataKind {
fn parse(self, input: &mut &[u8]) -> DataTypes {
match self {
$(
DataKind::$name => DataTypes::$name({
let (bytes, _) = input.split_at(
std::mem::size_of::<$target_type>()
);
<$target_type>::$conversion(bytes.try_into().unwrap())
}),
)*
}
}
}
};
}
generate_datatypes_parsing![
IEEE754LSBSingle(f32 => from_le_bytes),
IEEE754LSBDouble(f64 => from_le_bytes),
...etc
];

然后你可以像这样使用DataKind::parse

fn main() {
// Simulated Data that would come from parsing the binary file
let mut data: &[u8] = &[172, 152, 111, 195];
let mut data2: &[u8] = &[172, 152, 111, 195, 117, 93, 133, 192];

// parsing will eventually go in a loop somewhere
println!("First Examplet{:?}", DataKind::IEEE754LSBSingle.parse(&mut data));
println!("First Examplet{:?}", DataKind::IEEE754LSBDouble.parse(&mut data2));
}

操场

为什么选择DataKind

最好使用像DataKind这样的枚举,因为这样可以从编译器获得更多的保证。传递一个没有生存期的Copy枚举也比传递一个有一些生存期需要担心的&str要容易得多。

当然,您应该为DataKind使用#[derive(Deserialize)],这样serde就可以为您从&str进行转换。

返回Result

您可能希望从fn parse返回一个具有自定义错误类型的Result。如果您这样做,我建议您使用自定义的split_at函数,如果超出范围,该函数也会返回Result

相关内容

  • 没有找到相关文章