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