Help with lifetimes


#1

I am implementing a SignalR client library. I am unable to find a way to model my Connection and Proxy structs. This is my attempt so far:

use std::collections::HashMap;

struct Connection<'b, 'a: 'b> {
	map: HashMap<String, Proxy<'b, 'a>>,
}

struct Proxy<'a, 'b: 'a> {
	c: &'b mut Connection<'b, 'a>,
}

impl<'b, 'a> Connection<'b, 'a> {
	fn new() -> Self {
		Connection {
			map: HashMap::new(),
		}
	}

	fn create(&mut self, hub: String) -> &mut Proxy {
		let p = Proxy::new(self);
		//ignore map check for now
		self.map.insert(hub, p);
		self.map.get("myhub").as_mut().unwrap()
	}

	fn start(&mut self) {}
}

impl<'a, 'b> Proxy<'a, 'b> {
	fn new(c: &mut Connection) -> Self {
		Proxy { c: c }
	}
	//other methods
}

fn main() {
	let mut c = Connection::new();
	let mut p = c.create();
	//do stuff with p
	c.start();
}

I don’t seem to be getting the lifetime sub-typing right here. Maybe the above approach is just completely wrong but i want to see how far I can go.


#3

You’re going to hit a dead-end because you’re on your way to trying to create a self-referencing struct (Connection owns the Proxy by virtue of putting it into map and Proxy references the Connection), which is disallowed in Rust.

Maybe you can sketch you a bit more how you envision this working? What type of API and requirements do you have?


#4

As an aside:

struct Proxy<'a, 'b: 'a> {
	c: &'b mut Connection<'b, 'a>,
}

This will never work because you cannot borrow something, that’s tied to a lifetime 'a, for ’b where 'b: 'a. That would imply that you could be left holding a reference to Connection (borrowed with 'b) while whatever it borrows using 'a has expired.


#5

SignalR is used to implement real time updates from a server to a client over Http. This is done by exposing endpoints called hubs that contain a list of methods. A hub method can be invoked through a ‘proxy’. An event handler/callback is attached to the proxy which is invoked when the hub method response is returned.
A proxy is created through connection and a connection can maintain several hub-to-proxy mappings.
The methods can be invoked asynchronously.

I am trying to mimic the C# API so that users who are good with C# can easily use the Rust version.


#6

That’s helpful to know at a high level. One thing you’ll want to decide on in the Rust version is ownership over the Connection and Proxy objects. Who will keep the Connection alive? Is the connection going to be shared across callers on a single thread? Will it be shared across threads? Will Connection require a &mut self to perform anything? Do you want multiple Proxy instances for a single hub to be handed out at the same time (or allow that facility)? Or should there only be 1 Proxy outstanding (i.e. client having a handle to it) for a given hub?

These types of things are usually not a consideration in C# but you’ll want to think about this part for Rust. This will dictate how you design your API.

At first blush, the closest thing to a C# version (without the asynchronous/multithreaded parts for now) would probably be Connection and Proxy objects that internally have some shareable (in the Rc/Arc sense) parts. A client requests a new proxy via create, and this either returns a clone() of an existing one (if it’s already in the map) or inserts a clone into the map and returns another clone (again, in the Rc sense) to the caller. To hide the refcounting, you’d keep these inner shareable components private to your crate; the publicly visible Connection and Proxy would just be shims over these internal parts.

The above may not be the best, but I envision something like that being the closest to what you probably have in C#, although I’m just speculating. You can also consider foregoing the Proxy object and just allowing calling hub methods on the Connection itself (or whatever you name that type).

At any rate, you’ll want to think extra carefully about ownership and relationships (who owns what? how is it borrowed? etc) in the Rust version.