2

I would like to wrap a low-level third-party API with my own struct and functions to make it more friendly. Unfortunately, the third-party API needs a reference to a socket in its constructor, which means I'd like my Struct to "own" the socket (someone has to own it so that it can be borrowed, right? and I'd like that hidden as an implementation detail in my API).

The third-party API looks something like this:

struct LowLevelApi<'a> {
    stream: &'a mut TcpStream,
    // ...
}

impl<'a> LowLevelApi<'a> {
    pub fn new(socket: &'a mut TcpStream, ... ) -> LowLevelApi<'a> {
        // ...
    }
}

I would like to make the interface to my function look like:

pub fn new(host: String, port: u16, ...) -> HighLevelApi {
    // ...
}

I tried this:

pub struct HighLevelApi<'a> {
    stream: TcpStream,
    low: LowLevelApi<'a>
}

impl <'a> HighLevelApi<'a> {
    pub fn new(host: String, port: u16) -> HighLevelApi<'a> {
        // Ignore lack of error checking for now
        let mut stream = TcpStream::connect(format!("{}:{}", host, port)).unwrap();
        HighLevelApi {
            stream,
            low: LowLevelApi::new(&mut stream)
        }
    }
}

Rust is (rightly) angry: It has no way of knowing that I'm not going to do something bad with the low field later. And even worse, I would need to somehow guarantee that when my structure gets dropped, low gets dropped first, and stream second (since by that point, any relationship between the two is lost - or rather, there never is/was a relationship between the two).

(actually, it's worse than that, because the stream local variable gets moved into the new struct, so the local can't possibly be borrowed by LowLevelApi, but I can't think of a way to initialize HighLevelApi with the stream from the struct, since there's no way to get a handle to that from within the struct's initialization, is there? But based on my guess about what would happen in the paragraph above, it doesn't really matter since it still wouldn't do what I wanted)

What are examples of the various techniques that can be used to store a wrap a third-party (not under my control) struct that needs a reference to something?

Adam Batkin
  • 51,711
  • 9
  • 123
  • 115
  • I *think* that the "correct" solution is for the third-party API to take ownership of the socket (there's no reason for it not to own the socket), but alas that's not in the cards for today... – Adam Batkin Oct 10 '19 at 02:10
  • Thanks @Jmb for the tip - I wouldn't have been able to solve this without it. That question is not exactly the same problem (it's not the "same" struct) and my question is less "why" (I *think* I already know why) and more "how do I fix it", but it's pretty close. From there, I spent a lot of time reading and experimenting, and I think I have an answer. Comments welcome, and for completeness, it would be great to have a solution with owning_ref and another without external crates (i.e. `unsafe`) since it's really hard to decode what Rental is doing (since it is so feature-ful) – Adam Batkin Oct 13 '19 at 04:46

1 Answers1

0

The Rental crate seems to do what is needed here, albeit with documentation and examples that leave a lot to the imagination (i.e. trial and error).

Here's roughly what solves this

rental! {
    pub mod rentals {
        #[rental_mut]
        pub struct Wrapper {
            stream: Box<TcpStream>,
            low: LowLevelApi<'stream>
        }
    }
}

pub struct HighLevelApi {
    wrapper: rentals::Wrapper
}

impl HighLevelApi {
    pub fn new(host: String, port: u16) -> HighLevelApi {
        Api {
            // Ignore lack of error checking for now
            wrapper: rentals::Wrapper::new(
                Box::new(TcpStream::connect(format!("{}:{}", host, port)).unwrap()),
                |s| LowLevelApi::new(s)
            )
        }
    }

    pub fn do_something(&mut self) {
        self.wrapper.rent_mut(|ll| ll.do_something()) // ll is the LowLevelApi
    }
}

I noticed two important things that made this work:

  1. The lifetime name on low in the Wrapper struct must match the name of the "owning" field (in this case "'stream")
  2. You never get direct access to the reference - you get it through a callback/closure:
    1. In the auto-generated constructor (new()) the second parameter isn't a LowLevelApi, it's a closure that gets the &mut TcpStream, and that closure is then expected to return a LowLevelApi
    2. When you want to actually use the LowLevelApi you can "rent" it, hence the wrapper.rent_mut(f) where f is the closure that gets passed a LowLevelApi (ll) and can do what it needs

With these facts, the rest of the Rental documentation makes a lot more sense.

Adam Batkin
  • 51,711
  • 9
  • 123
  • 115