The interplay between exceptions and foreign code is a complicated
topic. Here are some miscellaneous thoughts.
1. I don't believe there is any memory safety reason we could not
support catchable exceptions in Rust. The main concern is
more one of good design; it's difficult to recover meaningfully
and correctly from an exception. Generally the best thing to do,
particularly in an interactive application, is to catch the
exception up at a high level (an event loop, say) and try to
throw away all the affected state and start over. Tasks provide
a wonderful abstraction for doing this, of course.
2. Now, that said, some exceptions are expected and should be caught.
File not found and so forth. Java recognized this distinction and
called those checked exceptions; for various reasons, this is a
pain in the neck to work with. But erasing the distinction, as many
other languages chose to do, has its downsides. Currently the Rust
path seems to be the same as Haskell -- use the return type,
specifically `Result<>` or something similar, to indicate common
error conditions. If you think about it, this is very analogous to
what Java does, but because we are using types, we support
parameteric error handling -- that is, you can have generic
wrappers that return a `Result` iff the thing they are wrapping
returns a `Result` (which Java can't do).
3. Presuming we want to keep the above philosophy (which I personally
like, I find exception handlers to be awkward at the best of
times), there is still the question of what to do when a C library
fails. The C library will (typically) indicate this by returning
an error code in some fashion. Ideally, the Rust task would observe
this error code and then fail in response. OK, so far so good.
(If the callee does not signal errors by returning a result code,
then Rust probably won't be able to respond to it. We can't
generate cleanup code for every possible exception scheme. So I
think the best thing here would be for the author to insert a
shim that intercepts exceptions and translates them to a return
code.)
4. Problems start to occur though when you have interleaved C and Rust
code, such as Rust1 -> C -> Rust2. Now what happens if the Rust task
fails? A well-designed C callback API will include a way for the
Rust task to indicate failure, probably a return code. So, in an
ideal world, the Rust code would return this error code when
failing. Of course, we can't know in advance what error code would
be needed, so that implies that Rust2 must have some way
to catch the failure and translate it into the C error code.
Hopefully this will cause the C code to unwind and return an error
to Rust1, which can then fail! again. This process can be repeated
as needed.
5. But what do we do if some C library doens't include error return
codes, or doesn't (necessarily) unwind in response to an error?
This is a hard question and I think there is no single right
answer, instead the person wrapping the library will have to find
the right answer for their particular case. For example, Rust1 and
Rust2 could communicate using TLS, so that an exception occurring
in Rust2 can (eventually) be re-raised in Rust1, even if the C code
doesn't consider the possibility of failure. Or maybe the author
just doesn't really care if Rust2 fails and they just swallow the
failure, violating Rust's failure semantics (because they know
better than us for their particular case anyhow).
6. Some would argue that based on that last point, we might as well
just permit catch anywhere, and discourage its use. I am
sympathetic to this as well though I think Rust's general approach
to error handling (which we stole from Erlang) is really the
more robust one: when things go wrong, tear it all up, then build
it back up again. Still, perhaps community norms are enough here.
Niko
On Tue, Nov 12, 2013 at 11:07:50AM -0800, Kevin Ballard wrote:
> Right now, Rust does not support catching task failure from within a task, it
> only supports preventing task failure from cascading into other tasks. My
> understanding is that this limitation is done because of safety; if a task
> unwinds through a few frames of code, and then stops unwinding, data
> structure invariants may have been broken by the unwinding, leaving the task
> in an unsafe state. Is this correct?
>
> Given this assumption, my worry now is about task unwinding outside of the
> control of Rust. Namely, if I’m using Rust to write a library with extern “C”
> functions, or I’m providing callbacks to C code from within Rust, (and my
> Rust code calls back into C at some point), then it’s very possible for the
> called C code to throw an exception that is then caught in the calling C code
> a few frames up. The net effect is that the thread will unwind through my
> Rust code, but it will then be caught before unwinding any further,
> potentially leaving any data structures in an invalid state (assuming that
> there’s still Rust code higher up on this same stack that cares).
>
> Has this been considered before? Is this actually a danger or am I just being
> paranoid?
>
> -Kevin
> _______________________________________________
> Rust-dev mailing list
> [email protected]
> https://mail.mozilla.org/listinfo/rust-dev
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev