1

I'm learning Rust, and I have problem with understanding passing objects by reference and lifetimes. I decided to create a simple REST (Axum + HashMap as database). I got a problem, because I do not know how to properly set database instance to save and retrieve from.

I have database.rs file:

pub struct Database<'a> {
    pub players: HashMap<u32, &'a Player>
}

impl<'a>  Database<'a>   {
    pub fn save_to_db(&mut self, player: &'a Player) -> () {
        self.players.insert(player.id, player);
    }

}

File routes.rs which get JSON passed from user:

pub struct Routes<'a> {
    pub(crate) database: Database<'a>
}

impl<'a> Routes<'a> {
    pub async fn create_player(&mut self, Json(payload): Json<CreatePlayer>) -> impl IntoResponse {
        let player = Player {
            id: 1,
            name: payload.name,
            age: payload.age,
            position: payload.position,
        };

        self.database.save_to_db(&player);

        (StatusCode::OK, Json(player))
    }

    pub async fn get_player(Path(_): Path<String>) -> impl IntoResponse {
        let player = Player {
            id: 1,
            name: String::from("Some Player"),
            age: 28,
            position: String::from("midfielder"),
        };

        (StatusCode::OK, Json(player))
    }
}

and main.rs to run it all:

mod http;
use crate::http::routes::Routes;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let routes = Routes {
        database: Database {
            players: HashMap::new()
        }
    };

    let router = Router::new()
        .route("/player/:id", get(routes.get_player()))
        .route("/player", post(routes.create_player));


    axum::Server::bind(&"0.0.0.0:8080".parse().unwrap())
        .serve(router.into_make_service())
        .await
        .unwrap();
}

The problem is with these lines:

 .route("/player/:id", get(routes.get_player()))
 .route("/player", post(routes.create_player));

It is impossible in Axum to pass my database object here. I tried also to move this:

let routes = Routes {
    database: Database {
        players: HashMap::new()
    }
};

into routes.rs file but the problem would be same - could not pass self into get_player and create_player methods.

How should I create an instance of my HashMap database and how to pass it correctly through the whole application?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Developus
  • 1,400
  • 2
  • 14
  • 50
  • 1
    I see lots of potential issues here...your database struct is effectively a global singleton and so could be given a `'static` lifetime to simplify things. Your hashmap is trying to store player references but should almost certainly store *owned* players instead: if the hashmap doesn't own the memory who does? In what scope and for what lifetime? But really, the problem here is you're trying to do too much all at once. Write a program with your "database" that just makes insert and retrieval calls and dumps to STDOUT or something before you mix in asynchrony and network calls. – Jared Smith Aug 30 '23 at 11:46
  • So how this `HashMap` signature should looks like? `HashMap` or smth else? Also if I want to have it "global" I need to include it in some struct, yes? No other way? Asnync is added for REST functions only – Developus Aug 30 '23 at 11:54
  • 1
    @Developus I think he's trying to say `HashMap`. – Finomnis Aug 30 '23 at 12:02
  • @Developus what Finomnis said: data structures in Rust generally need to own the memory for their contents. – Jared Smith Aug 30 '23 at 12:06
  • But I already have `HashMap` so this map owns memory now, as I understand? But is there any good example or common rules how to store this kind of data in collections? – Developus Aug 30 '23 at 12:09
  • 1
    @Developus I added an "answer", it will probably be easier to talk about that code, please @ me with whatever questions you still have about it. – Jared Smith Aug 30 '23 at 12:23

1 Answers1

1

Sorry this may not be entirely an answer but it's easier than the back and forth in the comments.

You can't actually use a struct field as a hashmap key when the struct is the value as this would make the hashmap itself a self-referential struct, an undertaking fraught with peril. You can always just copy the key, it's only a 32-bit integer:

use std::collections::HashMap;

#[derive(Debug)]
struct Player {
    id: i32,
    name: String,
    position: String,
}

struct Database {
    players: HashMap<i32, Player>
}

