Is there a better (more idiomatic?) implementation for converting a slice of bytes to a typed value?

Hello!

I'm looking for a better (more idiomatic) way to implement a method that converts raw bytes into a typed value. This conversion depends on known type information (and data type size) and endianness. In my implementation I have a struct that holds the endianness, type information and has access to the raw bytes, for example something like this:

struct MyData {
    pub typename: CTypes,
    pub endianness: Endianness,
    pub bytes: [u8; 8], // for illustration purposes. real implementation reads bytes from a buffer
}

// supporting enums
enum CTypes { UInt8, UInt16, UInt32, Int8, Int16, Int32, Float32, Float64 }; 
enum Endianness { Big, Little, Native };

I have an enum to hold the typed value:

enum CValue{
    UInt8(u8),
    UInt16(u16),
    Uint32(u32),
    Uint64(u64),
    Int8(i8),
    Int16(i16),
    int32(i32),
    int64(i64),
    Float32(f32),
    Float64(f64),
}

The get() method below does the conversion of a &[u8] slice into the appropriate typed value. My current implementation is defined something like the following. IMO this implementation feels clunky and ugly(?). You can see that the same basic code is written three time, one for each of the Endianness options, then in each of those 3 basic "copies", there's the conversion of bytes to a typed value. I wonder if there's a more idiomatic / concise way of expressing the same functionality. Any ideas or thoughts?

pub fn get(&self) -> Option<CValue> {
    let size = self.typename.type_size(); // returns number of bytes in the given type
    let buf = &self.bytes[0..size];
    let val = match self.endianness {
        Endianness::Big => match self.typename {
            CTypes::UInt8 => CValue::UInt8(u8::from_be_bytes(buf.try_into().unwrap())),
            CTypes::UInt16 => CValue::UInt16(u16::from_be_bytes(buf.try_into().unwrap())),
            CTypes::UInt32 => CValue::UInt32(u32::from_be_bytes(buf.try_into().unwrap())),
            CTypes::Int8 => CValue::Int8(i8::from_be_bytes(buf.try_into().unwrap())),
            CTypes::Int16 => CValue::Int16(i16::from_be_bytes(buf.try_into().unwrap())),
            CTypes::Int32 => CValue::Int32(i32::from_be_bytes(buf.try_into().unwrap())),
            // ...and so on
        }
        Endianness:Little => match self.typename {
            CTypes::UInt8 => CValue::UInt8(u8::from_le_bytes(buf.try_into().unwrap())),
            CTypes::UInt16 => CValue::UInt16(u16::from_le_bytes(buf.try_into().unwrap())),
            CTypes::UInt32 => CValue::UInt32(u32::from_le_bytes(buf.try_into().unwrap())),
            CTypes::Int8 => CValue::Int8(i8::from_le_bytes(buf.try_into().unwrap())),
            CTypes::Int16 => CValue::Int16(i16::from_le_bytes(buf.try_into().unwrap())),
            CTypes::Int32 => CValue::Int32(i32::from_le_bytes(buf.try_into().unwrap())),
            // ...and so on        
        },
       // for Native endianness, use `u8::from_ne_bytes()`, `u16::from_ne_bytes()`, etc
    };
    Some(val)
}

Thanks!

Edit: code indentation

I'm not really an expert on this, but what you're doing looks right. Using a more general solution would likely run into UB problems when you account for padding and setting the enum variant tag. That's why crates like abomonation have warnings plastered all over them.

Unless you've got a performance issue or a need to do this with non-numeric types, I would leave it as is.

1 Like

You might want to write a macro to make that easier. Not entirely sure how that would work, I'm not too familiar with macros, but I suspect they could cut down on the code duplication quite a bit.

1 Like

I would recommend looking into something like the byteorder crate.

1 Like

You may also want to take a look at @joshlf's zerocopy crate, which can automate the process of "casting" complex structures to and from slices of bytes without copies. It's slightly higher level than what you're doing, but maybe it will serve your higher-level need.

3 Likes