Global mutable variables in Rust

  1. Background:

I want to create an app using Tauri. It allows you to create app with html/JS frontend and Rust backend.

For communicating between frontend and backend, you can create tauri-commands using #[tauri::command] macro. This allows you to create functions that can be called in the frontend.

I was trying to implement the game-of-life in rust and display it using tauri.

  1. Game Implementation Overview:

I have declared the game struct

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}

pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}

There are three functions that are important:

impl Universe {
  pub fn new() -> Universe // for creating the Game object
  pub fn render(&self) -> String // for getting the game state as string, so that it can be rendered on the fronend. 
  pub fn tick(&mut self) // For changing the cell to the next state
}
  1. Problems I am facing

I want to create two tauri-commands

#[tauri::command]
fn get_game_state() -> String

#[tauri::command]
fn tick_game() 

The problem is that I cannot pass the game object to these functions as a parameter. So I thought of creating a global game object. Rust was giving error with this, so I used an external crate: lazy_static to initialize the global game object.

lazy_static! {
    static ref GAME: Universe = {
        let mut game = Universe::new();
        game
    };
}

Now I'm implementing the tauri-commands as follows:

#[tauri::command]
fn get_game_state() -> String {
    GAME.render()
}

#[tauri::command]
fn tick_game() {
    GAME.tick();
}

However, the rust compiler is giving the following error:
error[E0596]: cannot borrow data in dereference of GAMEas mutable

30 |     GAME.tick();
   |     ^^^^^^^^^^^ cannot borrow as mutable

How do I fix this error ?. Also, is there a better way to manage my game state instead of using global variable. ?

Looks like you might need a RwLock around your global state. This can hand out mutable or immutable locks so your state can only be modified in one place at a time.

2 Likes

You can wrap it in a Mutex or an RwLock like @tuffy said ( playground ):

lazy_static! {
    static ref GAME: Mutex<Universe> = {
        let game = Universe::new();
        Mutex::new(game)
    };
}

fn main() {
    GAME.lock().unwrap().tick();
}

This means that every modification to GAME will be required to make sure it is either the only one using it ( with a Mutex ) or that it follows the Rust borrowing rules of many readers or one writer ( with an RwLock ).

1 Like

Hey, we use Tauri for an application at work!

You should use Builder::manage() to tell Tauri to manage a piece of state and make it accessible to your commands later on. All you'll need to do is add a universe: tauri::State<Universe> argument to your command function.

I'll copy the example here:

use std::{collections::HashMap, sync::Mutex};
use tauri::State;
// here we use Mutex to achieve interior mutability
struct Storage {
  store: Mutex<HashMap<u64, String>>,
}
struct Connection;
struct DbConnection {
  db: Mutex<Option<Connection>>,
}

#[tauri::command]
fn connect(connection: State<DbConnection>) {
  // initialize the connection, mutating the state with interior mutability
  *connection.db.lock().unwrap() = Some(Connection {});
}

#[tauri::command]
fn storage_insert(key: u64, value: String, storage: State<Storage>) {
  // mutate the storage behind the Mutex
  storage.store.lock().unwrap().insert(key, value);
}

tauri::Builder::default()
  .manage(Storage { store: Default::default() })
  .manage(DbConnection { db: Default::default() })
  .invoke_handler(tauri::generate_handler![connect, storage_insert])
  // on an actual app, remove the string argument
  .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
  .expect("error while running tauri application");

Global variables really hurt you in the long run because it makes it hard to manage access to the state, and their singleton nature makes testing quite difficult.

6 Likes

Ooh, that's a nice solution! I used Tauri a while back now, it looks like it's gotten some upgrades. :slight_smile: And they've gotten 1.0 out now!