当前位置:   article > 正文

rust unsafe 黑科技小例子:ref_with_flag

rust unsafe 黑科技小例子:ref_with_flag

阅读《Programming Rust 2nd Edition》的 unsafe 章节时看到了这样一个黑科技,感觉很有意思

mod ref_with_flag {
    use std::marker::PhantomData;
    use std::mem::align_of;

    /// A `&T` and a `bool`, wrapped up in a single word.
    /// The type `T` must require at least two-byte alignment.
    ///
    /// If you're the kind of programmer who's never met a pointer whose
    /// 2⁰-bit you didn't want to steal, well, now you can do it safely!
    /// ("But it's not nearly as exciting this way...")
    pub struct RefWithFlag<'a, T> {
        ptr_and_bit: usize,
        behaves_like: PhantomData<&'a T> // occupies no space
    }

    impl<'a, T: 'a> RefWithFlag<'a, T> {
        pub fn new(ptr: &'a T, flag: bool) -> RefWithFlag<T> {
            assert!(align_of::<T>() % 2 == 0);
            RefWithFlag {
                ptr_and_bit: ptr as *const T as usize | flag as usize,
                behaves_like: PhantomData
            }
        }

        pub fn get_ref(&self) -> &'a T {
            unsafe {
                let ptr = (self.ptr_and_bit & !1) as *const T;
                &*ptr
            }
        }

        pub fn get_flag(&self) -> bool {
            self.ptr_and_bit & 1 != 0
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

这段代码创建了一个类似union的类型RefWithFlag,可以同时存放一个变量的地址和一个布尔值,比如这样使用

use ref_with_flag::RefWithFlag;

let vec = vec![10, 20, 30];
let flagged = RefWithFlag::new(&vec, true);
assert_eq!(flagged.get_ref()[1], 20);
assert_eq!(flagged.get_flag(), true);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

变量flagged同时存放了vec的地址和true,那么是怎么实现的呢?其实RefWithFlag本质上是一个结构体,包含两个字段

 pub struct RefWithFlag<'a, T> {
     ptr_and_bit: usize,
     behaves_like: PhantomData<&'a T> // occupies no space
 }
  • 1
  • 2
  • 3
  • 4

ptr_and_bit就是用来存放地址和布尔值的字段,在new()时首先进行下面的检查

assert!(align_of::<T>() % 2 == 0);
  • 1

如果类型T的最小对齐字节不是偶数就会报错,为什么要求类型T的最小对齐字节是偶数呢?这里涉及到一些内存对齐的概念,假设当前内存已经使用到地址x,如果x是偶数个字节,也就意味着地址x最低位一定是 0(二进制中 2 的倍数最低位为 0),而如果x是奇数个字节,而类型T的最小对齐字节是偶数,那么在分配时也会从偶数地址开始分配,比如x+1的位置,这样最低位仍然是 0。既然可以保证地址的最低位永远为 0,那就可以把布尔值存入到最低位,下面就是通过位运算实现了这一操作

ptr_and_bit: ptr as *const T as usize | flag as usize
  • 1

虽然这一操作看起来像是 unsafe 的,但实际上只要不访问或者修改指向的内存就是安全的,然后在取值时再进行相应的还原即可

// 获取地址
let ptr = (self.ptr_and_bit & !1) as *const T;
// 获取布尔值
self.ptr_and_bit & 1 != 0
  • 1
  • 2
  • 3
  • 4

另外一个字段behaves_like也很有意思,它不占用任何内存,看起来后面也没有用到,但却不能删除,因为它是用来接收生命周期和泛型参数,如果删掉 rust 就会警告

parameter `'a` is never used
parameter `T` is never used
  • 1
  • 2

在官方文档的介绍中说PhantomData<T>类型的作用是“看起来像是拥有了类型 T”,其实就是可以为编译器提供更多信息,避免上面的警告。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/994154
推荐阅读
相关标签
  

闽ICP备14008679号