Hacker News new | past | comments | ask | show | jobs | submit login

> There is an exception-like mechanism in Rust, in the form of the "try!" macro.

Correct. That's not the problem. If Rust's standard library returned Result in all cases where allocation could fail, I'd be satisfied. My primary issue is that they didn't, because Result is awkward.

Rust's designers went wrong in trying to have their cake and eat it too. They wanted to avoid exceptions and not make people care locally about errors. That's why they assert that errors just don't happen and abort if they do.

Throwing exceptions is a reasonable design choice. Returning error codes is a reasonable design choice. Pretending errors don't exist is not.




> Pretending errors don't exist is not.

We don't and we never have.


I don't think that there's any guarantee in Rust that malloc failure will abort rather than panic. That just happens to be the current implementation. I'm not sure I've ever heard of anyone running into that being an issue in practice, as opposed to this kind of abstract discussion. But I think that it wouldn't be considered a breaking change to switch from aborting to panicking if there were any kind of demand for it.

In Rust, exceptions (panic) are used for truly exceptional situations, like programmer error (indexing beyond the end of an array, division by zero) or things that practically are not expected to happen in a recoverable way in the course of ordinary use, like malloc failure. On modern virtual memory operating systems, malloc failure is so unlikely, and in application code there's so little you could reasonably do if it happened, that it is considered be a truly exceptional case.

On the other hand, Result is used for those kinds of errors that are expected to happen in practice even with working code on reasonable systems. IO errors, errors decoding UTF-8, etc.

Right now, catching exceptions (panics) using recover() is still considered unstable. There is some work ongoing to try and work out the API to help ensure safety, by marking types based on whether they are exception-safe or not; so you can use recover() with types that are built in an exception-safe way, or you can wrap types in AssertRecoverSafe to assert that you are providing exception-safety guarantees yourself, but you can't just arbitrarily recover from panics in code that has access to arbitrary data without someone having added an annotation somewhere that they believe that the code is exception-safe. https://github.com/rust-lang/rust/issues/27719 Note that based on the latest discussion, recover() will likely be named something else involving "unwind" to be more explicit about what it's doing.

And exception safety is quite important to the Rust authors. Note that Mutex has a built-in exception safety mechanism, poisoning the mutex on panic so that other users can't accidentally access the protected resource without being aware that another thread panicked while holding it.

Now, there are times when handling memory allocation failures properly is more important, such as in embedded systems or in operating system kernels, where you don't have a virtual memory abstraction with over-provisioning. However, in those cases you couldn't use the standard library anyhow, as the standard library depends on OS support; so you might as well use alternate data types that do return Result on allocating operations.

I'm just not sure about the utility of providing a convenient way to recover from malloc failure in applications running on virtual-memory operating systems. Can you show me an example in C++ (or any other language) where this is handled properly in application code in any way that doesn't simply log and abort, in which all unwinding code in the same application also avoids allocation as it may occur while unwinding from an allocation failure, and in which these code paths are actually tested in the test suite to ensure they behave properly?


> Right now, catching exceptions (panics) using recover() is still considered unstable. ... you can't just arbitrarily recover from panics in code that has access to arbitrary data without someone having added an annotation somewhere that they believe that the code is exception-safe

And it's for this reason that I don't think I'll be choosing Rust for any of my projects in the near future. This cavalier attitude toward memory exhaustion is not only concerning itself, but also makes me doubt the robustness and design principles of the rest of the system.

Besides, if you make exception-safe code difficult to write, nobody in practice will write it, so you'll end up with a system that's tantamount to one that just aborts. Saying that "Rust the language handled OOM just fine without stdlib!" and "we can convert OOM to panic!" is useless when these measures don't help real world code.

> In Rust, exceptions (panic) are used for truly exceptional situations

I've never accepted the argument that we need to use one error-recovery scheme for "normal" errors and another for "exceptional" ones. That kind of claim sounds reasonable, sober, and measured, but it leads to bad outcomes in every system I've seen, because the "exceptional" case in practice becomes a hard abort. A unified error handling scheme is a boon because it greatly simplified the cognitive analysis of errors.

Java is a good example of how to do right-ish. Serious errors are Throwables not derived from Exception, so normal catch blocks are unlikely to catch them. But serious errors are still exceptions (if not Exception), and all the usual language features for processing exceptions, including unwinding, stack trace recording, and chaining, operate normally.

Uniformity of error processing in Java is a great feature, and the language gets it without sacrificing the ability to distinguish between serious and expected errors. Now, I'm not arguing that Rust get checked exceptions, but I do have to insist that experience shows that you don't need two completely different error handling mechanisms (say, panic and Result) to mark problem severity.

> But I think that it wouldn't be considered a breaking change to switch from aborting to panicking if there were any kind of demand for it.

I'm not comfortable to casual changes in core runtime semantics.

> On modern virtual memory operating systems,

Are you just defining "modern" as "overcommit"? People (especially from the GNU/Linux world) constantly assert that allocation failure is rare, but I've seen allocations fail plenty of times, due to both address space exhaustion and global memory exhaustion. I don't have any firm numbers, but I haven't seen any from the abort-on-failure camp either.

> Can you show me an example in C++ (or any other language) where this is handled properly in application code in any way that doesn't simply log and abort, in which all unwinding code in the same application also avoids allocation as it may occur while unwinding from an allocation failure, and in which these code paths are actually tested in the test suite to ensure they behave properly?

SQLite [1] and NTFS [2] come to mind, as well as lots of tools I've discovered.

[1] https://www.sqlite.org/malloc.html

[2] guaranteed to make forward progress; pre-reserves all needed recovery resources; yes, I know NTFS runs in ring zero, but it's not the case that the kernel doesn't have to deal with dynamic memory allocation


  Besides, if you make exception-safe code difficult to 
  write, nobody in practice will write it, so you'll end up 
  with a system that's tantamount to one that just aborts. 
  Saying that "Rust the language handled OOM just fine 
  without stdlib!" and "we can convert OOM to panic!" is 
  useless when these measures don't help real world code.
I'm not sure where you get the "difficult to write" part from. It's no more or less difficult to write than in any other language, as far as I know; you just do have to go through the effort to indicate that "yes, I did think this through and believe this is exception safe" for types that you want to be able to use across an exception-catching boundary.

As I said, work is ongoing to determine if this AssertUnwindSafe approach is actually workable in practice. The initial implementation had some usability issues, but it looks like it may be more workable now that you can use it on the entire closure if you need to. It's still a speedbump, but a very minor one.

  That kind of claim sounds reasonable, sober, and 
  measured, but it leads to bad outcomes in every system 
  I've seen, because the "exceptional" case in practice 
  becomes a hard abort.
Can you point out what these bad outcomes or bad systems have been? I agree that in practice, the most common case is that the exceptional case becomes a hard abort, but I don't necessarily agree that that's a bad thing.

For people who are not trying to write extremely fault-tolerant code like SQLite, and going to great lengths to do so, that is a good thing; adding some half-assed normal error handling around these truly exceptional cases is more likely to lead to mistakes and problems down the line than just aborting is.

For people who are trying to write extremely robust, fault tolerant code, you can either handle panics, or avoid the standard library and do error handling via results. Both should approaches should be viable, depending on your requirements; the standard library does take exception safety into account, so it shouldn't on its own cause issues if you handle errors via panics.

  I'm not comfortable to casual changes in core runtime 
  semantics.
But you are comfortable with the sheer amount of undefined and unspecified behavior in C and C++? Remember, at the moment Rust only has a single implementation and no formal specification, while C and C++ have many different implementations, and the standards allow very wide amounts of leeway in how implementations differ.

Now, Rust not having a formal specification or multiple implementations is not a good thing; it's just a fact of life for a language that is not yet very mature. But I think that this particular behavior is something that should be considered similar to unspecified behavior at the moment. Just like out of memory situations or stack overflow behave differently on different platforms in C and C++ at the moment, how the Rust runtime behaves on out of memory could also be subject to change or different implementations. Given the standard library API, you couldn't return a result, but either aborting or panicking would both be consistent with the language as currently defined.

  People (especially from the GNU/Linux world) constantly 
  assert that allocation failure is rare
I'm not asserting that allocation failure is rare. Just that there are some cases where you don't have a chance to handle it at all, like GNU/Linux where you overcommit, and that handling it in any way other than abort is rare.

  SQLite [1] and NTFS [2] come to mind, as well as lots of 
  tools I've discovered.
Neither SQLite nor NTFS use exceptions, nor are they applications, so they aren't very good examples of applications using exception handling to deal with memory allocation failure.

SQLite is written in C, which doesn't have exceptions, nor a standard library similar to the C++ or Rust standard library. SQLite has had to implement all of their data structures by hand. You can do exactly the same in Rust by using #![no_std] and just using the core library, which only defines basic data types and never allocates.

NTFS is written in the NT kernel, which doesn't have support for exceptions either, nor does it use the C++ standard library.

So yes, you can actually write code that handles allocation failure properly. The examples you've given both eschew a high-level standard library, and instead implement all of their data structures and memory handling themselves, reporting errors by passing error values back. All of which you can do in Rust using #![no_std].

Meanwhile, there are lots of user-space applications that people use all the time which have no special handling for OOM situations; they rely on the OS to provide them with sufficient amounts of virtual memory, and either be killed by not handling an exception, aborting explicitly on getting NULL from malloc, or being killed by an OOM killer if they exceed the capacity of the machine and try to access an overcommitted page.

I'm sure there are some examples out there, somewhere, of user-space applications that actually do catch such issues, and attempt to do graceful cleanup. On the other hand, I don't know how successful they will be, especially if they have to be cross-platform; since any kind of cleanup you may do, such as writing state out to disk before dying, will hit the kernel's page cache, which may involve allocating memory, which may fail in such a situation, even if you do try to handle the issue gracefully in user-space you may not have anything you can do.


There's more to the world than end-user applications though. I think your mental model is that there are two kinds of Rust user: OS kernel writers and people who create applications with menu bars and save buttons.

What about network services that would rather begin failing requests on overload than shut down entirely and restart, incurring potentially big delays in the process? What about scientific computing projects that are happy delaying work once they've hit pre-defined limits? I think you're suffering from a failure of imagination.

If Rust's goal is to supplant C, it needs to be capable of everything C is capable of doing. Arguing that applications in general need X or Y is a canard, because most of those applications have no specific need of the kind of direct memory control that Rust affords.

To put it another way: who are you trying to satisfy? Are you trying to compete with Go, Nim, Python, and Java and provide high-level facilities that work most of the time, at the cost of control, or are you trying to compete with C and C++, which still fill an essential niche?

By appealing to arguments about the requirements of applications in general instead of requirements of systems programming languages specifically, you're suggesting that the former audience is the better bet.

That kind of targeting is sad, since one of the promises of Rust is that its memory safety would save us from the plague of security holes in low-level software. The decisions the Rust project is making right now make it less likely that Rust will be able to fully fill C and C++'s niche.

