Rust: How to solve "one type is more general than the other" error when using thread local OnceCell?

I'm on 1.70 version of rust playing with recently added OnceCell in std.

I'm trying to create global configuration object in thread local storage wrapped in OnceCell. I do that purposefully to avoid unnecessary synchronization overhead of std::sync::OnceLock since my application will be single threaded.

Here's some sample code:

thread_local! {
    static CONFIGURATION: OnceCell<Configuration> = OnceCell::new();
}

pub fn init() -> Result<(), ConfigurationLoadingError> {
    let configuration = Configuration::try_from_file()?;
    CONFIGURATION.with(|cell| {
        cell.get_or_init(|| configuration);
    });
    Ok(())
}

pub struct Configuration {
    source_file: PathBuf,
    // ...
}

pub struct ConfigurationLoadingError;

The issue appears when I try to access CONFIGURATION in main:

fn main() -> Result<(), ConfigurationLoadingError> {
    init()?;
    let default_config = CONFIGURATION.with(|cell| cell.get()).unwrap();
    let _reader = BufReader::new(
        File::create(&default_config.source_file)?);
    Ok(())
}

On compilation compiler reports the following:

error: lifetime may not live long enough
  --> src/main.rs:70:52
   |
70 |     let default_config = CONFIGURATION.with(|cell| cell.get()).unwrap();
   |                                              ----- ^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                                              |   |
   |                                              |   return type of closure is Option<&'2 Configuration>
   |                                              has type `&'1 OnceCell<Configuration>`                        ^

I'm guessing that compiler has no way of knowing that contents of OnceCell will never change, which alongside CONFIGURATION being global means that references to CONFIGURATION are effectively 'static.

So, my question is: Is there any non-unsafe way for me to informing the compiler that reference to CONFIGURATION contents will indeed live long enough? Perhaps I'm terribly misusing the OnceCell api? From examples that I've seen this seems like a main use-case for this type and as such I would expect that using it would be unsafe free (and little less verbose).

Here's minimal working example on playground.

I know that there are many crates that supply functionality I'm trying to implement -- This project is only for educational purposes only.

Offtop

Btw I've tried initializing CONFIGURATION using OnceCell::from directly without using new and get_or_init but then I cannot bubble up loading errors to main and exit with an error. These kinds or errors are expected outcome of the program so panic does not fit here. I considered std::process::exit but I'm not too sure about the destructor situation described in docs:

If a clean shutdown is needed it is recommended to only call this function at a known point where there are no more destructors left to run; or, preferably, simply return a type implementing Termination (such as ExitCode or Result) from the main function and avoid this function altogether

So I followed the advice to simply return errors from main. Is there any reason why to worry about calling exit here? It there a better way of doing it altogether without any external crates?

I've tried searching on SO and other forums but with this feature being so new there was nothing to be found.

Upvotes: 2

Views: 295

Answers (1)

Chayim Friedman
Chayim Friedman

Reputation: 70950

The problem is just as with any other thread-local: the compiler cannot prove you won't move the reference to your data to another thread and use it after the original thread was destroyed, i.e. a use-after-free.

My advice: just use OnceLock in a normal static. As I wrote in a comment, it'll probably be even faster.

But if you really want, you can wrap your data in Rc and clone it:

thread_local! {
    static CONFIGURATION: OnceCell<Rc<Configuration>> = OnceCell::new();
}

pub fn init() -> Result<(), ConfigurationLoadingError> {
    let configuration = Configuration::try_from_file()?;
    CONFIGURATION.with(|cell| {
        cell.get_or_init(|| Rc::new(configuration));
    });
    Ok(())
}

fn main() -> Result<(), ConfigurationLoadingError> {
    init()?;
    let default_config = CONFIGURATION.with(|cell| Rc::clone(cell.get().unwrap()));
    let _reader = BufReader::new(
        File::create(&default_config.source_file)?);
    Ok(())
}

Playground.

Upvotes: 1

Related Questions