Rust 游戏引擎搭建 - 6. ECS 搭建(二) 类型擦除数组

222 阅读2分钟

本次实现的是一个类型擦除数组. 与 Rust 游戏引擎搭建 - 4. 类型擦除指针 - 掘金 (juejin.cn) 实现的目的类似, 都是为了在标准容器中储存不同数据类型.

比起称为“类型擦除”, 也许“类型隐藏”更为合适. 因为不管是指针还是数组, 内部都保存了类型信息以做校验, 实际上并不能随意转换.

这种数组我命名为 DataBlock.

DataBlock 需求说明

在已经实现了类型擦除指针后, 可以用这种指针的数组来模拟擦除了类型的数组. 不过, 数组的内存是连续分布的, 用指针来模拟就失去了内存连续带来的 cache 命中优势. 为了保留连续内存的同时擦除数据类型, 制作这一抽象结构.

功能描述

  1. 支持基础的数组数据操作, 包括 new, push, get, get_mut.
  2. 边界检查, 类型检查.

Example 测试

#[test]
fn push_and_get() {
    let mut block = DataBlock::new::<Integer>();

    block.push(&Integer(12)).unwrap();
    let read = block.get::<Integer>(0).unwrap();
    assert_eq!(read.0, 12);

    block.push(&Integer(32)).unwrap();
    let write = block.get_mut::<Integer>(1).unwrap();
    assert_eq!(write.0, 32);

    write.0 = 22;
    let read = block.get::<Integer>(1).unwrap();
    assert_eq!(read.0, 22);
}

实现细节

这一节主要的难点和此前的 Ptr 实现一样, 在于类型的擦除与复原.

Struct 定义

首先是数据存储的形式:

pub struct DataBlock {
    type_id: TypeId,
    data: Vec<u8>,
}

擦除了类型的数据以 u8 的形式存储在 data 字段中, 同时保留类型信息作为校验.

&[u8] 与 Struct 的转换

unsafe fn serialize_raw<T: Sized>(input: &T) -> &[u8] {
    let type_raw = input as *const T;
    let data_raw = type_raw as *const u8;
    std::slice::from_raw_parts(data_raw, size_of::<T>())
}

unsafe fn deserialize_raw<T: Sized>(input: &[u8]) -> Option<&T> {
    let data_raw = input.as_ptr();
    let type_raw = data_raw as *const T;
    type_raw.as_ref()
}

unsafe fn deserialize_raw_mut<T: Sized>(input: &mut [u8]) -> Option<&mut T> {
    let data_raw = input.as_mut_ptr();
    let type_raw = data_raw as *mut T;
    type_raw.as_mut()
}

这几个 unsafe 的辅助函数实现了类型信息的擦除和复原. 在实现过程中没有任何校验操作, 因此无法保证操作的安全性, 需要在函数声明出标注 unsafe.

数组基础操作

有了类型转换, 操作就顺理成章了:

impl DataBlock {
    pub fn new<T: Sized + 'static>() -> Self {
        Self {
            type_id: TypeId::of::<T>(),
            data: Vec::new(),
        }
    }

    pub fn push<T: Sized + 'static>(&mut self, value: &T) -> Result<(), &'static str> {
        if self.type_id != TypeId::of::<T>() {
            return Err("The type of the value doesn't match.");
        }

        let slice = unsafe { serialize_raw(value) };
        for byte in slice.iter() {
            self.data.push(byte.clone());
        }

        Ok(())
    }

    pub fn get<T: Sized + 'static>(&self, index: usize) -> Option<&T> {
        if self.type_id != TypeId::of::<T>() { return None; }

        let (start, end) = self.boundary::<T>(index)?;
        let slice = &self.data[start..end];
        unsafe { deserialize_raw(slice) }
    }

    pub fn get_mut<T: Sized + 'static>(&mut self, index: usize) -> Option<&mut T> {
        if self.type_id != TypeId::of::<T>() { return None; }

        let (start, end) = self.boundary::<T>(index)?;
        let slice = &mut self.data[start..end];
        unsafe { deserialize_raw_mut(slice) }
    }

    fn boundary<T: Sized>(&self, index: usize) -> Option<(usize, usize)> {
        let size = size_of::<T>();
        let start = size * index;
        let end = start + size;
        if end > self.data.len() { None }
        else { Some((start, end)) }
    }
}

函数不长, 名字也写的比较清楚, 那我就摸了.