This issue formally proposes stabilizing the #[alloc_error_handler]
attribute as-is, after adding some documentation.
Tracking issue: https://github.com/rust-lang/rust/issues/51540
Normally the tracking issue is where we propose FCP to stabilize, but this one already has many comments that go into a number of sub-topics. Since the feature did not originally go through the RFC process, this proposal is loosely structured after the RFC template.
libstd
Many parts of the standard library rely on a global heap memory allocator. For example Box::new
takes a single parameter, the value to be boxed, and returns a struct that wraps a pointer to newly-allocated heap memory. The allocator is not part this API (on many platforms it defaults to malloc
) and neither is the possibility that allocation fails: the return value always contains a valid pointer.
Allocation can in fact fail (malloc
can return a null pointer), but in practice this is uncommon enough and hard enough to recover from that Box::new
and many other APIs make the choice of not propagating that error to callers. We call these APIs āinfallibleā because allocation failure is not a concern of the caller (as opposed to āfallibleā APIs like Vec::try_reserve
which return a Result
). āInfallibleā APIs deal with failures by calling the handle_alloc_error(Layout) -> !
function, which never returns. The current behavior in libstd
is to print an error message and abort the process. Any low-level code that makes allocations and wants to expose an infallible API is expected to call this function. For example a custom container library could look like:
use std::alloc::{Layout, alloc, handle_alloc_error};
use std::ptr::NonNull;
impl<T> MyBox<T> {
pub fn new(x: T) -> Self {
let layout = Layout::new::<T>();
assert!(layout.size() > 0); // Not dealing with the zero-size case for example brevity
let maybe_null = unsafe { alloc(layout) };
let ptr = NonNull::new(maybe_null)
.unwrap_or_else(|| handle_alloc_error(layout));
Self(ptr.cast())
}
}
no_std
and liballoc
The Rust standard library is split into three crates (that are relevant to this issue): core
, alloc
, and std
.
std
expects much functionality to be provided by the underlying operating system or environment: a filesystem, threads, a network stack, ā¦ and relevant here: a memory allocator and a way to abort the current process. Large parts of its code are target-specific. Porting it to a new target can take non-trivial efforts.
core
contains the subset of std
that has almost no such requirement. A crate can use the #![no_std]
attribute to opt into having its implicit dependency to std
replaced by an implicit dependency to core
. When all crates in an application do this, this enables porting to a target that might not have std
at all. Notably, @rust-embedded does this with micro-controllers that do not have an operating system.
alloc
is in-between. It depends on core
and std
depends on it. It contains the subset of std
that relies on heap memory allocation, but makes no other external requirements over those of core
. Specifically, using alloc
requires:
alloc
function and related functions.handle_alloc_error
function.The std
crate provides both of these, so linking it in an application (having any crate in the dependency graph that doesnāt have #![no_std]
, or has extern crate std;
) is sufficient to use alloc
. Of course this doesnāt work for targets/environments where std
is not available.
#[panic_handler]
core
does have an external requirement: a way to handle panics. std
normally provides this by printing a message to stderr, optionally with a stack trace, and unwinding the thread. In a no_std
application however there may not be an stderr to print to, and unwinding may not be supported. Such apps can therefore provide a handler:
#[panic_handler]
fn panic(panic_info: &core::panic::PanicInfo) -> ! {
// ā¦
}
(See also in the Nomicon.)
The attribute is effectively a procedural macro that checks the signature of the function and turns it into an extern "Rust" fn
with a known symbol name, so that it can be called without going through Rustās usual crate/module/path name resolution.
The compiler also checks for ātop-levelā compilations (executables, cdylib
s, etc.) that there is exactly one panic handler in the whole crate dependency graph. std
(effectively) provides one, so the attribute is both necessary for no_std
applications and can only be used there.
#[global_allocator]
Depending on the workload, an alternative allocator may be more performant than the platformās default. In earlier versions of Rust, the standard library used jemalloc. In order to leave that choice to users, Rust 1.28 stabilized the GlobalAlloc
trait and #[global_allocator]
attribute, and changed the standard libraryās default to the systemās allocator.
This incidentally enabled (in Nightly) the use of alloc
in no_std
applications which can now provide an allocator implementation not just to be used instead of std
ās default, but where std
is not necessarily available at all. However such applications still require Rust Nightly in order to fulfil alloc
ās second requirement: the allocation error handler.
#[global_allocator]
is similar to #[panic_handler]
: it also expands to extern "Rust" fn
function definitions that can be called by a crate (this time alloc
instead of core
) that doesnāt have a Cargo-level dependency on the crate that contains the definition, and in that the compiler checks for ātop-levelā compilation that it isnāt used twice. (It differs in that it can be used when std
is linked, and overrides std
ās default.)
As of Rust 1.36, specifying an allocation error handler is the only requirement for using the alloc
crate in no_std
environments (i.e. without the std
crate being also linked in the program) that cannot be fulfilled by users on the Stable release channel.
Stabilizing #[alloc_error_handler]
as the way to fulfil this requirement would allow:
no_std
+ liballoc
applications to start running on the Stable channelno_std
applications that run on Stable to start using liballoc
Many of the APIs in the alloc
crate that allocate memory are said to be āinfallibleā. Allocation appears to always succeed as far as their signatures are concerned. When allocation does fail, they call alloc::alloc::handle_alloc_error
which never returns. For example, Vec::reserve
is said to be infallible while Vec::try_reserve
is fallible (and returns a Result
). Other libraries who want to expose this infallible style of API may also call handle_alloc_error
.
We call an application no_std
if it doesnāt link the std
crate. That is, if all crates in its dependency graph have the #![no_std]
attribute and (after cfg
-expansion) do not contain extern crate std;
.
A no_std
application may use the standard libraryās alloc
crate if and only if it specifies both a global allocator with the #[global_allocator]
attribute, and an allocation error handler with the #[alloc_error_handler]
attribute. Each may only be defined once in the crate dependency graph. They can be defined anywhere, not necessarily in the top-level crate. The handler defines what to do when handle_alloc_error
is called. It must be a function with the signature as follows:
#[alloc_error_handler]
fn my_example_handler(layout: core::alloc::Layout) -> ! {
panic!("memory allocation of {} bytes failed", layout.size())
}
The handler is given the Layout
of the allocation that failed, for diagnostics purpose. As it is called in cases that are considered not recoverable, it may not return. std
achieves this by aborting the process. In a no_std
environment ā which might not have processes in the first place ā panicking calls the #[panic_handler]
which is also required to not return.
#[alloc_error_handler]
is very similar to #[panic_handler]
: it locally checks that it used on a function with the appropriate signature and turns it into an extern "Rust" fn
with a known symbol name, so that alloc::alloc::handle_alloc_error
can call it.
Like with the panic handler, the compiler also checks for ātop-levelā compilations (executables, cdylib
s, etc.) that there is exactly one allocation error handler in the whole crate dependency graph. std
literally provides one, so the attribute is both necessary for no_std
applications and can only be used there.
The above is already implemented, although not well documented. This issue is about deciding to stabilize the attribute. If we find consensus on this direction, documentation should come before or with a stabilization PR. The alloc
crateās doc-comment could be a good place for this documentation, which could be based on the guide-level explanation above.
#[panic_handler]
is already stable, so the Rust project is already committed to maintaining this style of attribute.
The status quo is that no_std
+ alloc
requires Nightly
Despite already having one with panic_handler
, such ad-hoc attributes could be considered inelegant or otherwise problematic compared to a more general mechanism. This is not my opinion: even if alloc_error_handler
is not the last such attribute that the standard library will even need, I donāt expect them to proliferate in large number.
RFC 2492 Existential types with external definition proposed a general mechanism but was was postponed.
A comment in the alloc_error_handler
tracking issue proposed a less ambitious general mechanism limited to functions. I personally feel that even that proposal has enough design questions to resolve that it would warrant an RFC.
Instead of stabilizing a way to fulfil the requirement to define a handler, another way to unlock the no_std
+ liballoc
on Stable use case could be to remove that requirement: when no handler is defined, the compiler could inject a default handler that panics (similar to the example handler in the guide-level explanation above).
Proposing FCP to stabilize, is described above:
@rfcbot fcp merge
Team member @SimonSapin has proposed to merge this. The next step is review by the rest of the tagged team members:
Concerns:
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
Instead of stabilizing a way to fulfil the requirement to define a handler, another way to unlock the
no_std
+liballoc
on Stable use case could be to remove that requirement: when no handler is defined, the compiler could inject a default handler that panics
If that default handler is accepted, should we not stabilize this attribute and wait for a more general "global things" mechanism?
@rfcbot fcp what if default
@rfcbot concern what if default
Just throwing this out there (and maybe it's already been proposed and rejected): if we wanted to go with the default handler approach, we could make it possible through the PanicInfo
API to distinguish alloc failures from other panics, making it straight forward to handle this specially in your panic handler, unlike other panics. Two ways we could do this:
Layout
the &dyn Any
of the payload
, or a newtype wrapping Layout
. Downcasting the payload to this type assumes an alloc error. Possibly this interacts poorly with existing users' panic handlers.Probably this would mean there's little point in having alloc_error_handler
.
I don't have any opinion about which of these is best. In my opinion we should just make core + alloc work on stable as soon as reasonable.
This is an interesting idea! (Though perhaps one for https://github.com/rust-lang/rust/issues/66741. The perils of starting two related threads at the same timeā¦)
One thing is that thereās currently no stable way in no_std
to end up with PanicInfo::payload
returning Some
, so making the default allocation error handler do this would be slightly magic. Single-argument core::panic!
requires &str
and creates an fmt::Arguments
, unlike single-argument std::panic!
which takes a generic T: Any + Send
and creates a payload
.
We could extend single-argument core::panic!
to also be generic and make a dyn Any
payload. In order to not regress also setting a message
for &str
weād need some kind of specialization, although itās the kind that can be hacked together on Stable with auto-ref. Or maybe we can regress this, since PanicInfo::message
is still unstable. (CC https://github.com/rust-lang/rust/issues/66745)
@withoutboats Do you feel this idea is a reason not to stabilize the existing #[alloc_error_handler]
as-is, as proposed by this thread?
If that default handler is accepted, should we not stabilize this attribute and wait for a more general "global things" mechanism?
No comment in this direction so far, so Iām gonna
@rfcbot resolve what if default
This pFCP seems close to consensus, ping @Centril @alexcrichton @joshtriplett @pnkfelix @scottmcm for checkboxes in https://github.com/rust-lang/rust/issues/66740#issuecomment-558182950
My feelings on this are the same as on the related issue. I disapprove of this stabilization and do not believe we should do so. I do not have the energy, time, or motivation to pursue a blocking objection though. As a result I believe enough others will need to check their boxes such that my approval is not required to proceed.
@withoutboats Do you feel this idea is a reason not to stabilize the existing #[alloc_error_handler] as-is, as proposed by this thread?
I find Alex's comment on the related issue pretty convincing - we should have a clear idea of what we're doing here, not just stabilize what exists. Personally the idea of handling all of this through the panic handler is appealing to me. I'd like if at the allhands we developed a more complete roadmap for the alloc crate which incorporates solving this issue (in whatever way) as its first step.
@withoutboats Those concerns seem reasonable to me, and worth considering. I do, in general, hope that the use cases currently served by alloc
and core
and no_std
are in the future addressed by feature flags and custom compliations of std
.
Do you believe that we should defer this pair of stabilizations in favor of a better solution?
I respect Alex's concerns, however having a stable allocator for no_std/embedded has been blocked for a pretty significant amount of time to date, and while it is not a fundamental blocker for all use cases, it makes certain things very difficult, as well as being a soft-blocker for prototyping/less resource constrained use cases.
I understand the need to get this right, rather than just done, and Alex noted a lack of ownership on this topic. If the stabilization proposal from Simon here and in #66741 is judged as unacceptable, I'd be happy to volunteer to help push this effort forward in the next iteration of discussions. I certainly don't think I have any immediate solutions, but I would be happy to shepherd the push to get a minimal no_std allocator story stabilized.
@joshtriplett @withoutboats @SimonSapin - do you have suggestions on who should be contacted to coordinate this discussion? I have a reasonable "allocator implementer/users perspective" (at least from an embedded user's perspective), though I think Alex's points were rather aimed at the coherence of the core/std library's perspective.
@jamesmunns In no_std/embedded, have you ever had a need to differentiate out-of-memory situations from other types of panics? If not then I feel that #66741 is probably the "safest" approach to go for since it would be forward-compatible with any future mechanism we decide to have for specifying behavior on OOM (including the one proposed in this issue).
Checking Centril's box as he's taking a break from the project.
:bell: This is now entering its final comment period, as per the review above. :bell:
Stabilizing #[alloc_error_handler] as the way to fulfil this requirement would allow:
- no_std + liballoc applications to start running on the Stable channel
- no_std applications that run on Stable to start using liballoc
I am somewhat confused by this framing, which is not actually accurate given the parallel developments in https://github.com/rust-lang/rust/issues/66741. These are some great motivations, but it looks like we are currently seeing two independent changes motivated that way when really the motivation only requires one of them -- once either solution is accepted, the motivation ceases to apply to the other. It doesn't seem fair to use this motivation to justify both changes, when one of them (the one that lands later) will not actually help the cause.
I see little discussion for whether we shouldn't rather just adopt one of the two instead of both ways to enable no_std + liballoc.
the motivation only requires one of them
That is correct. This issue and https://github.com/rust-lang/rust/issues/66741 list each other in their respective "Alternatives" section of the description/proposal. When initially filing them I did not expect both of them to enter FCP at (roughly) the same time.
The final comment period, with a disposition to merge, as per the review above, is now complete.
As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.
The RFC will be merged soon.
What's the status of this stabilization?
To the letter of the process, the proposal is accepted and the next step is a stabilization PR. However @RalfJung made a good point above and Iād like some input from @rust-lang/lang on how they feel about accepting both this and https://github.com/rust-lang/rust/issues/66741 when either would achieve the main motivation (which is liballoc in no_std programs).
I think we should only stabilize one of the two proposals. It doesn't make sense to stabilize both IMO.
I mentioned in a previous comment hoping that we could solve this at the all hands, which didn't happen of course. I'd be happy to sit on a call to hammer out a final resolution to this problem so that we can have alloc available on stable. I don't think it's that hard, we just need a higher bandwidth hour to make an executive decision together.
To the letter of the process, the proposal is accepted and the next step is a stabilization PR. However @RalfJung made a good point above and Iād like some input from @rust-lang/lang on how they feel about accepting both this and #66741 when either would achieve the main motivation (which is liballoc in no_std programs).
We discussed this oddity of these two proposals both being approved at today's lang team meeting.
Our current understanding that is that the two proposals are not inherently in conflict with one another; we thought it would not be unreasonable to approve both proposals.
I have tasked myself with the job of trying to digest the content of both proposals and the developments in their comment threads well enough to understand why there is a relatively strong push to only approve one.
In any case, it seems that the proposal in #66741 is smaller and has a much smaller surface area of impact. That leads us to say that the T-lang design team is inclined to push for adoption of #66741 in the short-term, and then take our time deciding about what to do with #66740. However, during my immediate review of #66741, I did see @alexcrichton's dismay over the adoption of #66741, so I guess it will be best to wait a bit long so that I have time to digest the content of both comment threads.
Removing from nomination since there isn't more to discuss in lang team meeting (yet).
Okay I completed my review of the threads.
@alexcrichton 's comment relaying dismay over adoption of #66741 was, as far as I can tell, a general statement of disappointment in the state of the liballoc
crate overall, and bemoaning a lack of leadership/organization/documentation of that crate that alex feels somewhat personally responsible for.
Alex also took that comment and said that its feedback also applies to #66740
Here are some conclusions I took from this:
Assuming we adopt #66741 in the short term, there remains an open question as to whether to adopt #66740. If we adopt @withoutboats 's proposal to extend PanicInfo
with information about allocation failures when relevant, then we really do not need to adopt #66740 on top of #66741. But without such an extension of PanicInfo
in place, then we may still need to adopt #66740, solely to allow application authors to have more expressive control of how they respond to allocation failures.
We discussed this issue in the @rust-lang/lang meeting today and felt that we could delay stabilizing the #[alloc_error_handler]
proposal until there is a specific request for it for some reason other than access to lib-alloc from a no-std context.
Ah, I somehow missed @withoutboats's proposal. I think that's a great idea which neatly solves the disconnect between panics and OOM aborts.
I agree that we should start with the approach proposed in #66741 since it resolves the immediate problem and is forward compatible with any solution we might choose in the future.
any timeline on this one ?
I believe the current status is that we chosen not to do this stabilization, and to proceed instead with #66741. As @pnkfelix noted here, this stabilization is blocked on (a) a motivating example and (b) perhaps exploration of @withoutboats 's proposal to extend PanicInfo
with information about allocation failures when relevant.
Should we then close this? I think @rust-lang/lang is in favor, but do I hear a second from @rust-lang/libs ?
I'm also happy to close this in favor of #66741.
Thanks for the clarification. If all in favour, when can we expect the PanicInfo
to be "augmented" and stable ? :)
@eloraiby I think probably we should open an issue describing the idea and then somebody could open a PR and ping the @rust-lang/libs (and perhaps @rust-lang/lang) teams to decide on that PR.
which neatly solves the disconnect between panics and OOM aborts.
Note that https://github.com/rust-lang/rust/issues/66741 only affects programs that do not link the std
crate: it is about the behavior when no #[alloc_error_handler]
is defined, and std
defines one. The handler in std
calls a runtime-configurable hook https://github.com/rust-lang/rust/issues/51245, then aborts the process.
So https://github.com/rust-lang/rust/issues/66741 does not affect OOM aborts.
Closing per https://github.com/rust-lang/rust/issues/66740#issuecomment-664549885
Most helpful comment
I respect Alex's concerns, however having a stable allocator for no_std/embedded has been blocked for a pretty significant amount of time to date, and while it is not a fundamental blocker for all use cases, it makes certain things very difficult, as well as being a soft-blocker for prototyping/less resource constrained use cases.
I understand the need to get this right, rather than just done, and Alex noted a lack of ownership on this topic. If the stabilization proposal from Simon here and in #66741 is judged as unacceptable, I'd be happy to volunteer to help push this effort forward in the next iteration of discussions. I certainly don't think I have any immediate solutions, but I would be happy to shepherd the push to get a minimal no_std allocator story stabilized.
@joshtriplett @withoutboats @SimonSapin - do you have suggestions on who should be contacted to coordinate this discussion? I have a reasonable "allocator implementer/users perspective" (at least from an embedded user's perspective), though I think Alex's points were rather aimed at the coherence of the core/std library's perspective.