5

I'm writing a Rust program which needs to save some data at the end of its execution, whatever happens.

In the Java world, I would do that with a shutdown hook. There is a crate, aptly called shutdown_hooks, but it seems only able to register extern "C" functions (and I'm not totally sure it will run on panic!(...)).

How can I implement a shutdown hook that triggers on normal exit as well on panic?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Riduidel
  • 22,052
  • 14
  • 85
  • 185
  • 4
    You should not be relying on a program terminating the way you want: the user might kill it, the computer might be powered off, etc. `shutdown_hooks` uses `atexit`, which is about as good as it gets on POSIX systems. It will run on most panics, but not on eg. segfaults. – mcarton Sep 09 '19 at 20:26

1 Answers1

12

In the general case, it's impossible. Even ignoring effects from outside of the program (as mentioned by mcarton), whoever compiles the final binary can choose if a panic actually triggers stack unwinding or if it simply aborts the program. In the latter case, there's nothing you can do.

In the case of unwinding panic or normal exit, you can implement Drop and use the conventional aspects of RAII:

struct Cleanup;

impl Drop for Cleanup {
    fn drop(&mut self) {
        eprintln!("Doing some final cleanup");
    }
}

fn main() {
    let _cleanup = Cleanup;

    panic!("Oh no!");
}
thread 'main' panicked at 'Oh no!', src/main.rs:12:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Doing some final cleanup

It appears that Java's shutdown hooks allow running multiple pieces of code in parallel in threads. You could do something similar with some small modifications:

use std::{
    sync::{Arc, Condvar, Mutex},
    thread,
};

#[derive(Debug, Default)]
struct Cleanup {
    hooks: Vec<thread::JoinHandle<()>>,
    run: Arc<Mutex<bool>>,
    go: Arc<Condvar>,
}

impl Cleanup {
    fn add(&mut self, f: impl FnOnce() + Send + 'static) {
        let run = self.run.clone();
        let go = self.go.clone();

        let t = thread::spawn(move || {
            let mut run = run.lock().unwrap();

            while !*run {
                run = go.wait(run).unwrap();
            }

            f();
        });
        self.hooks.push(t);
    }
}

impl Drop for Cleanup {
    fn drop(&mut self) {
        eprintln!("Starting final cleanup");

        *self.run.lock().unwrap() = true;
        self.go.notify_all();

        for h in self.hooks.drain(..) {
            h.join().unwrap();
        }

        eprintln!("Final cleanup complete");
    }
}

fn main() {
    let mut cleanup = Cleanup::default();

    cleanup.add(|| {
        eprintln!("Cleanup #1");
    });

    cleanup.add(|| {
        eprintln!("Cleanup #2");
    });

    panic!("Oh no!");
}

See also:

Riduidel
  • 22,052
  • 14
  • 85
  • 185
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Such a clever solution. @Shepmaster is it okay if I use this method to block the main thread for user input at the end? Like "Press any key to exit". I just want to keep the console window open in case the user launched my application directly. – m4heshd Jul 18 '23 at 01:24