代码之家  ›  专栏  ›  技术社区  ›  Ricardo Machado

在Copy类型中使用std::ptr::write_volable的内部可变性的安全性(即没有UnsafeCell)

  •  2
  • Ricardo Machado  · 技术社区  · 2 年前

    我试图在 Copy 值类型(用于缓存目的)。

    问题是,据我所知,没有一种类型可用于内部可变性(例如。 UnsafeCell 以及相关类型、原子类型)允许 复制 特质 这是稳定的Rust btw

    我的问题是:使用安全性如何 std::ptr::write_volatile std::ptr::read_volatile 在这种情况下实现内部可变性?

    具体来说,我有这样的代码,我想知道其中是否有任何gotcha或未定义的行为:

    use std::fmt::{Debug, Display, Formatter};
    
    #[derive(Copy, Clone)]
    pub struct CopyCell<T: Copy> {
        value: T,
    }
    
    impl<T: Copy> CopyCell<T> {
        pub fn new(value: T) -> Self {
            Self { value }
        }
    
        pub fn get(&self) -> T {
            unsafe { std::ptr::read_volatile(&self.value) }
        }
    
        pub fn set(&self, value: T) {
            let ptr = &self.value as *const T;
            let ptr = ptr as *mut T;
            unsafe { std::ptr::write_volatile(ptr, value) }
        }
    }
    
    impl<T: Default + Copy> Default for CopyCell<T> {
        fn default() -> Self {
            CopyCell { value: T::default() }
        }
    }
    
    impl<T: Display + Copy> Display for CopyCell<T> {
        fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
            let value = self.get();
            write!(f, "{value}")
        }
    }
    
    impl<T: Debug + Copy> Debug for CopyCell<T> {
        fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
            let value = self.get();
            write!(f, "{value:?}")
        }
    }
    

    我的目标是使用它在值类型中实现本地缓存(例如,一种内存化)。我特别想要 复制 语义(即 Clone )因为他们有更好的人体工程学来实现我想要实现的目标。

    以下是用法示例:

    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn simple_test() {
            let a = CopyCell::default();
            let b = CopyCell::new(42);
    
            assert_eq!(a.get(), 0);
            assert_eq!(b.get(), 42);
    
            a.set(123);
            b.set(b.get() * 10);
            assert_eq!(a.get(), 123);
            assert_eq!(b.get(), 420);
    
            let a1 = a;
            let b1 = b;
    
            a.set(0);
            b.set(0);
    
            assert_eq!(a1.get(), 123);
            assert_eq!(b1.get(), 420);
        }
    
        #[test]
        fn cached_compute() {
            let a = CachedCompute::new(10);
            assert_eq!(a.compute(), 100);
            assert_eq!(a.compute(), 100);
    
            let b = a;
            assert_eq!(b.compute(), 100);
        }
    
        #[derive(Copy, Clone)]
        pub struct CachedCompute {
            source: i32,
            result: CopyCell<Option<i32>>,
        }
    
        impl CachedCompute {
            pub fn new(source: i32) -> Self {
                Self {
                    source,
                    result: Default::default(),
                }
            }
    
            pub fn compute(&self) -> i32 {
                if let Some(value) = self.result.get() {
                    value
                } else {
                    let result = self.source * self.source; // assume this is expensive
                    self.result.set(Some(result));
                    result
                }
            }
        }
    }
    

    上面的代码乍一看是有效的。但我想知道这种方法是否存在任何表面上不明显的潜在问题。

    我对Rust的内存模型、编译器优化、机器代码级语义等没有足够的了解,这可能会在这种情况下造成问题。

    即使这种方法适用于当前的平台,我也想知道我是否会在未来遇到关于未定义行为的潜在问题。

    2 回复  |  直到 2 年前
        1
  •  4
  •   Chayim Friedman    2 年前

    不。没有Rust绝对不可能有内部可变性 UnsafeCell

    您的代码是UB并且 Miri marks it as such :

    error: Undefined Behavior: attempting a write access using <1698> at alloc880[0x0], but that tag only grants SharedReadOnly permission for this location
      --> src/main.rs:20:18
       |
    20 |         unsafe { std::ptr::write_volatile(ptr, value) }
       |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |                  |
       |                  attempting a write access using <1698> at alloc880[0x0], but that tag only grants SharedReadOnly permission for this location
       |                  this error occurs as part of an access at alloc880[0x0..0x4]
       |
       = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
       = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
    help: <1698> was created by a SharedReadOnly retag at offsets [0x0..0x4]
      --> src/main.rs:18:19
       |
    18 |         let ptr = &self.value as *const T;
       |                   ^^^^^^^^^^^
       = note: BACKTRACE (of the first span):
       = note: inside `CopyCell::<i32>::set` at src/main.rs:20:18: 20:54
    note: inside `main`
      --> src/main.rs:51:5
       |
    51 |     a.set(123);
       |     ^^^^^^^^^^
    

    通过使用volatile,您只是欺骗了优化器,使您的代码“工作”,但它仍然是UB。

    People want indeed an UnsafeCell that is Copy ,但现在添加它是个问题,因为人们依赖 T: Copy 意思是 T 没有 UnsafeCell 在…内也许有一天它会被添加。

    推荐文章