Reputation: 33
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.
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
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(())
}
Upvotes: 1