impl Database {
    pub fn add_player(&mut self, p: Player) -> () {
        let pid = p.id.clone();
        self.players.insert(pid, p);
    }
    
    pub fn get_player(&self, id: &i32) -> Option<&Player> {
        self.players.get(id)
    }
}

fn main() -> () {
    let p = Player {
        id: 0,
        name: String::from("Tom Brady"),
        position: String::from("Quarterback"),
    };
    
    let mut db = Database {
        players: HashMap::new()
    };
    
    db.add_player(p);
    let retrieved = db.get_player(&0);
    println!("{:?}", retrieved);
}

Playground

As for Player vs &Player as the hashmap value type, if you are constructing the player struct in a route handler function, the memory for that player is deallocated as soon as the function returns and the player is out of scope. So you can't store a borrowed reference in a data structure: you'd essentially have a dangling pointer to freed memory (same reason the struct fields are String and not &str). You want to transfer ownership of the player to the hashmap.

Jared Smith
  • 19,721
  • 5
  • 45
  • 83
  • `Rc` is useless. `i32` is better in all aspects. – Chayim Friedman Aug 30 '23 at 12:45
  • @ChayimFriedman I'm guessing because the cost of cloning the value to use as a hashmap key is equal to or less than the cost of constructing the rc and bumping the ref count? – Jared Smith Aug 30 '23 at 12:48
  • 1
    Because it's not mutable, so changing one does not affect the other, and the cost of allocating an `Rc` is very big compared to just creating an integer, it occupies 6x more space on the heap and 2x more space for each pointer (on 64-bit machine), cloning it (bumping the refcount) is more expensive than copying an integer, and it litters your code with calls to `clone()`. – Chayim Friedman Aug 30 '23 at 13:01
  • Thanks. So in Rust there is no rules to separate code into different files? The perfect for me would be to have `struct Player`, database, routes and main in different files, but as I understand it is connected with memory and object borrowing? So the only solution here is to have everything in one file? Also, why you pass `id: &i32` with reference? Doesn't simple types always copied? – Developus Aug 30 '23 at 17:35
  • And if I want to have this one `let mut db = Database { players: HashMap::new() };` in my `main.rs`, how should I pass it correctly to routes and insert player there? – Developus Aug 30 '23 at 17:38
  • 1
    @Developus "So in Rust there is no rules to separate code into different files?" correct. "As I understand it is connected with memory and object borrowing?" no, *scope* is associated with memory and borrowing. You can't (normally) have a reference to memory like a borrow live *longer than the scope it was allocated in lasts*. Rust like many other languages has function scope. So if you allocate memory to hold a value inside a function, *once that function returns the thing is gone* **unless** you transfer ownership of it (like transferring it to the caller by returning an owned value). – Jared Smith Aug 30 '23 at 17:49
  • "how should I pass it correctly to routes and insert player there?" by taking a mutable references &mut to self to the routes and accessing self.db. Since only one of your routes will be called at a time, it can have the one exclusive mutable reference. When a route creates a new Player it will transfer ownership of the memory holding the struct to your HashMap (otherwise it would be deallocated when the route handler function returned). – Jared Smith Aug 30 '23 at 18:03
  • I may have stripped down too much, [check this out](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=041b56e9e7f3eebcbd5a27a9de3ea115), here it might be a little more clear how the definitions of routes and database and player can all be in different files, and main just wires everything up. – Jared Smith Aug 30 '23 at 18:10
  • @JaredSmith thanks. I have updated my program https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f6823be9103702cae1b445dfe2af4107 but I got an compilation error in `get_player` method: `(StatusCode::OK, Json(*found)) move occurs because `*found` has type `Player`, which does not implement the `Copy` trait` When I added `Copy` to `Player` struct it also said: `name` field does not implement Copy. You know where could be problem here? – Developus Aug 30 '23 at 20:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/255120/discussion-between-jared-smith-and-developus). – Jared Smith Aug 30 '23 at 20:08