0

Background

I have been learning rust recently and my most recent project involves setting a global APP_STATE that can then be accessed throughout the app. There are a few other globals as well.

Note: These variables pretty much need to be globals, otherwise I will have to pass them as arguments into every function and trait I have - which is not very elegant.

The Problem

The aforementioned globals are mutable, i.e they are represented like the following:

pub Struct AppState {
    running: bool
    suspended: bool
}

static mut APP_STATE = AppState {running: true, suspended:false}

To access these values, I must use unsafe like so: (Ignore the logic of code itself, just an example)


pub unsafe fn create_app {
    APP_STATE.running = true;
    APP_STATE.suspended = false;
}

unsafe fn confirm_app_state_valid() {
    if APP_STATE.running == APP_STATE.suspended { // equality on booleans is just the XNOR(Logical Bi-conditional) operator.
        fatal("Fatal! App was both running and suspended at same time. Could not resolve. Crashed")
    };
}

The Question

How can I change my code to

  1. remove the unsafe (I understand why having unsafe is needed for mutable statics - avoid race conditions). Note that my app uses concurrency and performance is critical(graphics).

I want to remove the unsafe or atleast reduce its usage - currently it encompasses the entire main loop.

  1. not have to pass state as an argument everywhere
Already looked at this. I did not understand how I might implement the solution provided here.
Naitik Mundra
  • 418
  • 3
  • 14
  • 1
    Sidenote: you could make ensure at compile time that appstate is valid by using an Enum instead of a struct of bools – mhutter Oct 16 '22 at 08:57
  • As for your state problem, THE BOOK might help: https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html – mhutter Oct 16 '22 at 09:00
  • @mhutter I know that all the answers are always in the book. Problem is I cannot find the correct solution that is also not too verbose. Mutex/Arc or Mutex/Rc or Box or something else? And how do I implement it? – Naitik Mundra Oct 16 '22 at 09:13
  • As for the enums, thanks for the suggestion! – Naitik Mundra Oct 16 '22 at 09:14
  • "otherwise I will have to pass them as arguments into every function and trait I have - which is clearly not optimal." citation needed – Colonel Thirty Two Oct 16 '22 at 15:37
  • @ColonelThirtyTwo Improved my wording now. I meant not very elegant. However, now after implementing and testing both this and the soltuion provided by prog-fh, I have actually decided to use arguments, considering them better practice and realising that with some small changes, I dont really need all of them, especially not the mutable ones to be global. – Naitik Mundra Oct 16 '22 at 16:53

1 Answers1

2

If you absolutely want some static global state, you can use once_cell in conjunction with a Mutex (see the example below).

However, I don't understand your remark « which is clearly not optimal » about passing the state as a parameter; do you mean inelegant or inefficient? Moreover, you state « performance is critical »; in my opinion, this static global state requiring runtime borrow checking at each access is less efficient than the usual static borrow checking.

use once_cell::sync::OnceCell;
use std::sync::{Mutex, MutexGuard};

#[derive(Debug)]
struct AppState {
    running: bool,
    suspended: bool,
}
static APP_STATE: OnceCell<Mutex<AppState>> = OnceCell::new();

fn access_app_state() -> MutexGuard<'static, AppState> {
    APP_STATE.get().unwrap().lock().unwrap()
}

fn confirm_app_state_valid() {
    let app_state = access_app_state();
    if app_state.running == app_state.suspended {
        panic!("Fatal! App was both running and suspended at same time...");
    }
    println!("App state is correct: {:?}", app_state);
}

fn change_app_state() {
    let mut app_state = access_app_state();
    app_state.running = !app_state.running;
    app_state.suspended = !app_state.suspended;
}

fn main() {
    APP_STATE
        .set(Mutex::new(AppState {
            running: true,
            suspended: false,
        }))
        .unwrap();
    confirm_app_state_valid();
    change_app_state();
    confirm_app_state_valid();
}
/*
App state is correct: AppState { running: true, suspended: false }
App state is correct: AppState { running: false, suspended: true }
*/

As stated by Chayim Friedman in a comment, since Rust 1.63 Mutex::new() is const. We can get rid of once_cell and just initialise the global mutex with a content which is known at compile-time.

use std::sync::{Mutex, MutexGuard};

#[derive(Debug)]
struct AppState {
    running: bool,
    suspended: bool,
}
static APP_STATE: Mutex<AppState> = Mutex::new(AppState {
    running: true,
    suspended: false,
});

fn access_app_state() -> MutexGuard<'static, AppState> {
    APP_STATE.lock().unwrap()
}

fn confirm_app_state_valid() {
    let app_state = access_app_state();
    if app_state.running == app_state.suspended {
        panic!("Fatal! App was both running and suspended at same time...");
    }
    println!("App state is correct: {:?}", app_state);
}

fn change_app_state() {
    let mut app_state = access_app_state();
    app_state.running = !app_state.running;
    app_state.suspended = !app_state.suspended;
}

fn main() {
    confirm_app_state_valid();
    change_app_state();
    confirm_app_state_valid();
}
/*
App state is correct: AppState { running: true, suspended: false }
App state is correct: AppState { running: false, suspended: true }
*/
prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • By optimal, i meant inelegant. But I can see now compare the two methods and see which one is better. So that is why I wanted a solution to statics. – Naitik Mundra Oct 16 '22 at 09:33
  • Why would this require runtime borrow checking? A link to the book/documentation would be helpful? – Naitik Mundra Oct 16 '22 at 09:34
  • Side question, if you were designing a similar problem? Which solution would you choose? – Naitik Mundra Oct 16 '22 at 09:35
  • 1
    @NaitikMundra Each access to the global state requires a check by once_cell that it is initialised, and in order to allow mutations wee need a [mutex](https://doc.rust-lang.org/std/sync/struct.Mutex.html) which perform runtime checks too. – prog-fh Oct 16 '22 at 09:42
  • 1
    @NaitikMundra I don't know precisely how I would organise your application. Maybe you still need a kind of big struct containing anything in your application but you could rely on [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) to borrow some of its parts when needed. It's still runtime check however, but you can split these checks on different parts of the struct and go on with static check on these parts. [This is not an easy problem](https://stackoverflow.com/q/71980809/11527076). – prog-fh Oct 16 '22 at 09:51
  • (a) Prefer `Lazy` to `OnceCell`. (b) You don't need `once_cell` anymore, `Mutex::new()` is const now! – Chayim Friedman Oct 17 '22 at 22:49
  • @ChayimFriedman Why should we prefer `Lazy` to `OnceCell`? Is this advice specific to this simple example, or is it general? If the initialisation of the global depended upon some command line arguments for example, I would find more explicit to initialise it once for all at the beginning of the process, when the required arguments are made available, and assume it is necessarily fully initialised starting from now, than letting this global accessible but in the state « this will be fully initialised one day or another ». – prog-fh Oct 18 '22 at 07:28
  • @prog-fh I went by this example. I can understand why to use `OnceCell` for command line arguments (although personally I would not). – Chayim Friedman Oct 18 '22 at 17:54