Fight against borrow checker?

I am trying to fight against borrow checker for about 2hours... And I am tired...

use tokio::net::TcpListener;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    let mut clients = HashMap::new();
    loop {
        let (client, socket_addr) = listener.accept().await?;
        println!("Client connected on IP: {}", socket_addr);
        clients.insert(socket_addr, &client);

        tokio::spawn(async move {
            loop {
                //  how to use client object here ?
               let _result_type = client.readable().await;
               // ...
            }
        });
    }
}

The error message is very clear, an that is, when client goes out of loop block, it drop its value,

But how to fix that, I think cloning does not solve it. Even stream doesn't support cloning, Does it?

error[E0597]: `client` does not live long enough
  --> src/main.rs:11:37
   |
11 |         clients.insert(socket_addr, &client);
   |         -------                     ^^^^^^^ borrowed value does not live long enough
   |         |
   |         borrow later used here
...
19 |     }
   |     - `client` dropped here while still borrowed

error[E0505]: cannot move out of `client` because it is borrowed
  --> src/main.rs:13:33
   |
11 |           clients.insert(socket_addr, &client);
   |           -------                     ------- borrow of `client` occurs here
   |           |
   |           borrow later used here
12 | 
13 |           tokio::spawn(async move {
   |  _________________________________^
14 | |             loop {
15 | |                let _result_type = client.readable().await;
   | |                                   ------ move occurs due to use in generator
16 | |                // ...
17 | |             }
18 | |         });
   | |_________^ move out of `client` occurs here
1 Like

Looks like reference counting could solve your problem. Try wrapping the TcpStream in an Rc.

What exactly are you trying to do? Why are you keeping the clients in a map?

Follow the tutorial on how to share state. Rc won't be useful since tokio may schedule each async task on a different thread.

This is never going to work. Don't put the socket into a hash map. Don't try to clone the socket. Don't try to reference count the socket with Rc or Arc. When you work with sockets, every socket should have exactly one owner, or it should be split in to a read and write half, with each half having a single owner.

If you want something to put in a map, you should build an actor and store the actor handle. There's an example here, which implements a chat server.

5 Likes

I suppose it would be possible to put the socket in a hashmap, but then where to go from there. The map would take ownership, which means you'd have to lock and borrow the entire hashmap for anything you'd want to do with the socket (including sending/receiving data), which in turn means you'd have a serial (non-concurrent) application.

This particular problem can be solved by storing Mutex<Socket> in the hash map instead. (Not to say I'm recommending it.)

There are some things you can try where you put the socket in the map and access it in the map when you need it, or you use a lock, but I've seen quite a few people try them, and it always turns out poorly in my experience. Actors are just a much better pattern for this kind of problem.

"Don't put the socket into a hash map. Don't try to clone the socket. Don't try to reference count the socket with Rc or Arc . Every socket should have exactly one owner" -

But why @alice ? specially with Rc or Arc? Or is there any alternative, to share multiple shared reference across deep scoop?

What are those disadvantage, If I put the socket into a hash map?...

Can I use lazy_static ? initialize it (clients map) at runtime ? Because clients object would live entire program...

        let (client, socket_addr) = listener.accept().await?;
        println!("Client connected on IP: {}", socket_addr);
        EDIT: let rc = Rc::new(client);
        EDIT: clients.insert(socket_addr, rc.clone());

        tokio::spawn(async move {
            loop {
                //  how to use client object here ?
               EDIT: let _result_type = rc.readable().await;
               // ...
            }
        });

Does this solve your borrow checker problems? (I have no idea if this is a good idea.)

The problem is that to put it in the hash map, then either it must be only in the map, or it must be shared.

If it's only in the map, you will run in to issues with only one connection in the map being able to operate at the time.

If it's shared, you will run in to issues with tasks interfering with each other if they both try to write, or both try to read. (sharing is ok if there are exactly two owners, with one of them reading only, and the other writing only)

As for whether there are alternatives, the answer is yes. The alternative is called the actor pattern. There are links in my post above for more details.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.