One of the purposes of having a standard library for a project is to be a universal resource for all users of a language. If Rust's standard library isn't suitable for all environments where Rust might be used (like C++'s standard library is), then maybe it should be packaged as a separate project, like Qt.


  There's more to the world than end-user applications 
  though. I think your mental model is that there are two 
  kinds of Rust user: OS kernel writers and people who 
  create applications with menu bars and save buttons.
Not at all. I myself work with more types of applications than that; I work with high-reliability networked daemons, GUI applications, and web applications.

  What about network services that would rather begin 
  failing requests on overload than shut down entirely and 
  restart, incurring potentially big delays in the 
  process?
High reliability network services generally need to be distributed across multiple machines anyhow, to provide reliability against the machine going down, so they have some notion of processes that can be stopped without shutting the whole system down. If your system can't handle one of the daemons being restarted, then it has bigger problems.

However, even for this case, you can handle OOM more gracefully if you change allocation failure to panic rather than abort (either by changing the default in Rust's standard library, or using a custom allocator). At that point, you can define a proper task boundary on which you catch unwinding, make sure that everything shared across that task boundary is exception safe, and recover gracefully.

  What about scientific computing projects that 
  are happy delaying work once they've hit pre-defined 
  limits? I think you're suffering from a failure of 
  imagination.
How many of these applications use malloc failure as their backpressure mechanism against over-allocation of resources? In general, I think they have a tendency to distribute small jobs across a large cluster, balancing them based on resource utilization, and accepting that some jobs may fail for various reasons with the ability to re-run said jobs if necessary.

  If Rust's goal is to supplant C, it needs to be capable 
  of everything C is capable of doing. Arguing that 
  applications in general need X or Y is a canard, because 
  most of those applications have no specific need of the 
  kind of direct memory control that Rust affords.
Rust's goal is not necessarily to supplant C or C++; they are far too widely used for that ever to be realistic.

The goal is to provide a reasonable, safe alternative, that offers better abstractions and greater safety, and can be used in situations that other high-level safe languages are unsuitable for.

As far as replacing C, Rust absolutely is capable of replacing C; just use #![no_core] and handle allocation failure however you want. C++'s standard library is more comparable to Rust's standard library.

  To put it another way: who are you trying to satisfy? Are 
  you trying to compete with Go, Nim, Python, and Java and 
  provide high-level facilities that work most of the time, 
  at the cost of control, or are you trying to compete with 
  C and C++, which still fill an essential niche?

  By appealing to arguments about the requirements of 
  applications in general instead of requirements of 
  systems programming languages specifically, you're 
  suggesting that the former audience is the better bet.
As an aside, when you say "you", it sounds like you may be addressing me as a member of the Rust team. I am not; I am a user of Rust, and have contributed a few small patches, but I am only speaking for myself and not anyone else.

Rust is a general purpose programming language, that is designed to appeal to a wide audience, but fill needs that other high-level languages cannot, and provide safety and abstraction that C or C++ cannot.

The first audience is likely a much larger audience, and so it is worth keeping their needs in mind, while the second audience can take the most advantage of Rust's safety and performance guarantees.

  That kind of targeting is sad, since one of the promises 
  of Rust is that its memory safety would save us from the 
  plague of security holes in low-level software. The 
  decisions the Rust project is making right now make it 
  less likely that Rust will be able to fully fill C and 
  C++'s niche.
There are many, many applications, including more than just GUI facing applications but also servers, high-performance computing, etc, written in C and C++ that do not, and do not or do not need to handle allocation failure explicitly. In fact, in this entire discussion, you still have not pointed to a single example of a C++ application that does anything other than abort on allocation failure.

However, even for applications that do not need to handle allocation failure, they would be able to take advantage of type safety, memory safety, and easy, safe concurrency. You are focusing on one, small issue, and ignoring the huge swath of other issues that you run into when writing C or C++ code that can go away by using Rust.

  One of the purposes of having a standard library for a 
  project is to be a universal resource for all users of a 
  language. If Rust's standard library isn't suitable for 
  all environments where Rust might be used (like C++'s 
  standard library is), then maybe it should be packaged as 
  a separate project, like Qt.
But C++'s standard library is not suitable for all environments in which it's used. Other examples that have already been brought up in this discussion are in kernels, embedded systems, in any code running at Google, and heck, as you mention there are third-party libraries like Qt that are widely used frequently to the exclusion of the standard library.

Something like C++ or Rust's standard library cannot be used in all situations, and even in places where it could run, no general purpose standard library is ever going to satisfy all users. What Rust aims to provide is one that works best, and most naturally, for a wide variety of use cases, which includes GUI applications, web apps, network daemons, and scientific application.

Since handling allocation failure as anything but an abort is so uncommon, it chooses to avoid either of the other two options: requiring everything that allocates to return a Result, making the interfaces to every collection type much more painful to use, or having pervasive exceptions and exception handling, meaning you need to think about exception safety everywhere.

The approach that Rust takes is a moderate approach; it uses return values for those errors that pretty much any user will have to handle, and panics for truly exceptional situations that normally should lead to an abort but which you can add special handling for at task boundaries if you need to provide higher availability, which means that you limit the number of places in which exception-safety needs to be considered to just those boundaries.

At the moment, it uses aborts for allocation failure, but there's nothing inherent to the language about that, just the current implementation.

I think the main point where our opinions diverge is that I see handling memory allocation failure with anything other than an abort as much, much more rare than the extremely common cases of exceptional situations leading to much worse results in C or C++. The sheer amount of undefined behavior, the mysterious bugs caused by buffer overruns overwriting random bits of the stack, the security vulnerabilities, the bugs caused by some undefined behavior you didn't realize was there causing the optimizer do do something strange to your code, and so on.

If allocation failure causing an abort when pushing to a Vec, unless you supply a custom allocater that panics instead and implement proper panic handling, is something that you think is fatal in terms of choosing a language, why is it not fatal that one single missed buffer length check buried in one library somewhere can cause completely unrelated parts of your application to fail mysteriously? As far as appropriateness for the kinds of projects you describe, other than the greater library and tool support due to being much more mature ecosystems, I can think of very few cases in which C or C++ would be preferable to Rust; so much of their behavior on unexpected situations is so much worse than an abort.


> If Rust's goal is to supplant C, it needs to be capable of everything C is capable of doing.

We have demonstrated this multiple times. You can either use your own stdlib like sqlite, or use recover. You may not like the solution, but the fact still remains that it still is a tangible solution (well, the latter one is -- "your own stdlib" is a pretty specialized solution which you shouldn't need) to the problem. Given that a solution exists, the only issue is with usability -- and you have to ask the question if there are any improvements to the OOM-handling API that can be made without burdening the users who don't care about OOM too much. There is one improvement which can be made that doesn't affect non-OOM users at all (custom allocators v2, which lets you use Rust error handling with stdlib heap types). This improvement is something the core team cares about and will probably happen (don't know about the time frame, since it handles a lot more things than just Resulty heap types). Other improvements will either mean having regular users check for null all the time, or make panics standard fare, neither of which are good ideas.

Please stop ignoring the fact that Rust does have a solution to the OOM problem; I'm tired of reiterating this argument. One can make arguments that it's much not as usable as C++ or C -- that's okay, but ignoring it entirely is just silly.

(As far as usability wrt C++ and C, I still don't see why it's less usable, C has the horrible check-every-time-or-else situation, and Rust's solution is more or less identical to C++ with the exception that it's the road less traveled on. Given that the API handles exception safety explicitly, this should not be that big a problem).

> you're suggesting that the former audience is the better bet

Not necessarily. The former audience encompasses the latter. Rust doesn't want to put undue burden on general users (like having to check all allocations or having to think about exception safety). That's a reasonable ask. It similarly doesn't want to put undue burden on systems users, and it doesn't -- not any more than C or C++. I don't think the Rust designers feel that they have, recover() is a pretty decent API with a lot of thought put into exception safety.

> If Rust's standard library isn't suitable for all environments where Rust might be used (like C++'s standard library is)

The reason #[no_std] was brought up was because you gave an example of sqlite, which does the same thing. It's meant to be used in certain situations in embedded programming or writing a kernel (note that Rust still has a "core std lib", called libcore, which is available and doesn't need malloc) where things like malloc may not even exist. Embedded programming in C++ does something similar.


You haven't changed my mind about Rust being unfit for purpose.

> Please stop ignoring the fact that Rust does have a solution to the OOM problem;

I disagree that what you're calling a solution is, in fact, a solution. It's more like defining away the problem. It's the case that most Rust programs, those that use stdlib, will never be able to rigorously respond to all allocation failures.

You don't get to wave away problems with Rust stdlib with appeals to an unhosted environment when C++'s stdlib doesn't have the problems I'm highlighting. There's no reason std::vector couldn't be used in a kernel --- just no history.

The SQLite criticism is not the point. The request was for a tested component that recovers from allocation failure. Now you're saying that this example isn't good enough because it's written in C. You're moving the goalposts.

I've already outlined what it would take for me to agree that Rust's OOM problem is solved. It looks like Rust is just adding a few ways of optionally doing more stringent checks, not actually propagating failure from core routines appropriately.

> Not necessarily. The former audience encompasses the latter. Rust doesn't want to put undue burden on general users (like having to check all allocations or having to think about exception safety)

Should these poor users get a pony too? Programming is about managing resources. I've outlined elsewhere the kind of trap you force yourself into when you simultaneously avoid both exceptions and error codes. By doing both, you're not making the world a simpler case. You're just hiding the nasty bits that can go wrong, and users deserve better.


> It's the case that most Rust programs, those that use stdlib, will never be able to rigorously respond to all allocation failures.

I'm not talking about using a different stdlib, I'm talking about recover().

> You don't get to wave away problems with Rust stdlib with appeals to an unhosted environment when C++'s stdlib doesn't have the problems I'm highlighting.

I didn't do that. I'm asserting that Rust's stdlib is appropriate for more or less all situations where you would use C++s stdlib. I have already explained why recover() should be adequate when you want to handle OOM, and recover() is part of the regular stdlib.

I was just putting the raison d'etre for no_std out there, and noting that the situations where you would use it in Rust exist in C++ too. I was trying to dispel the argument that "no_std exists in Rust, hence the stdlib isn't appropriate for all use cases, hence it shouldn't be part of the distribution", which you might have been making in the grandparent comment (I'm not sure if you were).

> The SQLite criticism is not the point. The request was for a tested component that recovers from allocation failure. Now you're saying that this example isn't good enough because it's written in C. You're moving the goalposts.

Fair. I'm not the one who made the original request, so I forgot about that.

> It looks like Rust is just adding a few ways of optionally doing more stringent checks, not actually propagating failure from core routines appropriately.

I'm not sure what you mean here.

Rust already has the ability to catch all panics and handle OOMs at an abstraction boundary of your choice as a global solution, similar to exceptions in C++.

Rust is getting the ability to do fine-grained C-like (or "C++ with try/catch around every `new`" -like) allocation failure handling in custom allocators v2, which can also tie in with your regular error propagation machinery.

> I've already outlined what it would take for me to agree that Rust's OOM problem is solved.

You really haven't. You've just attacked Rust's lack of exceptions incessantly without much arguments to back it up. You've not mentioned why recover() (given that it has exception safety built in and exception safety was a first-class concern during its design) is inadequate.

> you simultaneously avoid both exceptions and error codes.

Rust's Result type is basically a safer and more robust error code. Custom allocators v2 gets you error-code-like allocation that can tie in with your regular error handling.

(FWIW you can do errno-like error handling of OOM using the current support for custom allocators already, though making this safe might be tricky)




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

Search: