Pre-RFC: I/O Safety

IMHO, UnwindSafe is language bloat, that doesn't serve any real purpose other than making the language more complex and harder to understand. You cannot design sound unwind-unsafe data structures due to the lack of unsafe. Being able to convert !UnwindSafe to UnwindSafe via AssertUnwindSafe in safe Rust means, that practically everything has to be UnwindSafe.

In conclusion, data structures that would benefit from being able to transition into a state, that behaves well when dropped, but must not be operated on in any other way, if a panic occurs, are impossible to design in Rust except for panic=abort programs. You must add additional code to ensure, that such dangerous operations don't happen, most likely by forcing a manual abort, but that requires additional branches and probably additional state-tracking, as well.

I haven't read the entire discussion here so sorry if this is off-topic, but I @RalfJung suggested I share my design for a safer AsRawFd trait here (from the discussion in `SockRef::from`, `Socket::sendfile` and other functions that operate on arbitrary file descriptors or `SOCKET`s potentially should be unsafe · Issue #218 · rust-lang/socket2 · GitHub).

struct BorrowedFd<'fd> {
    fd: RawFd,
    _lifetime: PhantomData<&'fd RawFd>, // Lifetime of the file descriptor we're borrowing from
}

trait AsBorrowedFd {
    // Perhaps some types need mutable access, so we would need `&'fd mut self` instead.
    fn as_borrow_fd<'fd>(&'fd self) -> BorrowedFd<'fd>;
}

I think that UnwindSafe machinery serves a different goal.

To use C++ terminology, there’s “no exception safety”, “basic exception safety”, “full exception safety”.

First means that a type is unsafe to use after unwinding. Second means that the type is safe to use, but might give wrong results, and third means that the type has transactional semantics and is always valid after unwinding.

You seem to say that “it’s impossible to use UnwindSafe for the unsafe code to opt out of basic guarantee”. This is true: all code in rust should provide at least basic exception safety.

But my understanding is that UnwindSafe serves a different purpose: it differentiates between full and basic exception safety guarantees. This is definitely not useless. Ie, in rust-analyzer, where we use catch_unwind, there is a case where UnwindSafe bound prevents (wrongly) re-use chalk instances whose caches become inconsistent after unwinding.

So, unwind safety does work as intended, and serves its purpose. It’s unclear though if this is worth the effort (plus, there are ergonomic issues like Send not implying UnwindSafe or the traits being in std rather than core).

3 Likes

Consider code which simplifies down to this:

struct StuffDoer {
    owned: Box<dyn AsRawFd>, 
    ffi_thing: FFIThing,
}

impl StuffDoer {
    pub fn new(owned: impl AsRawFd) -> Self {
        let raw_fd = owned.as_raw_fd();
        Self {
            owned: Box::new(owned),
            ffi_thing: unsafe { new_ffi_thing(raw_fd) },
        }
    }

    pub fn do_stuff(&self) -> io::Result<()> {
        unsafe { ffi_thing_do_stuff(&self.ffi_thing as *const FFIThing) }
    }
}

Of course, we don't know what's in the box. But we own it, and have encapsulated it, so we know we're not doing any other operations on it. And of course, this is an I/O analog of a self-referential type, but unlike with memory, I/O objects aren't safely movable, so in practice, this kind of thing can work reliably. This code should be ok with AsRawFd + DanIoSafe, but it wouldn't be ok with AsRawFd + RalfIoSafe.

My assumption was that, at this point in Rust's maturity, the first step here would need to be to introduce the minimum requirements to achieve I/O safety, so that it's as easy as possible to migrate existing code, and after that, design new and better (and more opinionated) opt-in features. Would Rust be ok saying that code patterns like the above example are discouraged or deprecated at this point?

Your explanation was short and clear and your arguments are compelling. If I ever argue about UnwindSafe, again, it'll be about me focusing on Rust not having offered no-exception-safety handling rather than completely dismissing UnwindSafe. The other part I dislike about UnwindSafe is, that it is an auto trait, but that is more of a topic for another thread and about auto traits, in general.

2 Likes