mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-23 07:14:28 +00:00
48ae1b335f
Clarify that copied allocators must behave the same Currently, the safety documentation for `Allocator` says that a cloned or moved allocator must behave the same as the original. However, it does not specify that a copied allocator must behave the same, and it's possible to construct an allocator that permits being moved or cloned, but sometimes produces a new allocator when copied. <details> <summary>Contrived example which results in a Miri error</summary> ```rust #![feature(allocator_api, once_cell, strict_provenance)] use std::{ alloc::{AllocError, Allocator, Global, Layout}, collections::HashMap, hint, marker::PhantomPinned, num::NonZeroUsize, pin::Pin, ptr::{addr_of, NonNull}, sync::{LazyLock, Mutex}, }; mod source_allocator { use super::*; // `SourceAllocator` has 3 states: // - invalid value: is_cloned == false, source != self.addr() // - source value: is_cloned == false, source == self.addr() // - cloned value: is_cloned == true pub struct SourceAllocator { is_cloned: bool, source: usize, _pin: PhantomPinned, } impl SourceAllocator { // Returns a pinned source value (pointing to itself). pub fn new_source() -> Pin<Box<Self>> { let mut b = Box::new(Self { is_cloned: false, source: 0, _pin: PhantomPinned, }); b.source = b.addr(); Box::into_pin(b) } fn addr(&self) -> usize { addr_of!(*self).addr() } // Invalid values point to source 0. // Source values point to themselves. // Cloned values point to their corresponding source. fn source(&self) -> usize { if self.is_cloned || self.addr() == self.source { self.source } else { 0 } } } // Copying an invalid value produces an invalid value. // Copying a source value produces an invalid value. // Copying a cloned value produces a cloned value with the same source. impl Copy for SourceAllocator {} // Cloning an invalid value produces an invalid value. // Cloning a source value produces a cloned value with that source. // Cloning a cloned value produces a cloned value with the same source. impl Clone for SourceAllocator { fn clone(&self) -> Self { if self.is_cloned || self.addr() != self.source { *self } else { Self { is_cloned: true, source: self.source, _pin: PhantomPinned, } } } } static SOURCE_MAP: LazyLock<Mutex<HashMap<NonZeroUsize, usize>>> = LazyLock::new(Default::default); // SAFETY: Wraps `Global`'s methods with additional tracking. // All invalid values share blocks with each other. // Each source value shares blocks with all cloned values pointing to it. // Cloning an allocator always produces a compatible allocator: // - Cloning an invalid value produces another invalid value. // - Cloning a source value produces a cloned value pointing to it. // - Cloning a cloned value produces another cloned value with the same source. // Moving an allocator always produces a compatible allocator: // - Invalid values remain invalid when moved. // - Source values cannot be moved, since they are always pinned to the heap. // - Cloned values keep the same source when moved. unsafe impl Allocator for SourceAllocator { fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> { let mut map = SOURCE_MAP.lock().unwrap(); let block = Global.allocate(layout)?; let block_addr = block.cast::<u8>().addr(); map.insert(block_addr, self.source()); Ok(block) } unsafe fn deallocate(&self, block: NonNull<u8>, layout: Layout) { let mut map = SOURCE_MAP.lock().unwrap(); let block_addr = block.addr(); // SAFETY: `block` came from an allocator that shares blocks with this allocator. if map.remove(&block_addr) != Some(self.source()) { hint::unreachable_unchecked() } Global.deallocate(block, layout) } } } use source_allocator::SourceAllocator; // SAFETY: `alloc1` and `alloc2` must share blocks. unsafe fn test_same(alloc1: &SourceAllocator, alloc2: &SourceAllocator) { let ptr = alloc1.allocate(Layout:🆕:<i32>()).unwrap(); alloc2.deallocate(ptr.cast(), Layout:🆕:<i32>()); } fn main() { let orig = &*SourceAllocator::new_source(); let orig_cloned1 = &orig.clone(); let orig_cloned2 = &orig.clone(); let copied = &{ *orig }; let copied_cloned1 = &copied.clone(); let copied_cloned2 = &copied.clone(); unsafe { test_same(orig, orig_cloned1); test_same(orig_cloned1, orig_cloned2); test_same(copied, copied_cloned1); test_same(copied_cloned1, copied_cloned2); test_same(orig, copied); // error } } ``` </details> This could result in issues in the future for algorithms that specialize on `Copy` types. Right now, nothing in the standard library that depends on `Allocator + Clone` is susceptible to this issue, but I still think it would make sense to specify that copying an allocator is always as valid as cloning it. |
||
---|---|---|
.. | ||
benches | ||
primitive_docs | ||
src | ||
tests | ||
Cargo.toml |