用于低级数据结构的Bitfield和union以及Rust中的类型转换

Nic*_*ult 3 casting unions rust

我需要管理位域数据和联合.这是我认为在C中的代码:

typedef struct __attribute__((__packed__)){
    union {
        struct __attribute__((__packed__)){
            unsigned short protocol : 4;
            unsigned short target : 12;
            unsigned short target_mode : 4;
            unsigned short source : 12;
            unsigned char cmd;
            unsigned char size;
        };
        unsigned char unmap[6]; // Unmapped form.
    };
}header_t;
Run Code Online (Sandbox Code Playgroud)

我使用此联合可以轻松地从映射到未映射的表单切换.我可以写入header_t.protocol或者使用header_t.source它作为u8数组返回header_t.unmap.此开关不使用时间并共享相同的内存块.

我试着在Rust中做同样的事情,但我找不到干净的方法来做到这一点.我成功地使用了两个结构并专门impl在它们之间切换:

#[allow(dead_code)]
pub struct Header {
    protocol:    u8,  // 4 bits used
    target:      u16, // 12 bits used
    target_mode: u8,  // 4 bits used
    source:      u16, // 12 bits used
    cmd:         u8,  // 8 bits used
    size:        u8,  // 8 bits used
}

#[allow(dead_code)]
pub struct UnmapHeader{
    tab:[u8; 6],
}

impl Header {
    #[allow(dead_code)]
    pub fn unmap(&self) -> UnmapHeader {
        let mut unmap_header = UnmapHeader { tab: [0; 6],};
        unmap_header.tab[0] = (self.protocol & 0b0000_1111) | (self.target << 4) as u8;
        unmap_header.tab[1] = (self.target >> 4) as u8;
        unmap_header.tab[2] = ((self.target_mode as u8) & 0b0000_1111) | (self.source << 4) as u8;
        unmap_header.tab[3] = (self.source >> 4) as u8;
        unmap_header.tab[4] = self.cmd;
        unmap_header.tab[5] = self.size;
        unmap_header
    }
}

impl UnmapHeader {
    #[allow(dead_code)]
    pub fn map(&self) -> Header {
        Header{
        protocol: self.tab[0] & 0b0000_1111,
        target: ((self.tab[0] & 0b1111_0000) >> 4) as u16 & (self.tab[1] << 4) as u16,
        target_mode: self.tab[2] & 0b0000_1111,
        source: ((self.tab[2] & 0b1111_0000) >> 4) as u16 & (self.tab[3] << 4) as u16,
        cmd: self.tab[4],
        size: self.tab[5],
        }
    }
}

#[test]
fn switch() {
    let header = Header {
        protocol: 0b0000_1000,
        target: 0b0000_0100_0000_0001,
        target_mode: 0b0000_0100,
        source: 0b0000_0100_0000_0001,
        cmd: 0xAA,
        size: 10,
    };
    let unmap_header = header.unmap();
    assert_eq!(unmap_header.tab[0], 0b0001_1000);
    assert_eq!(unmap_header.tab[1], 0b0100_0000);
    assert_eq!(unmap_header.tab[2], 0b0001_0100);
    assert_eq!(unmap_header.tab[3], 0b0100_0000);
    assert_eq!(unmap_header.tab[4], 0xAA);
    assert_eq!(unmap_header.tab[5], 10);
}
Run Code Online (Sandbox Code Playgroud)

是否有更惯用的Rust解决方案?

Pet*_*all 5

Rust(从最近开始)支持C风格的联合.但是,unsafe如果您不必与C联合进行交互,则联合需要一个块,并且对于纯Rust代码不是惯用的.

一种方法是将您的基础数据建模为a [u8; 6],然后提供更友好的访问器功能:

pub struct Header {
    tab: [u8; 6],
}

impl Header {
    pub fn get_protocol(&self) -> u8 {
        self.tab[0] & 0b0000_1111
    }

    pub fn set_protocol(&mut self, value: u8) {
        self.tab[0] = self.tab[0] & 0b1111_0000 | value & 0b0000_1111;
    }

    // etc..
}
Run Code Online (Sandbox Code Playgroud)

正如您在其中一个问题注释中提到的那样,您可以使用位域包来保持代码更简单.

另一种方法可能是使用单个字段定义结构,但转换为[u8; 6].正如你所呈现的那样,这些字段比空间占用的空间更多[u8; 6],因此无需方便的转换(例如不安全std::mem::transmute),无需为每个单独的字段移动位.所以上面的解决方案可能更好.

无论基本表示如何,在这种情况下定义友好访问器可能是一个好主意.这是一个免费的抽象,它可以让你稍后改变你对表示的看法,而不必改变它的使用方式.

  • 要在set_protocol函数中保留其他值,您应该添加一些掩码`self.tab [0] =(self.tab [0]&0b1111_0000)| (值&0b0000_1111);` (2认同)