Mess in lifetime annotations

I was a C++ coder hence Rust's lifetime annotations are new to me.
As code shown below, Transporter instance would contain a set of Connection instances.
The lifetime of Connection instance must not outlive Transporter instance's lifetime.

I tried several approaches in annotations but always get compliation errors.
Please can someone fix the annotations and explain what the mistake is

Appreciate your help :slight_smile:

use std::sync::atomic::{ Ordering, AtomicUsize};
use bytes::Bytes;
use std::sync::Mutex;
use std::collections::VecDeque;
use std::cell::Cell;
use std::collections::HashMap;
use futures::stream::FuturesUnordered;
use futures::channel::oneshot::Receiver;

// Represents a connection
#[allow(dead_code)]
pub struct Connection<'a> {
    id : u32,
    initiator_id: u32,
    total_bytes : AtomicUsize,
    queue : Mutex<VecDeque<Bytes>>,
    transporter : &'a Transporter<'a>,  // an instance of Transporter can’t outlive the reference Connection holds in its transporter field.
}

impl Connection<'_> {
    fn new<'a>(id: u32, transporter : &Transporter) -> Connection<'a> {
        Connection{
            id : id,
            initiator_id : 1,
            total_bytes : AtomicUsize::new(0),
            queue : Mutex::new(VecDeque::new()),
            transporter : transporter,
        }
    }

    pub fn send(&self, buf:&[u8]) -> usize{
        let mut queue = self.queue.lock().unwrap();
        queue.push_back(Bytes::from(buf));
        self.total_bytes.fetch_add(buf.len(), Ordering::Relaxed) + buf.len()
    }
}


#[allow(dead_code)]
pub struct Transporter<'a> {
    id_generator : AtomicUsize,
    receivers : Mutex<Cell<FuturesUnordered<Receiver<()>>>>,
    connections : Mutex<HashMap<u32, Box<Connection<'a>>>>, // Connection cannot outlive Transporter's lifetime
}


impl Transporter<'_> {
    pub fn new() ->  Transporter {
        Transporter{
            id_generator : AtomicUsize::new(1),
            receivers : Mutex::new(Cell::new(FuturesUnordered::new())),
            connections : Mutex::new(HashMap::new()),
        }
    }

    pub fn new_connection(&self) -> &Connection {
        let id : u32 = (self.id_generator.fetch_add(1, Ordering::SeqCst) % 4294967296) as u32;
        let conn = Connection::new( id, self);
        
        let mut connections = self.connections.lock().unwrap();
        connections.insert( id, Box::new(conn));

        &conn
    }

}

Not expert on lifetimes, but it seems Transporter has reference to Connection and Connection has reference to Transporter. Circular reference are troublesome in Rust.

Compiler is concerned about situation when Connection is destroyed but Transporter is still alive and now contains reference to invalid object.

In Rust, ownership model is part of design, whereas in C(++) it is ok to have invalid objects during construct and shutdown phases, as long as you remember which members you can not touch at which phase :slight_smile:

As it is design issue and not syntax one, you'd need to explain what it is you are trying to achieve.

1 Like

Thank you @vchekan :slight_smile: So circular reference is not allowed in Rust

One more thing.
In C++, when I returning an instance. It triggers overriden = operator or copy-constructor to create a copied instance outside of the function scope.

When I return a struct as below in Rust, the struct is copied after the function returns? Or it is a reference?

fn new(id: u32, transporter : &Transporter) -> Connection {
        Connection{
        }
}

When you pass/return something by value (i.e. the type is not prefixed with & or &mut, which would indicate it's a reference), two things can happen:

  • If the type implements the Copy trait, it will be copied. This is generally used for lightweight value structs (e.g. if you had a struct Vector { x: i32, y: i32 }, that would be a good candidate).
  • If the type does not implement Copy, it will be moved, and the value will no longer be accessible from the previous location.
    • I think moving is also implemented as a memcpy under the hood, but it's usually optimized away.

In this case, it doesn't really make any difference, but it's good to know the difference for the future.

1 Like

Rust references are absolutely not like C++ references, and it will cause you endless sorrow if you try to use them as such. Semantically, & is not a pointer, it's a temporary borrow. It's a read-only lock, except implemented at compile time rather than runtime.

Passing ownership by reference is done with Box, not & (Box compiles to a plain pointer).

Storing of temporary borrows in structs is a very rare thing. You probably don't intend for these structs to be temporary views of data stored and allocated elsewhere (a borrow never exists without the data also being owned somewhere else) bound to a function they were created in.

So you probably mean not to use & anywhere here, and store owned versions. This removes all of the lifetime annotations.

For transporter and connections mutual references you can:

  • Remove the parent reference, and figure out another way to track it. The borrow checker can't enforce that removal of child reference also removes parent reference, and won't let you have dangling pointers, so it forbids the entire pattern. https://rust-unofficial.github.io/too-many-lists/

  • Use a raw pointer for one of them, and ensure safety yourself.

  • Use shared references by wrapping them in Rc & Weak.

5 Likes

I certainly agree; however, it's noteworthy you can't write this kind of cyclical struct reference thing in C++, either, for the simple reason that C++ just doesn't allow you to put references in structs at all. You could do it with std::reference_wrapper, which is roughly equivalent to std::ptr::NonNull, or just with raw pointers which work the same as in Rust.

What Rust lacks is a way to automatically run code when the struct is moved -- there are no move constructors and you can't overload operator= -- so you need to guarantee the pointer is valid yourself, either by ensuring the struct is never moved or by updating the pointer when necessary. There are ways to write safe interfaces around both of these solutions; mostly they rely on closures. But I would recommend not doing that, and instead following up on one of @kornel's other suggestions.

Thank you, @17cupsofcoffee

@kornel @trentj Thank you

Good article for you @wangjia184. It discuss exact problem you have: Classes and students relation and they go over several options: use RefCells or "normalization" struct:
https://m-decoster.github.io//2017/01/16/fighting-borrowchk/

1 Like