跳转到主内容位标志和位域 | 物联网民工

位标志和位域

C 里位标志很常见:

#define FLAG_READ   (1u << 0)
#define FLAG_WRITE  (1u << 1)
#define FLAG_ERROR  (1u << 2)

uint32_t flags = FLAG_READ | FLAG_WRITE;

if (flags & FLAG_ERROR) {
    handle_error();
}

Rust 当然也能做位运算。但有两个习惯要先改:

C:    if (flags & FLAG_ERROR)
Rust: if flags & FLAG_ERROR != 0

Rust 的 if 条件必须是 bool。整数不会自动当作真假值。

位标志仍然可以用 const

C 里用宏定义 bit mask:

#define FLAG_READ   (1u << 0)
#define FLAG_WRITE  (1u << 1)
#define FLAG_ERROR  (1u << 2)

Rust 可以用 const:

const FLAG_READ: u32 = 1 << 0;
const FLAG_WRITE: u32 = 1 << 1;
const FLAG_ERROR: u32 = 1 << 2;

组合标志:

let flags = FLAG_READ | FLAG_WRITE;

判断某一位:

if flags & FLAG_ERROR != 0 {
    handle_error();
}

设置、清除、翻转也很直接:

let mut flags = 0_u32;

flags |= FLAG_READ;       // set
flags &= !FLAG_WRITE;     // clear
flags ^= FLAG_ERROR;      // toggle

注意 Rust 的按位取反是 !,不是 C 的 ~。

用函数或方法藏住位运算细节

裸 u32 到处传,和 C 里到处传 uint32_t flags 一样,容易把含义散掉。Rust 可以先用函数收口:

fn has_flag(flags: u32, flag: u32) -> bool {
    flags & flag != 0
}

if has_flag(flags, FLAG_READ) {
    read();
}

如果这是一个稳定的业务概念,可以用 newtype:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Permissions(u32);

impl Permissions {
    const READ: u32 = 1 << 0;
    const WRITE: u32 = 1 << 1;
    const EXECUTE: u32 = 1 << 2;

    fn empty() -> Self {
        Self(0)
    }

    fn from_bits(bits: u32) -> Self {
        Self(bits)
    }

    fn bits(self) -> u32 {
        self.0
    }

    fn contains(self, flag: u32) -> bool {
        self.0 & flag != 0
    }

    fn insert(&mut self, flag: u32) {
        self.0 |= flag;
    }

    fn remove(&mut self, flag: u32) {
        self.0 &= !flag;
    }
}

使用时:

let mut p = Permissions::empty();
p.insert(Permissions::READ);

if p.contains(Permissions::READ) {
    println!("can read");
}

这比到处写 flags & (1 << n) 更容易维护。外部仍然可以通过 bits() 拿到底层整数,用于文件、协议或 FFI。

多位字段用 mask 和 shift

C 里不只会用单 bit 标志,也会在一个整数里塞多位字段:

#define MODE_MASK   (0x3u << 4)
#define MODE_SHIFT  4

uint32_t mode = (reg & MODE_MASK) >> MODE_SHIFT;
reg = (reg & ~MODE_MASK) | ((new_mode << MODE_SHIFT) & MODE_MASK);

Rust 写法类似:

const MODE_MASK: u32 = 0x3 << 4;
const MODE_SHIFT: u32 = 4;

fn mode(reg: u32) -> u32 {
    (reg & MODE_MASK) >> MODE_SHIFT
}

fn set_mode(reg: u32, value: u32) -> u32 {
    (reg & !MODE_MASK) | ((value << MODE_SHIFT) & MODE_MASK)
}

如果值有范围,应该检查:

fn set_mode(reg: u32, value: u32) -> Option<u32> {
    if value > 0x3 {
        return None;
    }

    Some((reg & !MODE_MASK) | (value << MODE_SHIFT))
}

C 里很多代码靠调用方保证 value 不越界。Rust 里可以让函数签名把失败路径写出来。

C bit-field 不要直接照搬

C 有 bit-field:

struct Control {
    unsigned enable : 1;
    unsigned mode   : 2;
    unsigned error  : 1;
};

它看起来很适合硬件寄存器和协议头,但布局细节并不适合直接跨语言依赖。bit-field 的分配顺序、对齐、底层存储单元等都和编译器、ABI、目标平台有关。

Rust 没有稳定的 C 风格结构体位域语法。面对外部二进制布局时,更稳的做法是用明确宽度整数保存原始 bits,再用方法读写字段:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Control(u32);

impl Control {
    const ENABLE: u32 = 1 << 0;
    const MODE_MASK: u32 = 0x3 << 1;
    const MODE_SHIFT: u32 = 1;
    const ERROR: u32 = 1 << 3;

    fn from_bits(bits: u32) -> Self {
        Self(bits)
    }

    fn bits(self) -> u32 {
        self.0
    }

    fn enabled(self) -> bool {
        self.0 & Self::ENABLE != 0
    }

    fn mode(self) -> u32 {
        (self.0 & Self::MODE_MASK) >> Self::MODE_SHIFT
    }

    fn has_error(self) -> bool {
        self.0 & Self::ERROR != 0
    }
}

这比把 C bit-field 布局复制到 Rust 里更明确:外部数据是 u32,字段规则由 mask 和 shift 控制。

协议和文件格式要写清字节序

如果位字段来自网络包或文件,先把字节序处理清楚:

fn parse_control(data: &[u8]) -> Option<Control> {
    if data.len() < 4 {
        return None;
    }

    let raw = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
    Some(Control::from_bits(raw))
}

不要直接把字节数组强转成结构体位域。这里涉及:

  • 长度检查
  • 大端/小端
  • 对齐
  • padding
  • 编译器位域布局差异

显式解析虽然多几行,但规则不会藏在编译器布局里。

FFI 里按 C 头文件保持原始表示

如果 C API 接收的是整数 flags:

int open_device(uint32_t flags);

Rust FFI 声明也先按整数来:

unsafe extern "C" {
    fn open_device(flags: u32) -> i32;
}

Rust 内部可以用 newtype 封装:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct OpenFlags(u32);

impl OpenFlags {
    const READ: u32 = 1 << 0;
    const WRITE: u32 = 1 << 1;

    fn bits(self) -> u32 {
        self.0
    }
}

调用边界再取出 bits:

let flags = OpenFlags(OpenFlags::READ | OpenFlags::WRITE);
let rc = unsafe { open_device(flags.bits()) };

边界用 C 能理解的整数,内部用 Rust 能表达语义的类型。

这一层先记住什么

C 位标志和位域到 Rust 的第一层对应关系是:

#define FLAG (1u << n)       -> const FLAG: u32 = 1 << n
flags & FLAG as condition    -> flags & FLAG != 0
set / clear / toggle         -> |= / &= ! / ^=
C bit-field                  -> 明确宽度整数 + mask/shift 方法
协议/文件位字段              -> 先处理字节序,再封装字段访问
FFI flags                    -> ABI 边界用 u32,内部可用 newtype

Rust 没有取消位运算。它要求你把真假条件、字段宽度、字节序和布局边界写得更明确。对库 API 来说,这通常是好事:调用方看到的是 contains()、mode()、bits(),而不是一堆散落的 magic mask。

继续阅读