I'm working on a networking stack, and a general pattern that we use for performance is to use a single mutable buffer for both receiving and sending packets. If a packet comes in, it's placed in a buffer, and when the stack is done with the packet and wants to send out a new packet in response, it simply re-uses the buffer to serialize the new packet.
This leads to a problem: It would be great if non-lexical lifetimes worked across function boundaries (obviously, they do not). In particular, it would be great if we could take a buffer, and parse a packet out of it. Consider the following code, in which we take a &'a mut [u8]
and parse it into a TcpSegment<'a>
(note the lifetimes - TcpSegment
borrows the buffer). We would like to be able to pass both the segment and the buffer into another function call, and, once that callee is done with the segment, we would like the callee to be able to stop using the segment and get the original buffer back. It would be as if NLL extended from the caller to the callee. Here's a sketch of what would work in an ideal world:
fn receive_segment(src_ip: Ipv4Addr, dst_ip: Ipv4Addr, buffer: &mut [u8]) {
let segment = TcpSegment::parse(buffer));
if segment.syn() {
...
} else {
receive_data_segment(src_ip, dst_ip, segment, buffer);
}
}
fn receive_data_segment(src_ip: Ipv4Addr, dst_ip: Ipv4Addr, segment: TcpSegment, buffer: &mut [u8]) {
// Use the TcpSegment
let conn = get_conn(src_ip, dst_ip, segment.src_port(), segment.dst_port());
// get other various parameters based on the data in segment.
let (a, b, c) = conn.receive_data_segment(segment);
// Now that we no longer use segment, its lifetime ends,
// and we can use the original buffer to serialize a new segment.
conn.write_ack(buffer, a, b, c);
...
}
Of course, we don't have cross-function NLL, so this doesn't work. My question is: How can I make something like this work, possibly using unsafe code? My first attempt was the following. The idea was to wrap the borrowing object (TcpSegment
in the previous example) in an object - OwnedBorrow
- which could be consumed to get the original reference back.
struct OwnedBorrow<'a, T: 'a + ?Sized, U: 'a> {
t: *mut T,
u: U,
_marker: PhantomData<&'a mut T>,
}
impl<'a, T: 'a + ?Sized, U: 'a> OwnedBorrow<'a, T, U> {
fn new<F: FnOnce(&'a mut T) -> U>(t: &'a mut T, init: F) -> OwnedBorrow<'a, T, U> {
let t_ptr = t as *mut T;
OwnedBorrow {
t: t_ptr,
u: init(t),
_marker: PhantomData,
}
}
fn consume(this: OwnedBorrow<'a, T, U>) -> &'a mut T {
let OwnedBorrow { t, u, _marker } = this;
::std::mem::drop(u);
unsafe { &mut *t }
}
}
impl<'a, T: 'a + ?Sized, U: 'a> Deref for OwnedBorrow<'a, T, U> {
type Target = U;
fn deref(&self) -> &U {
&self.u
}
}
impl<'a, T: 'a + ?Sized, U: 'a> DerefMut for OwnedBorrow<'a, T, U> {
fn deref_mut(&mut self) -> &mut U {
&mut self.u
}
}
Unfortunately, the wise @cramertj pointed out a soundness hole in this design, which you can see in action here:
let mut x = [0; 16];
let y = &mut x;
let z: &mut [u8];
let mut ob = OwnedBorrow::<[u8], Option<&mut [u8]>>::new(y, |x| Some(x));
// z has the lifetime of y, and so outlives ob
z = ob.take().unwrap();
let y = OwnedBorrow::consume(ob);
// z and y are still alive at the same time!
z[0] = 5;
println!("{:?}", y[0]);
So is there a design - similar to this one, or entirely different - which will allow me to simulate cross-functional NLL in this way?
PS: Yes, I know about owning_ref, and no, it doesn't do what I need.