Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

There are still other blockers on embedded though. LLVM doesn't target every architecture, and still no allocator API or OOM handling.


The dynamically-allocating portions of the stdlib may panic on OOM, but in an embedded context you're not even linking that code into your program. And you're free to provide an alternative stdlib that bubbles up OOM for those rare occasions where you want to dynamically allocate and you want to be able to do something sane in the face of OOM and you're on a platform that doesn't overcommit.


Minorish point: a true OOM won't panic, it'll abort the program completely. Some APIs may identify that the requested amount of memory is impossible (would overflow) and panic instead, though.

Aborting once the allocator has actually been invoked is important for perf and exception safety, though we have briefly mused allowing it to panic: https://github.com/rust-lang/rust/issues/26951


OOM is an abort, not a panic (contrary to the official docs, interestingly)

> you're free to provide an alternative stdlib

But like... should you have to?

> for those rare occasions

In kernel programming, allocation failures are common and handling them is essential. To quote my (very talented) classmate, who actually wrote part of a kernel in Rust:

"The only really major issue is with how allocation failure works, which makes rust a (very) poor choice for real kernel development"

https://www.reddit.com/r/rust/comments/341v3n/cs_honors_thes...


> But like... should you have to?

This is the status quo (in all languages) if you're doing embedded work: you lose most of the standard library. Rust goes slightly beyond that default by bundling `core`, which is designed for these environments and serves as a building block on which embedded/bare-metal development can flourish. (Cargo works fine with such things, so one can even distribute alternate standard libraries on crates.io.)

> "The only really major issue is with how allocation failure works, which makes rust a (very) poor choice for real kernel development"

I think this was the result of bravely trying very hard to shoe-horn `std` and the inflexible `box` syntax into a kernel environment, somewhere both are 100% not designed for. The recommended approach is to just use `core` and layer non-`std` libs on top of that.


1. In practice most embedded toolchains will give you some of the standard library. A standards compliant C++ freestanding library provides new and delete, for example.

2. The libcore allocator API is marked unstable, so even if you go through the trouble of implementing it, how long will it last?


> will give you some of the standard library.

That's what libcore is. A lot of the stdlib is reexports over libcore.

new and delete are C++ isms, their Rust counterparts are `Box::new` (and destructors are automatic). `Box::new`, like `new`, has the OOM issue. If you're okay with that, you are free to link to the `alloc` crate, which gives you `Box` without pulling in additional deps.

There are plans for in-place boxing, and AFAICT that API will be available to embedded users.


> There are plans for in-place boxing, and AFAICT that API will be available to embedded users.

Already exists, unstably: http://doc.rust-lang.org/nightly/core/ops/trait.Placer.html


Just to be clear, the C++ equivalent of Box is unique_ptr. I don't see equivalents of new and delete, but I might be missing them.

I see Box::from_raw and core::ops::Placer. Is that what you mean by in-place boxing? But now there are two problems:

You have to manually allocate a buffer of the right size, check if the pointer is null, and box it. For every allocation. Forget a null check? Allocate too small a buffer? Guess what, you've got undefined behavior!

It also doesn't generalize to other parts of the standard library. Did you want to use Rust's built in containers in your kernel? Well, sorry, you're going to have to write your own ones that don't panic on allocation failure.


> Just to be clear, the C++ equivalent of Box is unique_ptr.

Yeah, I mean that it's used like `new` is. We don't have the notion of constructors.

> You have to manually allocate a buffer of the right size, check if the pointer is null, and box it. For every allocation.

No. With the placement API, you'll be able to use the `box` syntax (which at the moment just supports `Box`, which has OOM issues) for your custom wrapper (i.e., `Box` with OOM support, or whatever).

What `box` gives us is that `let x = box make_inner()` would have the returned value of `make_inner()` written directly to the heap location. (Unlike `let x = Box::new(make_inner())`, which may be a move of `make_inner()`)

