定义联合体

先看一个联合体的例子:

#![allow(non_camel_case_types)]
#![allow(clippy::module_name_repetitions)]
#![allow(dead_code)]

use std::ffi::c_void;

pub type poll_t = u32;

#[repr(C)]
#[derive(Default, Clone, Copy)]
pub struct epoll_event_t {
    pub events: poll_t,
    pub data: epoll_data_t,
}

#[repr(C)]
#[derive(Clone, Copy)]
pub union epoll_data_t {
    pub ptr: *const c_void,
    pub fd: i32,
    pub v_u32: u32,
    pub v_u64: u64,
}

impl Default for epoll_data_t {
    fn default() -> Self {
        Self {
            v_u64: 0,
        }
    }
}

fn main() {
    assert_eq!(size_of::<epoll_data_t>(), size_of::<u64>());
}

这个结构体是在 linux 的 epoll API 里使用. 它里面有 4 个元素, 但是这 4 个元素共享同一块内存. 比如, fd 只占用 4 个字节, 而 ptr 在 64 位的机器上会占用 8 个字节. 这个结构体占用的内存, 会按照占用内存最多的那个元素, 以保证能存下它, 比如上面的 v_u64 永远占用 8 个字节.

另外, 要注意它的内存成局方式是 repr(C), 这是因为联合体常用于与C/C++一起使用, 为了保证与C ABI 的兼容, 通常都要使用 repr(C) 的布局方式.

联合体的特点

  • 联合体中各个成员共享同一块内存, 其内存大小是占用内存最大的那个元素
  • 当向一个成员写入值时, 可能就会把别的成员的内容给覆盖

联合体的成员类型

不单联合体的使用很受限, 它里面包含的成员类型也受限, 只能是:

  • 实现了 Copy trait
  • 共享引用 &T 或者可变更引用 &mut T
  • ManuallyDrop<T>, 这个需要用户手动管理对象的生命周期, 对类型 T 不作限制
  • 数组 [T; N] 或者元组(T, T), 这里的类型 T 要满足上面的几个条件

以上的限定条件, 就是为了让联合体的对象不再需要自动被 drop. 但用户仍然可以手动实现 Drop trait.

联合体的初始化

联合体的初始化比较特殊, 只需要初始化其中的一个成员即可.

比如, 初始化一个上面介绍的 epoll_data_t 对象.

#![allow(unused)]
fn main() {
let data = epoll_data_t { fd: 1 };
}

但是, 上面的代码只初始化了其中的 4 个节字, 还有高位的 4 个字节是未初始化的, 所以也不应该直接 访问高 4 位的内存, 否则就产生未定义行为.

读写联合体中的成员

向联合体中的成员写入数据, 只是写内存而已, 所以是安全的, 不需要 unsafe; 但是从联合体的成员读数据, 编译器不能保证里面的内存都被初始化了, 所以是不安全的, 需要 unsafe 将它包括起来, 这里是提醒开发者可能会产生未定义行为.

比如这个例子:

#![allow(non_camel_case_types)]

use std::ffi::c_void;

#[repr(C)]
pub union literals_t {
    pub v_u8: u8,
    pub v_u16: u16,
    pub v_u32: u32,
    pub v_u64: u64,
    pub v_char: char,
    pub v_ptr: *const c_void,
}

impl literals_t {
    pub const NULL_PTR: Self = Self { v_u64: 0 };

    /// 将所有字节都重置为 0
    #[inline]
    pub fn reset(&mut self) {
        self.v_u64 = 0;
    }
}

fn main() {
    assert_eq!(size_of::<literals_t>(), 8);

    // 初始化时, 不是 unsafe的, 这里只写入其中 1 个字节, 其它 7 个字节位未初始化.
    let value = literals_t { v_u8: 42 };

    // 访问成员的值是, 是 unsafe
    unsafe {
        assert_eq!(value.v_u8, 0x2a);
    }

    let v_u64 = unsafe { value.v_u64 };
    let least_byte = (v_u64 & 0xff) as u8;
    assert_eq!(least_byte, 42);

    // 访问其它字节, 是未定义行为, 因为它们都没有被初始化.
    let _most_byte = (v_u64 >> 24) as u8;
}

定义方法

这个是跟 C/C++ 有很大不同的地方, Rust 可以给联合体定义方法.

还看上面用到的例子:

#![allow(unused)]
fn main() {
impl literals_t {
    pub const NULL_PTR: Self = Self { v_u64: 0 };

    /// 将所有字节都重置为 0
    #[inline]
    pub fn reset(&mut self) {
        self.v_u64 = 0;
}

与结构体和枚举一样, 使用 impl Union 语法可以给联合体定义方法.

甚至还可以给它定义常量值, 就像在结构体中一样.

与结构体的两个不同点:

  • 联合体中的成员共享同一块内存, 彼此之间可能会相互覆盖
  • 联合体中的成员类型比较受限, 通常需要实现 Copy trait

其它方面, 和结构体相比就相差不多了, 比如:

  • 可以定义常量
  • 可以通过 impl Union 语法实现方法
  • 可以给它们实现 trait impl Trait for Union
  • 可以给成员设置 pub, pub(crate), 修改其可见性 visibility
  • 支持泛型
  • 支持模式匹配