How to make a Rust singleton's destructor run?
Asked Answered
F

1

3

These are the ways I know of to create singletons in Rust:

#[macro_use]
extern crate lazy_static;

use std::sync::{Mutex, Once, ONCE_INIT};

#[derive(Debug)]
struct A(usize);
impl Drop for A {
    fn drop(&mut self) {
        // This is never executed automatically.
        println!(
            "Dropping {:?} - Important stuff such as release file-handles etc.",
            *self
        );
    }
}

// ------------------ METHOD 0 -------------------
static PLAIN_OBJ: A = A(0);

// ------------------ METHOD 1 -------------------
lazy_static! {
    static ref OBJ: Mutex<A> = Mutex::new(A(1));
}

// ------------------ METHOD 2 -------------------
fn get() -> &'static Mutex<A> {
    static mut OBJ: *const Mutex<A> = 0 as *const Mutex<A>;
    static ONCE: Once = ONCE_INIT;
    ONCE.call_once(|| unsafe {
        OBJ = Box::into_raw(Box::new(Mutex::new(A(2))));
    });
    unsafe { &*OBJ }
}

fn main() {
    println!("Obj = {:?}", PLAIN_OBJ); // A(0)
    println!("Obj = {:?}", *OBJ.lock().unwrap()); // A(1)
    println!("Obj = {:?}", *get().lock().unwrap()); // A(2)
}

None of these call A's destructor (drop()) at program exit. This is expected behaviour for Method 2 (which is heap allocated), but I hadn't looked into the implementation of lazy_static! to know it was going to be similar.

There is no RAII here. I could achieve that behaviour of an RAII singleton in C++ (I used to code in C++ until a year a back, so most of my comparisons relate to it - I don't know many other languages) using function local statics:

A& get() {
  static A obj; // thread-safe creation with C++11 guarantees
  return obj;
}

This is probably allocated/created (lazily) in implementation defined area and is valid for the lifetime of the program. When the program terminates, the destructor is deterministically run. We need to avoid accessing it from destructors of other statics, but I have never run into that.

I might need to release resources and I want drop() to be run. Right now, I end up doing it manually just before program termination (towards the end of main after all threads have joined etc.).

I don't even know how to do this using lazy_static! so I have avoided using it and only go for Method 2 where I can manually destroy it at the end.

I don't want to do this; is there a way I can have such a RAII behaved singleton in Rust?

Funda answered 10/9, 2016 at 13:17 Comment(0)
I
8

Singletons in particular, and global constructors/destructors in general, are a bane (especially in language such as C++).

I would say the main (functional) issues they cause are known respectively as static initialization (resp. destruction) order fiasco. That is, it is easy to accidentally create a dependency cycle between those globals, and even without such a cycle it is not immediately clear to compiler in which order they should be built/destroyed.

They may also cause other issues: slower start-up, accidentally shared memory, ...

In Rust, the attitude adopted has been No life before/after main. As such, attempting to get the C++ behavior is probably not going to work as expected.

You will get much greater language support if you:

  • drop the global aspect
  • drop the attempt at having a single instance

(and as a bonus, it'll be so much easier to test in parallel, too)

My recommendation, thus, is to simply stick with local variables. Instantiate it in main, pass it by value/reference down the call-stack, and not only do you avoid those tricky initialization order issue, you also get destruction.

Ideation answered 10/9, 2016 at 13:53 Comment(5)
We need a bot to automatically reply to every question with the word "singleton" with "you don't really want to do that".Crypt
In addition to the excellent points here, I'd also add multithreading to the list of problematic cases.Crypt
It has it's place - here's a simple example in Rust itself: say you need single object to persist across all your tests (which run as separate threads in parallel during cargo test). Maybe it's simulating a network, writing to a single file after bunch of transformations depending on global parameters which change as tests run and is important for other tests to see the changes. Pattern is too abstract to be simply trashed and even if some should be used rarely, they still have their place. But you do answer my question that it's not possible - so if i get no others i'll accept this - thanksFunda
@ustulation: It can be useful maybe, and the lazy_static! macro exists after all. However note how lazy_static! avoids the initialization order issue by being created lazily and avoids the destruction order issue by... not being destructed ever. In the case of tests... I am not comfortable in sharing a common item between randomly scheduled parallel tests, sounds like a recipe for spurious failures.Ideation
@Funda I agree that there are cases where it's useful, but I'd argue that for every good usage there are a great number of misuses. I think your "fake network in tests" example is an example of a misuse, for exactly the reason that Matthieu M. espouses.Crypt

© 2022 - 2024 — McMap. All rights reserved.