trybox/
lib.rs

1//! Stable, `no_std`-compatible, fallible heap allocation for [`Box`].
2//!
3//! Basic usage is as follows:
4//! ```
5//! # use trybox::ErrorWith;
6//! match trybox::new(1) {
7//!     Ok(heaped) => {
8//!         let _: Box<i32> = heaped;
9//!     }
10//!     Err(ErrorWith(stacked)) => {
11//!         let _: i32 = stacked; // failed object is returned on the stack
12//!     },
13//! }
14//! ```
15//!
16//! You may drop the object after allocation failure instead,
17//! choosing to e.g propogate or wrap the [`Error`].
18//!
19//! ```
20//! fn fallible<T>(x: T) -> Result<Box<T>, Box<dyn std::error::Error + Send + Sync>> {
21//!     Ok(trybox::or_drop(x)?)
22//! }
23//! ```
24//!
25//! Care has been taken to optimize the size of [`Error`] down to a single usize:
26//! ```
27//! # use std::mem::size_of;
28//! assert_eq!(size_of::<trybox::Error>(), size_of::<usize>());
29//! ```
30//!
31//! And to provide ergonomic error messages:
32//! ```text
33#![doc = include_str!("../tests/i32-error-message.expected")]
34//! ```
35//! ```text
36#![doc = include_str!("../tests/2.5k-error-message.expected")]
37//! ```
38//!
39//! Conversions to [`std::io::Error`] and [`std::io::ErrorKind::OutOfMemory`]
40//! are provided when the `"std"` feature is enabled:
41//!
42//! ```
43//! fn fallible<T>(x: T) -> std::io::Result<Box<T>> {
44//!     Ok(trybox::or_drop(x)?)
45//! }
46//! ```
47//!
48//! # Comparison with other crates
49//! - [`fallacy-box`](https://docs.rs/fallacy-box/0.1.1/fallacy_box/)
50//!   - [requires a nightly compiler](https://docs.rs/fallacy-box/0.1.1/src/fallacy_box/lib.rs.html#3).
51//! - [`fallible_collections`](https://docs.rs/fallible_collections/0.4.9/fallible_collections/)
52//!   - You must use either the [`TryBox`](https://docs.rs/fallible_collections/0.4.9/fallible_collections/enum.TryReserveError.html)
53//!     wrapper struct, or the [`FallibleBox`](https://docs.rs/fallible_collections/0.4.9/fallible_collections/boxed/trait.FallibleBox.html)
54//!     extension trait.
55//!   - The [returned error type](https://docs.rs/fallible_collections/0.4.9/fallible_collections/enum.TryReserveError.html)
56//!     doesn't implement common error traits, and isn't strictly minimal.
57
58#![cfg_attr(not(feature = "std"), no_std)]
59
60extern crate alloc;
61
62use alloc::{
63    alloc::{alloc, handle_alloc_error, Layout},
64    boxed::Box,
65};
66use core::{any, fmt, mem::MaybeUninit, ptr::NonNull};
67
68/// Attempt to move `x` to a heap allocation,
69/// returning a wrapped `x` on failure.
70///
71/// See [crate documentation](mod@self) for more.
72#[inline(always)]
73pub fn new<T>(x: T) -> Result<Box<T>, ErrorWith<T>> {
74    match imp(x) {
75        Ok(it) => Ok(it),
76        Err(e) => Err(ErrorWith(e)),
77    }
78}
79
80/// Attempt to move `x` to a heap allocation,
81/// immediately dropping `x` on failure,
82/// and returning a useful [`Error`].
83///
84/// See [crate documentation](mod@self) for more.
85#[inline(always)]
86pub fn or_drop<T>(x: T) -> Result<Box<T>, Error> {
87    match new(x) {
88        Ok(it) => Ok(it),
89        Err(e) => Err(e.without_payload()),
90    }
91}
92
93#[inline(always)]
94fn imp<T>(x: T) -> Result<Box<T>, T> {
95    let layout = Layout::for_value(&x);
96    match layout.size() == 0 {
97        true => {
98            let ptr = NonNull::<T>::dangling().as_ptr();
99            // SAFETY: This is recommended by the Box documentation
100            Ok(unsafe { Box::from_raw(ptr) })
101        }
102        false => {
103            // SAFETY: We've checked layout to be non-empty, above.
104            let ptr = unsafe { alloc(layout) }.cast::<T>();
105            match ptr.is_null() {
106                true => Err(x),
107                false => {
108                    // SAFETY:
109                    // - we've called GlobalAlloc::alloc above.
110                    // - Box::from_raw with such a pointer is explicitly called
111                    //   out as safe in the Box docs.
112                    let mut heap = unsafe { Box::<MaybeUninit<T>>::from_raw(ptr.cast()) };
113                    heap.write(x);
114                    // SAFETY: we've written an initialized T to the memory.
115                    Ok(unsafe { Box::from_raw(Box::into_raw(heap).cast()) })
116                }
117            }
118        }
119    }
120}
121
122/// Represents an allocation failure from [`or_drop`].
123///
124/// Designed to be small and propogatable.
125pub struct Error {
126    // This could be replaced by `&'static Info` once type_name is a const fn
127    info: fn() -> Info,
128}
129
130impl fmt::Debug for Error {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        let Info { layout, name } = self.info();
133        let mut d = f.debug_struct("Error");
134        d.field("layout", &layout).field("name", &name);
135        d.finish()
136    }
137}
138
139impl fmt::Display for Error {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write_info(self.info(), f)
142    }
143}
144
145fn write_info(info: Info, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146    let Info { layout, name } = info;
147
148    let mut size = layout.size() as f64;
149    let mut prefix = "";
150    let boundary = 1024.0;
151    for next in [
152        "kibi", "mebi", "gibi", "tebi", "pebi", "exbi", "zebi", "yobi",
153    ] {
154        if size <= boundary {
155            break;
156        }
157        size /= boundary;
158        prefix = next;
159    }
160    let precision = match fract(size) == 0.0 {
161        true => 0,
162        false => 2,
163    };
164    f.write_fmt(format_args!(
165        "memory allocation of {size:.precision$} {prefix}bytes (for type {name}) failed",
166    ))
167}
168
169/// `no_std` version of [`f64::fract`]
170const fn fract(x: f64) -> f64 {
171    x - trunc(x)
172}
173
174/// `no_std` version of [`f64::trunc`]
175// https://github.com/rust-lang/libm/blob/754daced79e320c6bc6d2a666a99a60a742c42c4/src/math/trunc.rs#L3-L33
176const fn trunc(x: f64) -> f64 {
177    // let x1p120 = f64::from_bits(0x4770000000000000); // 0x1p120f === 2 ^ 120
178
179    let mut i: u64 = x.to_bits();
180    let e: i64 = (i >> 52 & 0x7ff) as i64 - 0x3ff + 12;
181    let m: u64 = -1i64 as u64 >> e;
182
183    if e >= 52 + 12 {
184        return x;
185    }
186
187    if (i & m) == 0 {
188        return x;
189    }
190    // force_eval!(x + x1p120); // don't care about signalling
191    i &= !m;
192    f64::from_bits(i)
193}
194
195#[cfg(not(feature = "std"))]
196impl core::error::Error for Error {}
197
198#[cfg(feature = "std")]
199impl std::error::Error for Error {}
200
201impl Error {
202    #[inline(always)]
203    fn info(&self) -> Info {
204        (self.info)()
205    }
206    /// Call [`handle_alloc_error`], typically aborting the process.
207    ///
208    /// See that function for more.
209    #[inline(always)]
210    pub fn handle(self) -> ! {
211        handle_alloc_error(self.layout())
212    }
213    /// Get the [`Layout`] that corresponds to the failed allocation.
214    #[inline(always)]
215    pub fn layout(&self) -> Layout {
216        self.info().layout
217    }
218}
219
220#[cfg(feature = "std")]
221impl From<Error> for std::io::Error {
222    /// Create an [`OutOfMemory`](std::io::ErrorKind::OutOfMemory) error,
223    /// possibly with an [`Error`] as the [source](std::error::Error::source).
224    fn from(value: Error) -> Self {
225        let kind = std::io::ErrorKind::OutOfMemory;
226
227        // Creating a new io::Error with a source involves a heap allocation,
228        // but we're probably in a memory-constrained scenario,
229        // so _try_ and preserve the source,
230        // or just use an io::ErrorKind if we can't.
231        match or_drop(value) {
232            Ok(source) => {
233                std::io::Error::new(kind, source as Box<dyn std::error::Error + Send + Sync>)
234            }
235            Err(_cannot_preserve) => std::io::Error::from(kind),
236        }
237    }
238}
239
240#[cfg(feature = "std")]
241impl From<Error> for std::io::ErrorKind {
242    fn from(_: Error) -> Self {
243        std::io::ErrorKind::OutOfMemory
244    }
245}
246
247/// [`Layout`] is two words, but this function pointer is just one.
248trait Indirect: Sized {
249    fn info() -> Info {
250        Info {
251            layout: Layout::new::<Self>(),
252            name: any::type_name::<Self>(),
253        }
254    }
255}
256impl<T: Sized> Indirect for T {}
257
258#[derive(Debug, Clone, Copy)]
259struct Info {
260    layout: Layout,
261    name: &'static str,
262}
263
264/// Represents the failure to allocate a particular object on the heap,
265/// returned from [`new`].
266#[derive(Debug)]
267pub struct ErrorWith<T>(pub T);
268
269impl<T> ErrorWith<T> {
270    fn info(&self) -> Info {
271        Info {
272            layout: Layout::for_value(&self.0),
273            name: any::type_name::<T>(),
274        }
275    }
276    pub fn without_payload(self) -> Error {
277        Error { info: T::info }
278    }
279}
280
281impl<T> fmt::Display for ErrorWith<T> {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        write_info(self.info(), f)
284    }
285}
286
287#[cfg(not(feature = "std"))]
288impl<T: fmt::Debug> core::error::Error for ErrorWith<T> {}
289
290#[cfg(feature = "std")]
291impl<T: fmt::Debug> std::error::Error for ErrorWith<T> {}
292
293impl<T> From<ErrorWith<T>> for Error {
294    fn from(value: ErrorWith<T>) -> Self {
295        value.without_payload()
296    }
297}
298
299#[cfg(feature = "std")]
300impl<T> From<ErrorWith<T>> for std::io::Error {
301    /// Create an [`OutOfMemory`](std::io::ErrorKind::OutOfMemory) error,
302    /// possibly with an [`Error`] as the [source](std::error::Error::source).
303    fn from(value: ErrorWith<T>) -> Self {
304        Error::from(value).into()
305    }
306}
307
308#[cfg(feature = "std")]
309impl<T> From<ErrorWith<T>> for std::io::ErrorKind {
310    fn from(_: ErrorWith<T>) -> Self {
311        std::io::ErrorKind::OutOfMemory
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    static_assertions::assert_eq_size!(Error, *const u8);
320    static_assertions::assert_impl_all!(Error: Send, Sync);
321}