#[no_panic] again


#1

There was a short thread about #[no_panic] attribute in 2015.

I’ve recently written some amount of unsafe code, and I found it’s hard to make sure code is panic-safe. Code calls some functions and perform potentially-panicing operations like vec[i] while manually allocating and releasing memory.

Unsafe code could easily corrupt memory or leak on panic.

Consider an artificial example:

struct MySmallVec<T> {
    ptr: *mut T,
    len: u8,
    cap: u8,
}

impl<T> MySmallVec<T> {
    unsafe fn update_as_vec<R, F>(&mut self, f: F) -> R
        where F : FnOnce(&mut Vec<T>) -> R
    {
        unsafe {
            let mut v = Vec::from_raw_parts(self.ptr, self.len as usize, self.cap as usize);
            f(&mut v);
            assert!(v.capacity() <= u8::MAX);
            self.ptr = v.as_mut_ptr();
            self.len = v.len() as u8;
            self.cap = v.capacity() as u8;
            mem::forget(v);
        }
    }
}

There are hidden problems with this code: if it panics in f() or f reserves too much memory, destructor of Vec is called and and MySmallVec object becomes invalid.

But caller can catch_unwind and continue working with corrupted memory.

C++ has noexcept function attribute, and Rust could have similar #[no_panic] attribute:

    #[no_panic]
    unsafe fn update_as_vec<R, F>(&mut self, f: F) -> R { ... }

The program will terminate if code is paniced inside that function. And it is much safer than working with corrupted memory.

Bug in that function can be fixed. However, if function is not intended to panic, author could simply add #[no_panic] attribute and sleep better instead of thinking about panic-safety.

Can we have #[no_panic] please?


#2

In your example, couldn’t you use catch_unwind around f(&mut v) to clean up or abort?


#3

In your example, couldn’t you use catch_unwind around f(&mut v) to clean up or abort?

There are two arguments against it:

  1. I want to wrap whole function with it, not just call to f

So it will be something like

    unsafe fn update_as_vec_hidden<R, F>(&mut self, f: F) -> R { ... }

    unsafe fn update_as_vec<R, F>(&mut self, f: F) -> R {
        abort_on_panic(|| self.update_as_vec_hidden(f))
    }

for each function, which is too noisy.

  1. catch_unwind adds runtime overhead, which could be too large for small functions. #[no_panic] has no runtime overhead at all.

#4

Another option is to use some sentinal object:

struct Sentinal;
impl Drop for Sentinal {
    fn drop(&mut self) {
        panic!()
    }
}

Create one before your critical section, and forget it as you leave.

(I’m not saying #[no_panic] is a bad idea – just exploring what’s possible now.)


#6

That should work, and likely has no overhead.

It is not as ergonomic as #[no_panic] though (for example, you cannot use ? with it).


#7

Hmm, I spied before your edit that you suggested thread::panicking(), which I didn’t know about. That does seem like a good idea instead of manually calling forget, and then ? should work fine too.


#8

thread::panicking() is not zero-cost. I actually like your idea about mem::forget of Sentinal.


#9

FWIW, Rayon has a similar AbortIfPanic which is also used by forgetting.


#10

Hm, can you elaborate on why? I’d expect #[no_panic] to be implemented pretty much like wrapping the whole body in catch_unwind and aborting if unwinding was caught. This is also how noexcept is implemented in C++, AFAIK.


#11

catch_unwind calls non-inlinable function __rust_maybe_catch_panic with callback, so it prevents lots of optimizations.

When compiler handles #[no_panic], compiler could simply emit call to abort instead of calls to destructor in that place where it generates code for unwind on panic. So #[no_panic] is zero-cost: regular (non-panicking) code is no different from code without #[no_panic].


#12

That might just be a deficit of the current implementation. (Note that LTO can inline that function, but the resulting code still isn’t great).

IIUC that only works for panics directly in a #[no_panic] function. Most such functions will contain at least a few calls to other code, so there will still be invokes and landing pads (unless you separately compile with -C panic=abort, in which case the whole issue is moot anyway).