|
1 | 1 | # Allocating Memory
|
2 | 2 |
|
3 |
| -Using Unique throws a wrench in an important feature of Vec (and indeed all of |
4 |
| -the std collections): an empty Vec doesn't actually allocate at all. So if we |
5 |
| -can't allocate, but also can't put a null pointer in `ptr`, what do we do in |
6 |
| -`Vec::new`? Well, we just put some other garbage in there! |
| 3 | +Using NonNull throws a wrench in an important feature of Vec (and indeed all of |
| 4 | +the std collections): creating an empty Vec doesn't actually allocate at all. This is not the same as allocating a zero-sized memory block, which is not allowed by the global allocator (it results in undefined behavior!). So if we can't allocate, but also can't put a null pointer in `ptr`, what do we do in `Vec::new`? Well, we just put some other garbage in there! |
7 | 5 |
|
8 | 6 | This is perfectly fine because we already have `cap == 0` as our sentinel for no
|
9 | 7 | allocation. We don't even need to handle it specially in almost any code because
|
10 | 8 | we usually need to check if `cap > len` or `len > 0` anyway. The recommended
|
11 |
| -Rust value to put here is `mem::align_of::<T>()`. Unique provides a convenience |
12 |
| -for this: `Unique::dangling()`. There are quite a few places where we'll |
| 9 | +Rust value to put here is `mem::align_of::<T>()`. NonNull provides a convenience |
| 10 | +for this: `NonNull::dangling()`. There are quite a few places where we'll |
13 | 11 | want to use `dangling` because there's no real allocation to talk about but
|
14 | 12 | `null` would make the compiler do bad things.
|
15 | 13 |
|
16 | 14 | So:
|
17 | 15 |
|
18 |
| -```rust,ignore |
| 16 | +```rust |
| 17 | +# use std::ptr::NonNull; |
| 18 | +# use std::marker::PhantomData; |
| 19 | +# use std::mem; |
| 20 | +# |
| 21 | +# pub struct Vec<T> { |
| 22 | +# ptr: NonNull<T>, |
| 23 | +# cap: usize, |
| 24 | +# len: usize, |
| 25 | +# _marker: PhantomData<T>, |
| 26 | +# } |
| 27 | +# |
| 28 | +# unsafe impl<T: Send> Send for Vec<T> {} |
| 29 | +# unsafe impl<T: Sync> Sync for Vec<T> {} |
19 | 30 | impl<T> Vec<T> {
|
20 | 31 | fn new() -> Self {
|
21 | 32 | assert!(mem::size_of::<T>() != 0, "We're not ready to handle ZSTs");
|
22 |
| - Vec { ptr: Unique::dangling(), len: 0, cap: 0 } |
| 33 | + Vec { |
| 34 | + ptr: NonNull::dangling(), |
| 35 | + len: 0, |
| 36 | + cap: 0, |
| 37 | + _marker: PhantomData |
| 38 | + } |
23 | 39 | }
|
24 | 40 | }
|
| 41 | +# fn main() {} |
25 | 42 | ```
|
26 | 43 |
|
27 | 44 | I slipped in that assert there because zero-sized types will require some
|
28 | 45 | special handling throughout our code, and I want to defer the issue for now.
|
29 | 46 | Without this assert, some of our early drafts will do some Very Bad Things.
|
30 | 47 |
|
31 | 48 | Next we need to figure out what to actually do when we *do* want space. For
|
32 |
| -that, we'll need to use the rest of the heap APIs. These basically allow us to |
33 |
| -talk directly to Rust's allocator (`malloc` on Unix platforms and `HeapAlloc` |
34 |
| -on Windows by default). |
| 49 | +that, we use the global allocation functions [`alloc`][alloc], [`realloc`][realloc], and [`dealloc`][dealloc] which are available in stable Rust in [`std::alloc`][std_alloc]. These functions are expected to become deprecated in favor of the methods of [`std::alloc::Global`][Global] after this type is stabilized. |
35 | 50 |
|
36 | 51 | We'll also need a way to handle out-of-memory (OOM) conditions. The standard
|
37 |
| -library calls `std::alloc::oom()`, which in turn calls the `oom` langitem, |
38 |
| -which aborts the program in a platform-specific manner. |
| 52 | +library provides a function [`alloc::handle_alloc_error`][handle_alloc_error], |
| 53 | +which will abort the program in a platform-specific manner. |
39 | 54 | The reason we abort and don't panic is because unwinding can cause allocations
|
40 | 55 | to happen, and that seems like a bad thing to do when your allocator just came
|
41 | 56 | back with "hey I don't have any more memory".
|
@@ -151,53 +166,59 @@ such we will guard against this case explicitly.
|
151 | 166 |
|
152 | 167 | Ok with all the nonsense out of the way, let's actually allocate some memory:
|
153 | 168 |
|
154 |
| -```rust,ignore |
155 |
| -fn grow(&mut self) { |
156 |
| - // this is all pretty delicate, so let's say it's all unsafe |
157 |
| - unsafe { |
158 |
| - let elem_size = mem::size_of::<T>(); |
159 |
| -
|
160 |
| - let (new_cap, ptr) = if self.cap == 0 { |
161 |
| - let ptr = Global.allocate(Layout::array::<T>(1).unwrap()); |
162 |
| - (1, ptr) |
| 169 | +```rust |
| 170 | +use std::alloc::{self, Layout}; |
| 171 | +use std::marker::PhantomData; |
| 172 | +use std::mem; |
| 173 | +use std::ptr::NonNull; |
| 174 | + |
| 175 | +struct Vec<T> { |
| 176 | + ptr: NonNull<T>, |
| 177 | + len: usize, |
| 178 | + cap: usize, |
| 179 | + _marker: PhantomData<T>, |
| 180 | +} |
| 181 | + |
| 182 | +impl<T> Vec<T> { |
| 183 | + fn grow(&mut self) { |
| 184 | + let (new_cap, new_layout) = if self.cap == 0 { |
| 185 | + (1, Layout::array::<T>(1).unwrap()) |
163 | 186 | } else {
|
164 |
| - // as an invariant, we can assume that `self.cap < isize::MAX`, |
165 |
| - // so this doesn't need to be checked. |
166 |
| - let new_cap = 2 * self.cap; |
167 |
| - // Similarly this can't overflow due to previously allocating this |
168 |
| - let old_num_bytes = self.cap * elem_size; |
169 |
| -
|
170 |
| - // check that the new allocation doesn't exceed `isize::MAX` at all |
171 |
| - // regardless of the actual size of the capacity. This combines the |
172 |
| - // `new_cap <= isize::MAX` and `new_num_bytes <= usize::MAX` checks |
173 |
| - // we need to make. We lose the ability to allocate e.g. 2/3rds of |
174 |
| - // the address space with a single Vec of i16's on 32-bit though. |
175 |
| - // Alas, poor Yorick -- I knew him, Horatio. |
176 |
| - assert!(old_num_bytes <= (isize::MAX as usize) / 2, |
177 |
| - "capacity overflow"); |
178 |
| -
|
179 |
| - let c: NonNull<T> = self.ptr.into(); |
180 |
| - let ptr = Global.grow(c.cast(), |
181 |
| - Layout::array::<T>(self.cap).unwrap(), |
182 |
| - Layout::array::<T>(new_cap).unwrap()); |
183 |
| - (new_cap, ptr) |
| 187 | + // This can't overflow since self.cap <= isize::MAX. |
| 188 | + let new_cap = 2 * self.cap; |
| 189 | + |
| 190 | + // Layout::array checks that the number of bytes is <= usize::MAX, |
| 191 | + // but this is redundant since old_layout.size() <= isize::MAX, |
| 192 | + // so the `unwrap` should never fail. |
| 193 | + let new_layout = Layout::array::<T>(new_cap).unwrap(); |
| 194 | + (new_cap, new_layout) |
184 | 195 | };
|
185 | 196 |
|
186 |
| - // If allocate or reallocate fail, oom |
187 |
| - if ptr.is_err() { |
188 |
| - handle_alloc_error(Layout::from_size_align_unchecked( |
189 |
| - new_cap * elem_size, |
190 |
| - mem::align_of::<T>(), |
191 |
| - )) |
192 |
| - } |
| 197 | + // Ensure that the new allocation doesn't exceed `isize::MAX` bytes. |
| 198 | + assert!(new_layout.size() <= isize::MAX as usize, "Allocation too large"); |
193 | 199 |
|
194 |
| - let ptr = ptr.unwrap(); |
| 200 | + let new_ptr = if self.cap == 0 { |
| 201 | + unsafe { alloc::alloc(new_layout) } |
| 202 | + } else { |
| 203 | + let old_layout = Layout::array::<T>(self.cap).unwrap(); |
| 204 | + let old_ptr = self.ptr.as_ptr() as *mut u8; |
| 205 | + unsafe { alloc::realloc(old_ptr, old_layout, new_layout.size()) } |
| 206 | + }; |
195 | 207 |
|
196 |
| - self.ptr = Unique::new_unchecked(ptr.as_ptr() as *mut _); |
| 208 | + // If allocation fails, `new_ptr` will be null, in which case we abort. |
| 209 | + self.ptr = match NonNull::new(new_ptr as *mut T) { |
| 210 | + Some(p) => p, |
| 211 | + None => alloc::handle_alloc_error(new_layout), |
| 212 | + }; |
197 | 213 | self.cap = new_cap;
|
198 | 214 | }
|
199 | 215 | }
|
| 216 | +# fn main() {} |
200 | 217 | ```
|
201 | 218 |
|
202 |
| -Nothing particularly tricky here. Just computing sizes and alignments and doing |
203 |
| -some careful multiplication checks. |
| 219 | +[Global]: ../std/alloc/struct.Global.html |
| 220 | +[handle_alloc_error]: ../alloc/alloc/fn.handle_alloc_error.html |
| 221 | +[alloc]: ../alloc/alloc/fn.alloc.html |
| 222 | +[realloc]: ../alloc/alloc/fn.realloc.html |
| 223 | +[dealloc]: ../alloc/alloc/fn.dealloc.html |
| 224 | +[std_alloc]: ../alloc/alloc/index.html |
0 commit comments