#Guarantee drop impl is run

46 messages · Page 1 of 1 (latest)

dusty canopy
#

Is there a way to guarantee a struct's drop implementation will get run absent an abort? That is the drop implementation will run assuming the program either exits normally OR exits by panicking (assuming the configuration is to unwind, not immediately abort).

What I'm actually trying to do is make an API that spawns a new thread. That thread will open and turn on a GPIO output. Before the program exits including if it panics it should at a minimum turn off the GPIO output. If this was super mission critical, a hardware/electrical circuit would be the ultimate solution but I want to do as much as I reasonably can in software to guarantee the output is turned off prior to the program exiting.

I can nearly solve this by having the API

  1. Spawn a new thread
  2. Put the join handle of the thread in a struct with a drop impl
  3. The said drop impl signals the thread to stop (if applicable) and joins the thread
  4. Inside the spawned thread, the GPIO is opened and put in a struct that has a drop impl that turns off the GPIO

The problem is that if the API was called by some thread that is not the main thread, then it is the responsibility of the writer of main to join the thread that calls the API. In case the main thread were to panic, the writer of main would have to write a similar drop impl.

west girder
#

where you pass a callback, and then the function handles the callback panicking or returning normally

fervent salmon
#

libc::atexit exists, but idk how well that would work ferrisballSweat

dusty canopy
#

Both interesting.

#

Just a note about the complexity of my original post: It is not required to think about the API spawning a thread. Only that I need the drop impl of the API's struct to run which will be responsible for turning off the GPIO. I want the extra thread for other reasons though.

meager marsh
#

IIRC all you need to do is call _exit rather than exit

keen heart
#

It's impossible to forbid the user from forgetting a value manually

meager marsh
#

You can, however, prevent them from ever having ownership of the value

#

Which is how thread::scope ultimately works

delicate quest
#

By default, everything is dropped on normal exit or unwinding panic. What could happen is someone putting your struct into mem::forget or calling process::exit. For the first, the thread::scope method prevents it. You have a function like this:

fn scope(f: impl FnOnce(&MustDrop)) {
  let must_drop = create();
  f(&must_drop);
  drop(must_drop);
}
```and now the user can't forget `must_drop` since they never own it. This is done when dropping is a matter of safety, but for other things just documenting not to forget the value should be sufficient. People shouldn't normally be calling `mem::forget`.
fervent salmon
#

can one at least prevent unintentionally not executing the Drop?

#

And this scope approach doesn't work in the case where the main thread terminates the entire program while another thread is in the scope, I think.

delicate quest
#

It's really hard to unintentionally bypass drop. Basically only reference loops or data structures that leak on panic. And I guess process::exit.

meager marsh
#

OP explicitly asked about ensuring drops except if an abort happens, I'm lumping exit and double panics in the same category

fervent salmon
#

Oh. For some reason, I assumed that the user were the one that manually spawns the thread. If the API is the one that spawns the thread, that makes more sense.

dusty canopy
#

Also mem::forget is intentional enough I'm not going to worry about it.

dusty canopy
#

I mean, I guess the memory will get cleaned up by the OS. But I want to make sure my drop impl(s) will get run too.

meager marsh
#

Those do run on panic

dusty canopy
# meager marsh Those do run on panic

That's the problem. Here's an example where it doesn't.

use std::thread::{sleep, spawn};
use std::fs::File;
use std::time::Duration;

fn main() {
    spawn(alpha_thread);
    // sleep(Duration::from_secs(2));
    Err::<(), &str>("Artificial error").expect("Found error");
}

fn alpha_thread() {
    let _gpio = Gpio;
    sleep(Duration::from_secs(1));
}

struct Gpio;

impl Drop for Gpio {
    fn drop(&mut self) {
        // Test if this code runs
        File::create("good bye.txt").unwrap();
    }
}
meager marsh
icy sable
#

thread::scope is great, can't imagine when thread::spawn would be ever preferable over it

junior gull
#

when you don't need variables from the scope ferrisClueless

#

which is reasonably often

civic turret
delicate quest
#

scope already does that

#

Unsafe things though, like rayon, use spawn

elfin plover
#

in general, if you have some kind of “background” processing that continues past the stack frame that started it, then you need thread::spawn.
“Structured concurrency” is the paradigm of not doing that, but not everything fits into that paradigm.

#

In principle, everything could be done by passing down a really big Scope from main(), but many people don't want to do that passing.

icy sable
#

name one thing that doesn't get better from structured concurrency

#

You do a bit more writing to reduce debugging surface

#

Isn't that what rust is all about

elfin plover
#

I don't have an excellent example handy. I will someday.

icy sable
#

Because there is no case where going around structured concurrency makes things better

civic turret
# delicate quest `scope` already does that

Does what, runs the spawning thread concurrently? Maybe I should have been more specific. I meant the scope method doesn't return until after joining all spawned threads. That limits what you can do concurrently with the spawning thread.

delicate quest
#

Anything you want to be in parallel with the spawned threads can be done inside scope.

civic turret
#

These are fairly contrived examples, but there are some things you can't do:

  1. You can't exit the spawning thread. 🙂
  2. You can't/shouldn't spawn scoped threads from an async context, since you would block.
delicate quest
#

The second one is fine as long as the async runtime is blocked on inside the scope.

civic turret
#

Of course.

#

Rust is about writing things more reliably as you said. But it is also about not limiting what you can do. You may not want to use fire-and-forget, but others may need it.

dusk smelt
#

not sure how helpful this is, but it's always possible to unplug a device, crack the mainboard in half while its running, etc which can prevent any code from executing after that point. that seems to be irrelevant here though since it's not mission critical that the drop impl runs it seems