|
| 1 | +- Feature Name: `vec_fallible_allocation` |
| 2 | +- Start Date: 2022-20-04 |
| 3 | +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +`Vec` has many methods that may allocate memory (to expand the underlying storage, or shrink it down). Currently each of these method rely on "infallible" |
| 10 | +allocation, that is any failure to allocate will call the global OOM handler, which will (typically) panic. Even if the global OOM handler does not panic, the |
| 11 | +return type of these method don't provide a way to indicate the failure. |
| 12 | + |
| 13 | +Currently `Vec` does have a `try_reserve` method that uses "fallible" allocation: if `try_reserve` attempts to allocate, and that allocation fails, then the |
| 14 | +return value of `try_reserve` will indicate that there was a failure, and the `Vec` is left unchanged (i.e., in a valid, usable state). We propose adding |
| 15 | +more of these `try_` methods to `Vec`, specifically for any method that can allocate. However, unlike most RFCs, we are not suggesting that this proposal is |
| 16 | +the best among many alternatives (in fact we know that adding more `try_` methods is undesirable in the long term), instead we are suggesting this as a way |
| 17 | +forward to unblock important work (see the "Motivations" section below) while we explore other alternatives. |
| 18 | + |
| 19 | +# Motivation |
| 20 | +[motivation]: #motivation |
| 21 | + |
| 22 | +The motivation for this change is well documented by other RFCs, such as the proposed [fallible_allocation feature](https://github.com/rust-lang/rfcs/pull/3140) |
| 23 | +and the accepted [fallible_collection_alloc feature](https://github.com/rust-lang/rfcs/blob/master/text/2116-alloc-me-maybe.md). |
| 24 | + |
| 25 | +As a brief summary, there are environments where dynamic allocation is required and failing to allocate is both expected and able to be handled. For example, |
| 26 | +in OS Kernel environments (such as [Linux](https://lore.kernel.org/lkml/CAHk-=wh_sNLoz84AUUzuqXEsYH35u=8HV3vK-jbRbJ_B-JjGrg@mail.gmail.com/)), some embedded |
| 27 | +systems, high-reliability systems (such as databases) and multi-user services (where a single request may fail without the entire service halting, such as a |
| 28 | +web server). |
| 29 | + |
| 30 | +# Guide-level explanation |
| 31 | +[guide-level-explanation]: #guide-level-explanation |
| 32 | + |
| 33 | +One often unconsidered failure in a software system is running out of memory (commonly referred to as "Out-of-memory" or OOM): if code attempts to dynamically |
| 34 | +allocate memory, and there is an insufficient amount of available memory on the system, how should this be handled? In most applications, the failure to |
| 35 | +allocate means that the code cannot make any progress, and so the appropriate response is to exit immediately indicating that there was a failure (i.e., to |
| 36 | +`panic`). Consider the Rust Compiler itself: if the compiler cannot allocate memory then it cannot fail that part of the compilation and attempt to continue |
| 37 | +the rest of the compilation - the only appropriate action is to fail the entire compilation and emit an error message. Since panicking on allocation failure is |
| 38 | +appropriate in most circumstances, this is the default within the Rust standard library. Confusingly, this approach is referred to as "infallible allocation": |
| 39 | +from the perspective of something calling a function it can assume that allocations cannot fail (otherwise the entire process will be terminated). |
| 40 | + |
| 41 | +There are, however, environments where allocation failures are both expected and able to be handled. Consider a multi-user service such as a database or a web |
| 42 | +server: if a request comes in to the service that requires allocating more memory than is available on the system, then the service can respond with a failure |
| 43 | +for just that request, and it can continue to service other requests. This approach is referred to as "fallible allocation": each function that may allocate |
| 44 | +needs to indicate to its caller if an attempt to allocate has failed. Within the Rust standard library, one can identify "fallible allocation" functions by |
| 45 | +their names being prefixed with `try_` and their return types being an instance of `Result`. |
| 46 | + |
| 47 | +# Reference-level explanation |
| 48 | +[reference-level-explanation]: #reference-level-explanation |
| 49 | + |
| 50 | +Currently any function in `alloc` that may allocate is marked with `#[cfg(not(no_global_oom_handling))]` (see <https://github.com/rust-lang/rust/pull/84266>), |
| 51 | +which the [fallible_allocation feature](https://github.com/rust-lang/rfcs/pull/3140) proposes to change to a check of the `infallible_allocation` feature |
| 52 | +(i.e., `#[cfg(feature = "infallible_allocation")]`). Any such method in `Vec` will have a corresponding "fallible allocation" method prefixed with `try_` that |
| 53 | +returns a `Result<..., TryReserveError>` and so is usable if `no_global_oom_handling` is enabled (or `infallible_allocation` is disabled). |
| 54 | + |
| 55 | +Under the covers, both the fallible and infallible methods call into the same implementation function which is generic on the error type to return (either |
| 56 | +`!` for the infallible version or `TryReserveError` for the fallible version) - this allows to maximum code reuse, while also avoiding any performance overhead |
| 57 | +for error handling in the infallible version. |
| 58 | + |
| 59 | +List of APIs to add: |
| 60 | + |
| 61 | +```rust |
| 62 | +try_vec! |
| 63 | + |
| 64 | +impl<T> Vec<T> { |
| 65 | + pub fn try_with_capacity(capacity: usize) -> Result<Self, TryReserveError>; |
| 66 | + pub fn try_from_iter<I: IntoIterator<Item = T>>(iter: I) -> Result<Vec<T>, TryReserveError>; |
| 67 | +} |
| 68 | + |
| 69 | +impl<T, A: Allocator> Vec<T, A> { |
| 70 | + pub fn try_append(&mut self, other: &mut Self) -> Result<(), TryReserveError>; |
| 71 | + pub fn try_extend<I: IntoIterator<Item = T>>(&mut self, iter: I, ) -> Result<(), TryReserveError>; |
| 72 | + pub fn try_extend_from_slice(&mut self, other: &[T]) -> Result<(), TryReserveError>; |
| 73 | + pub fn try_extend_from_within<R>(&mut self, src: R) -> Result<(), TryReserveError> where R: RangeBounds<usize>; // NOTE: still panics if given an invalid range |
| 74 | + pub fn try_insert(&mut self, index: usize, element: T) -> Result<(), TryReserveError>; // NOTE: still panics if given an invalid index |
| 75 | + pub fn try_into_boxed_slice(self) -> Result<Box<[T], A>, TryReserveError>; |
| 76 | + pub fn try_push(&mut self, value: T) -> Result<(), TryReserveError>; |
| 77 | + pub fn try_resize(&mut self, new_len: usize, value: T) -> Result<(), TryReserveError>; |
| 78 | + pub fn try_resize_with<F>(&mut self, new_len: usize, f: F) -> Result<(), TryReserveError> where F: FnMut() -> T; |
| 79 | + pub fn try_shrink_to(&mut self, min_capacity: usize) -> Result<(), TryReserveError>; |
| 80 | + pub fn try_shrink_to_fit(&mut self) -> Result<(), TryReserveError>; |
| 81 | + pub fn try_split_off(&mut self, at: usize) -> Result<Self, TryReserveError> where A: Clone; // NOTE: still panics if given an invalid index |
| 82 | + pub fn try_with_capacity_in(capacity: usize, alloc: A) -> Result<Self, TryReserveError>; |
| 83 | +} |
| 84 | + |
| 85 | +#[doc(hidden)] |
| 86 | +pub fn try_from_elem<T: Clone>(elem: T, n: usize) -> Result<Vec<T>, TryReserveError>; |
| 87 | + |
| 88 | +#[doc(hidden)] |
| 89 | +pub fn try_from_elem_in<T: Clone, A: Allocator>(elem: T, n: usize, alloc: A) -> Result<Vec<T, A>, TryReserveError>; |
| 90 | + |
| 91 | +``` |
| 92 | + |
| 93 | +# Drawbacks |
| 94 | +[drawbacks]: #drawbacks |
| 95 | + |
| 96 | +Bifurcating the API surface like this is undesirable: it adds yet another decision point for developers using a fundamental type, and requires the developer to |
| 97 | +stop and understand what could possible fail in the `try_` version. Additionally, this bifurcation can't stop at just `Vec`: What about the rest of the |
| 98 | +collection types? What about the traits that `Vec` implements (e.g., `IntoIter`)? What about other types that implement those traits? What about functions that |
| 99 | +use those traits? This bifurcation becomes viral both within the standard library and to other crates as well. |
| 100 | + |
| 101 | +Implementing both functions using a generic implementation method also complicates the code within `Vec`, making it harder to understand and maintain. While |
| 102 | +there is some complex code within `RawVec`, the code in `Vec` itself tends to be fairly straightforward and within reason for an inexperienced developer to |
| 103 | +understand. |
| 104 | + |
| 105 | +# Rationale and alternatives |
| 106 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 107 | + |
| 108 | +## Rationale |
| 109 | +Adding methods with the `try_` prefix follows the existing pattern within the standard library (as used by `Box::try_new` and `Vec::try_reserve`) and doesn't |
| 110 | +require any changes to the language or the compiler. Although this design does bifurcate the functions, it doesn't bifurcate the type: `Vec` is still `Vec` and |
| 111 | +so works with any crate expecting a `Vec`. Users of `Vec` can choose if they are in a path where allocation failures are recoverable (e.g., handling an |
| 112 | +incoming request), or non-recoverable (e.g., during application startup) without having to change the type they are using. |
| 113 | + |
| 114 | +If these methods are not added to the standard library, then it is highly likely that the code bases that require fallible allocation will take one of these |
| 115 | +approaches themselves, forking the standard library if required. |
| 116 | + |
| 117 | +## Alternatives |
| 118 | + |
| 119 | +### Rely on `panic=unwind` and `catch_unwind` |
| 120 | + |
| 121 | +The `panic::catch_unwind` function allows a `panic` to be "caught" by a previous function on the stack: that is, each function between the `panic` and the |
| 122 | +`catch_unwind` call will immediately return (including running `drop` methods) and then the `panic` is, effectively, canceled. This can be leveraged to handle |
| 123 | +OOM errors without any change to the existing APIs by ensuring that any calls to APIs that allocate are wrapped in `catch_unwind`. |
| 124 | + |
| 125 | +Advantages: |
| 126 | + |
| 127 | +- Requires no new or modified APIs. |
| 128 | +- The `catch_unwind` call could be placed directly where the error is handled (e.g., at the function handling an incoming request, or the dispatcher for an |
| 129 | + event loop) without having to apply the `?` operator throughout the code. |
| 130 | + |
| 131 | +Disadvantages: |
| 132 | + |
| 133 | +- It is not obvious if an API allocates or not (i.e., if `catch_unwind` is required or not): making it easy to either miss function calls, or to be too |
| 134 | + pessimistic. This could be worked around via static analysis. |
| 135 | +- `catch_unwind` requires that the function to be called is `UnwindSafe`, which precludes using a closure that captures a mutable reference. |
| 136 | +- This requires `panic=unwind`, which might not be possible in the environment that requires fallible allocation (e.g., embedded systems or OS kernels). |
| 137 | +- The standard library and 3rd party crates may not expect this pattern, and so have objects in invalid states at the point of OOM. |
| 138 | + |
| 139 | +### Create a new "fallible allocation" `Vec` type |
| 140 | + |
| 141 | +Instead of bifurcating the `Vec` methods, we could instead bifurcate the `Vec` type itself: thus we would keep `Vec` as the infallible version and introduce a |
| 142 | +new fallible version (for the purposes of this RFC, let's call it `FallibleVec`). Under the covers, these two types could rely on the same implementation |
| 143 | +type/functions to minimize code duplication. |
| 144 | + |
| 145 | +Advantages: |
| 146 | + |
| 147 | +- Avoids cluttering `Vec` with new APIs. |
| 148 | +- Reduces decision points for developers: they only need to decide when choosing the type, rather than for each method call. |
| 149 | + |
| 150 | +Disadvantages: |
| 151 | + |
| 152 | +- Still requires bifurcating traits, and the functions that call those crates. |
| 153 | +- `FallibleVec` cannot be used where a something explicitly asks for a `Vec` or any of the infallible allocation traits. |
| 154 | +- Requires a developer to choose at the type level if they need fallible or infallible allocation (unless there's a way to convert between the types). |
| 155 | + |
| 156 | +### Always return `Result` (with the error based on the allocator), but allow implicit unwrapping for "infallible allocation" |
| 157 | + |
| 158 | +If the `Allocator` trait is updated to indicate what error is return if the allocation fails (with infallible allocation returning `!`), then the allocating |
| 159 | +methods on `Vec` can be changed to return a `Result` using that error type. Normally this would be a breaking change, but we could also change the Rust |
| 160 | +compiler to permit an implicit conversion from `Result<T, !>` to `T`, thus any existing code using an infallible allocator will continue to compile. |
| 161 | + |
| 162 | +Advantages: |
| 163 | + |
| 164 | +- Avoids cluttering `Vec` with new APIs. |
| 165 | +- It may be potentially useful for allocators to indicate if they are fallible or not? |
| 166 | + |
| 167 | +Disadvantages: |
| 168 | + |
| 169 | +- Still a breaking change: we are adding an associated type to the `Allocator` trait, and any existing code that takes a `Vec` and allows a custom allocator |
| 170 | + will need to either handle the new return types, or restrict the error type for allocators to `!`. |
| 171 | +- Still requires bifurcating traits, and the functions that call those crates. |
| 172 | +- A `Vec` with a fallible allocator cannot be used where a function asks for the infallible allocation trait types. |
| 173 | +- Makes `Vec` confusing for new developers: introduces the concept of `Result` and `!`, but then adds a weird exception where `Result` can be ignored. |
| 174 | +- Requires a developer to choose at the type level if they need fallible or infallible allocation (unless there's a way to convert between the types). |
| 175 | + |
| 176 | +### Create a "fallible allocation" fork of the `alloc` crate |
| 177 | + |
| 178 | +Instead of bifurcating individual methods or types, we create a new "fallible allocation" version of the `alloc` crate (perhaps keyed off the |
| 179 | +`infallible_allocation` feature). To enable code sharing, we can still have a single implementation method and then have the public wrapper method switch |
| 180 | +depending on how the `alloc` crate is built. |
| 181 | + |
| 182 | +Advantages: |
| 183 | + |
| 184 | +- Avoids cluttering `Vec` with new APIs and bifurcating traits (and functions calling those traits). |
| 185 | +- It would be possible to use the fallible `Vec` in an API that asks for an infallible `Vec` IF it does not use any of the allocating methods. |
| 186 | + |
| 187 | +Disadvantages: |
| 188 | + |
| 189 | +- Forces developers to choose, at a crate level, whether they want fallible or infallible allocation. It might be possible to mix-and-match by referencing both |
| 190 | + the fallible or infallible versions of the `alloc` crate, but the conflicting names would make that messy and there would be no way to convert a type between |
| 191 | + the two. |
| 192 | +- Using a fallible `Vec` in crates that are unaware of the new methods would be hit-or-miss (it's hard to know if a 3rd party crate does or doesn't use an |
| 193 | + allocating method) and very fragile (using an allocating method where none were used previously would become a breaking change). |
| 194 | + |
| 195 | +### Add a side-channel for checking allocation failures |
| 196 | + |
| 197 | +Instead of reporting allocating failures via the return type, it could instead be exposed via a separate method on the allocator or `Vec` (e.g., |
| 198 | +`Allocator::last_alloc_failed` or `Vec::last_alloc_failed`). |
| 199 | + |
| 200 | +Advantages: |
| 201 | + |
| 202 | +- Avoids cluttering `Vec` with new APIs and bifurcating traits (and functions calling those traits). |
| 203 | + |
| 204 | +Disadvantages: |
| 205 | + |
| 206 | +- Very easy to forget to call the side-channel method, and not doing so is highly likely to lead to hard-to-diagnose bugs (e.g., code that thinks it has pushed |
| 207 | + an item but actually hasn't). |
| 208 | + - Static analysis could be added to assist with detecting this. |
| 209 | + - Existing code is unaware of the side-channel, and so will never call it. |
| 210 | + - Subsequent calls to the `Vec` could panic if the last allocation failed, but this would be a performance hit and might not help diagnosability (the |
| 211 | + developer would see a crash at the subsequent call, not the call that failed to allocate). |
| 212 | +- Requires that developers add a lot of boilerplate code, slightly less if the side-channel returns a `Result` that the developer can use `?` on. |
| 213 | +- It is not obvious if an API allocates or not, so even if a developer is aware of the side-channel method they may not think it needs to be called, or may call |
| 214 | + it too often. |
| 215 | + |
| 216 | +### Add a trait for the "fallible allocation" functions (to effectively hide them) |
| 217 | + |
| 218 | +We could create a new trait specifically for the `try_` methods in `Vec` - this will effectively hide them unless a developer is looking through the list of |
| 219 | +trait implementations for `Vec` or adds a `use` declaration for the trait. |
| 220 | + |
| 221 | +Advantages: |
| 222 | + |
| 223 | +- Although we are still adding new methods to `Vec`, they will be hidden from most developers and so avoids the cognitive burden that comes from them being |
| 224 | + directly on the type. |
| 225 | +- The trait and the additional methods could be put in a standalone crate (it would require duplicate quite a bit of code and relying on `unsafe` functions |
| 226 | + like `set_len`). |
| 227 | + |
| 228 | +Disadvantages: |
| 229 | + |
| 230 | +- Still requires bifurcating traits, and the functions that call those crates. |
| 231 | +- Unusual use for a trait, and would require additional documentation to explain why these methods have been implemented this way. |
| 232 | +- Any future allocating methods will require a new trait to implement them, since adding functions to a trait is a breaking change. |
| 233 | + |
| 234 | +# Prior art |
| 235 | +[prior-art]: #prior-art |
| 236 | + |
| 237 | +`Vec` already has a `try_reserve` method, and `Box` has a number of `try_new*` methods (which, when combined with the `_in`, `_uninit`, `_zeroed` and `_slice` |
| 238 | +suffixes, highlights how these variants can combine into an explosion of combinations). |
| 239 | + |
| 240 | +The ["Prior Art" section of the fallible_allocation feature](https://github.com/maurer/rust-rfcs/blob/fallible-allocation/text/0000-fallible-allocation.md#c-oom-handling) |
| 241 | +has a good discussion of C++ OOM Handling. |
| 242 | + |
| 243 | +# Unresolved questions |
| 244 | +[unresolved-questions]: #unresolved-questions |
| 245 | + |
| 246 | +- Is bifurcating the API in this way the correct approach? Or can something else can be done (perhaps with some compiler assistance)? |
| 247 | +- `try_insert`, `try_extend_from_within` and `try_split_off` will still `panic` if provided an invalid index/range: is this ok, or should they never `panic`? |
| 248 | + |
| 249 | +# Future possibilities |
| 250 | +[future-possibilities]: #future-possibilities |
| 251 | + |
| 252 | +## Variants |
| 253 | + |
| 254 | +The numerous variants of `Box::new` highlight the difficulty with handling variations of an API, especially when those variations have different return types: |
| 255 | +`_uninit` and `_zeroed` return a `MaybeUninit<T>` and `try_` returns a `Result<T, AllocError>`. This issue is not unique to Rust, but one wonders if Rust's |
| 256 | +rich support for generics and type inference could be leveraged to provide a solution? |
| 257 | + |
| 258 | +For example, consider some theoretical `Box::new` method like: |
| 259 | + |
| 260 | +```rust |
| 261 | +pub fn new<Variant>() -> Variant::ReturnType<T> |
| 262 | +``` |
| 263 | + |
| 264 | +Where `Variant` could be substituted with some sort of marker type like `Zeroed`, `AllocIn<SomeAllocator>`, `Fallible`, or a combination (like |
| 265 | +`Zeroed + Fallible`). Each of these markers could then change (or just wrap?) the return type as appropriate. If the compiler knew the set of markers that are |
| 266 | +in scope and could be applied, then it might also be able to use inference based on the usage of the returned value to detect which markers to use, for example |
| 267 | +seeing the `?` operator implies `Fallible`, and seeing the value used as `Box<T, SomeAllocator>` implies `AllocIn<SomeAllocator>`. |
| 268 | + |
| 269 | +This could also be done without a breaking change if we leveraged editions: if Edition N-1 disabled inference for variations then writing `Box::new()` would |
| 270 | +always produce the old behavior, and then Edition N could enable the new inference behavior (and the tooling could detect potential breaking changes by seeing |
| 271 | +where the inference did not produce the old behavior, thus allowing automatic fixes by rewriting those calls as `Box::new::<>()`). Alternatively, if none of |
| 272 | +the markers are in scope then the compiler will be forced to infer the old behavior, thus we can enable the behavior in Edition N by adding the markers to the |
| 273 | +prelude. |
| 274 | + |
| 275 | +A function will also need to be able to declare what markers it supports, either by marker types being "local" to the function (making the marker more like |
| 276 | +enum items on an enum declared by the function), or by listing the marker types that it supports. |
0 commit comments