Socket.split, lifetimes and structs

The various socket objects, including tokio::net::UnixStream have a method called split(), which returns a ReadHalf and WriteHalf. This is nice because it means that one can write a thread that reads requests from a socket, while other threads are more free to write to that socket.
I do IPC between two processes (processes, not threads, because they are in unique Linux network namespaces), and I would like to package this all up in a nice module that I can easily test.
Previously, I had a few pub fn in that module that got passed stuff in, and it worked okay, until I recognized that I needed to keep some extra state around to deal with reads that returned more than one serialized object. Okay, no problem, throw it all into a struct, and do the impl correctly.

Given a unix socket (from a pair/fork), I'd like to have an initializer function that returns the struct to the caller, having stored away the created UnixStream (from_std), and split it up into ReadHalf/WriteHalf.
The spit() function is broken in design I think. It doesn't consume (take onwership) of the socket it is given. It should perhaps do that, wrap an Arc around it, and reference it from each of the ReadHalf/WriteHalf objects that it creates. AFAIK, it just takes a reference. So the lifetime of the socket has to exceed that of the ReadHalf/WriteHalf. That's easy to arrange in a straight off function scope,
but it seems impossible if you want to create a struct to wrap it all up, OOP-like.

I've played with various Box<> and Option<> versions such that I could put the socket into a struct, then split that, taking a reference to that socket (which wouldn't move again), and then put the ReadHalf/WriteHalf into the Box, or the Option, but the borrow tracker doesn't accept that all objects in the struct will have the same lifetime. Or at least, I don't know how to tell it that, and it complains that I've tried to return something owned by the function.

This is the most naive way I've tried, so that you get what I'm trying to do:

pub struct ControlStream<'a> {
    reader: ReadHalf<'a>,
    writer: WriteHalf<'a>
}

impl ControlStream<'_> {
    pub fn child<'a>(mut sock: &'a UnixStream) -> ControlStream<'a> {
        let (r, w) = sock.split();
        ControlStream {
            reader: r,
            writer: w
        }
    }

In another part of my code, I ran into the same issue, and this is the version I tried with Option<>, which is a pain elsewhere, because it will never be None again, but I still have to check.

pub struct Acp<'a> {
    pub debug:         DebugOptions,
    pub child_socket:  tokio::net::UnixStream,
    pub child_stream:  Option<control::ControlStream<'a>>,
    pub acppid:        Pid
}

impl Acp<'_> {
    pub fn from_acp_init<'a>(init: AcpInit) -> Acp<'a> {
        let c_socket = tokio::net::UnixStream::from_std(init.child_io).unwrap();

        let mut a1 = Acp { child_socket: c_socket,
                           child_stream: None,
                           debug:        DebugOptions::empty(),
                           acppid:       init.dullpid };

        let c_stream    = control::ControlStream::parent(&a1.child_socket);
        a1.child_stream = Some(c_stream);

        return a1;
    }
}

This complains with:

error[E0505]: cannot move out of `a1` because it is borrowed
  --> src/acp.rs:97:16
   |
86 |     pub fn from_acp_init<'a>(init: AcpInit) -> Acp<'a> {
   |                          -- lifetime `'a` defined here
...
94 |         let c_stream    = control::ControlStream::parent(&a1.child_socket);
   |                                                          ---------------- borrow of `a1.child_socket` occurs here
...
97 |         return a1;
   |                ^^
   |                |
   |                move out of `a1` occurs here
   |                returning this value requires that `a1.child_socket` is borrowed for `'a`

maybe this part is solvable in some way.
I recognize that this is really a redo of a common thread about how one can reference one field from another within a struct, which basically doesn't work.

I am open to other models of how to do this kind of thing. Most seem to suggest that the caller needs to allocate all the relevant stuff for the routine, which I understand from a borrow-checker point of view, but is totally non-DRY, and annoying.

I just want to create a non-trivial object (on the heap), pass ownership of it back to the caller, and have the borrow checker keep it safe.

You are not going to be able to store the ControlStream in your Acp struct unless you change it to use into_split and take ownership of the UnixStream.

That was very helpful to point out. In reading the difference, I didn't understand that this was really about ownership. It goes on about threads, which obscured that the difference was about ownership. Yes, you are right, I would need into_split() to be able to do things from different tasks. I would be unable to move the halves into different threads, because of ownership/reference.
I will have to find the documentation and suggest a change.

If it is useful I can post the resulting code.

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.