Placer is a part of this (https://github.com/rust-lang/rfcs/blob/master/text/0809-box-... is the whole design). from_raw isn't, that's just a useful API for FFI.

> Did you want to use Rust's built in containers in your kernel? Well, sorry, you're going to have to write your own ones that don't panic on allocation failure.

When you're writing a kernel you're not supposed to be using the standard library at all, just libcore (which exists for this purpose). The same issue exists in C++.


Placer looks interesting. I'll try it out.

>When you're writing a kernel you're not supposed to be using the standard library at all, just libcore (which exists for this purpose). The same issue exists in C++.

Not quite. C++ standard containers are polymorphic on the allocator. That means you're welcome to use std::list or std::string in kernel mode. Just plug in a kernel allocator and you're good to go.

There's no technical reason why every Rust core project must reinvent the linked list and the hash map, like every C project.



1. Yes, as I just said, that's essentially libcore.

It doesn't literally offer working dynamic allocations, but I don't see how a cross-platform and cross-use-case freestanding library can do that, too many different environments/constraints to encode an built-in allocator. In fact, core says nothing about how allocation has to work or even if it needs to exist.

The rustc distribution has the additional `alloc` and `collections` crates layered on top of `core`, which offers some extra functionality when allocation does exist (without assuming other OS support, like IO), and the allocator used is pluggable.

(Do you have an example of a standards complaint C++ freestanding standard library?)

2. There will be some way to do this long term. I can offer no guarantees that it will be what's there right now, but something will exist. This is also something that has been on the radar for a long time, but isn't core to the language. In any case, what is in core is essentially just optimising stack usage, all of the sematics it offers can be done with normal function calls, e.g. `Box::new` is literally just a normal function and perfectly implementable in any environment, if you have some allocation routine (the compiler/language doesn't need to know about either). So the unstable libcore functionality is important and desirable, but not killer.

There's also the meaning of "allocators" as in allowing `Vec` (etc.) to use some other algorithm than what Rust does by default; again something desirable and been on the radar for a while, but not key to the language. I don't think this needs any new language features, just people to explore the library space. (NB. this is different to what libcore offers now, which is basically what is called "placement new" in C++.)


>(Do you have an example of a standards complaint C++ freestanding standard library?)

Yes, you've probably heard of libstdc++:

https://gcc.gnu.org/onlinedocs/libstdc++/faq.html#faq.what_i...

In other words, freestanding C++ allows you to to use new and delete normally (after providing malloc and free). Note that this a minimum requirement. There's nothing stopping you from using STL containers in embedded systems, and people do (especially now that STL containers can take user defined allocators).

It seems like Rust is trying to achieve the same result with libcore. That's good! Thanks for clarifying.

But now you lose all the nice parts of the standard library, and probably much of the safety. If you want to use Box, you have to manually do the allocation, check the pointer, and construct the box. Buffer too small? Forget the pointer check? Welcome to undefined behavior.

If you want a container, you have to roll your own. If you want a container that takes user-defined allocators, I'm not even sure what'd you do.


As you say, the libsupc++ requires you to provide malloc and free, just like libcore.

> probably much of the safety

Only if you want to make life hard for yourself. The standard library is not at all special in its ability to create safe abstractions for `unsafe` code, and doing this makes things so much smoother: you don't have to scatter `unsafe` all over your code, and you let the compiler help you as much as possible. Also, I think there's a lot of nice stuff in `core` too; even string formatting works.

You can still implement equally safe version of them tuned for your environment or, more likely (as the ecosystem develops), use a crate that someone else has written that does it for you. Here's the start of an implementation of a version of `Box` that returns `Result` instead of crashing on allocation failure:

  extern crate core;

  use core::{mem, ops, ptr};

  extern {
      fn malloc(size: usize) -> *const ();
      fn free(p: *const ());
  }

  pub struct MyBox<T> {
      p: *const T,
  }

  impl<T> MyBox<T> {
      fn new(x: T) -> Result<MyBox<T>, ()> {
          unsafe {
              let p = malloc(mem::size_of::<T>()) as *const T;
              if p.is_null() {
                  Err(())
              } else {
                  ptr::write(p as *mut T, x);
                  Ok(MyBox { p: p })
              }
          }
      }
  }

  impl<T> Drop for MyBox<T> {
      fn drop(&mut self) {
          unsafe {
              drop(ptr::read(self.p));
              free(self.p as *const _);
          }
      }
  }

  // allow `*` to work
  impl<T> ops::Deref for MyBox<T> {
      type Target = T;
      fn deref(&self) -> &T { unsafe { &*self.p } }
  }
This is a safe interface, and is a simplified version of how the built-in box is defined in `std` (well, `alloc`). I've completed the rest of the core functionality `Box` offers, including a demonstration at the bottom: https://play.rust-lang.org/?gist=77f2b15bcc4273846140&versio...

The whole of Rust's standard library (including `Box`, all the containers, all of IO) is built via this process: wrap the raw `unsafe` functionality into safe interfaces that manage things like checking `malloc`'s return value, bounds checking buffer accesses.

> If you want a container that takes user-defined allocators, I'm not even sure what'd you do.

Do exactly what C++ does: have some interface (a trait, in Rust) that an allocator needs to satisfy and have the collections take a type parameter that implements that interface. We "already" know what this looks like in Rust (broadly speaking):

  struct Box<T, A: Allocator = DefaultAllocator> {
      p: *const T,
      allocator: A,
  }
The hardest part and sticking point for including this in `std`/`core` is working out a good interface, because it will live forever, needs to cover as much of the problem space as possible and have accommodation for some possible future expansions to the language/library. This isn't such a problem for a container library outside `std`, which is much more flexible because it can be versioned independently, at whatever pace is necessary.

An out-of-tree version aimed at a single use-case (e.g. embedded development) is likely even easier, since the problem space is smaller.


That code sample is really helpful, thanks.

But doesn't that seem kind of redundant? Box and std containers are great as is, except for the one line that aborts on OOM. Would it be possible to use traits to choose OOM behavior at compile time?

To be clear, this isn't just important for embedded. A production database or web server should be able to handle an allocation failure without blowing up the whole process.

And I'm glad there's some talk about a standard allocator trait. I'll look for the relevant issue.


There could definitely be an alternate method on `Box` that returned Result, but I don't think this is so feasible for containers which allocate in many places and so would require duplicating most of the API, however there may be some tricks that work.

https://github.com/rust-lang/rfcs/issues/538 covers the allocator stuff.


Right, duplicating every method that allocates also isn't very elegant.

But could Box and containers be polymorphic on an OOM handling trait?

Alternatively, one could imagine an OOM-safe container that returns results, and a convenience wrapper class that unwraps them. That might harm efficient code generation though.


I really appreciate your excellent insight here, thank you.


  > OOM is an abort, not a panic (contrary to the official docs, interestingly)
Where in the docs do we say this? I'd like to fix it.


The docs (which are generally very good, especially for a language this young) should:

1. Distinguish between abort and panic.

2. Explain that allocation failure is an abort.

3. Explain that a vec can allocate up to twice its nominal size, which is a common cause of OOMs. To their credit, the docs do explain that vec over-allocates, but not that it allocates double the memory. They could also explain that you don't get that memory back unless you ask for it, even if you pop() or remove() elements.

Scenario:

I'm a new systems programmer excited to try Rust (fast and safe? wow!). I have 8GB of memory in my computer. I push ~4GB of data into a vector so I can process it, something I'm used to doing in Python.

POW! My program exits with no error message. What happened? I look at the error handling docs:

https://doc.rust-lang.org/book/error-handling.html

Well, it sure wasn't a recoverable failure! Must have been a panic. So I look at the docs for vec.push:

https://doc.rust-lang.org/std/vec/struct.Vec.html

"Panics if the number of elements in the vector overflows a usize."

A usize is big, right? I didn't overflow that. I guess I could have run out of memory, but I have 8GB. That should be plenty!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: