I am trying to implement a self-referential struct in rust.
I am writing a crate which is based on another crate. The underlying crate exposes a type Transaction<'a>
which requires a mutable reference to a Client
object:
struct Client;
struct Transaction<'a> {
client: &'a mut Client
}
The only way to construct a Transaction
is by calling a method on a client object. The function looks something like this:
impl Client {
async fn transaction<'a>(&'a mut self) -> Transaction<'a> {
// ...
}
}
Now I want to expose an api that allows something like this:
let transaction = Transaction::new().await;
Of course, this doesn't work without passing a &mut Client
.
Now my idea was to create a wrapper type which owns a Client
as well as a Transaction
with a reference to said client.
I would use unsafe
to create a mutable reference which can be used to create a Transaction
while still being able to pass the Client
object to my struct. I would pin the Client
on the heap to make sure the reference doesn't become invalid:
struct MyTransaction<'a> {
client: Pin<Box<Client>>,
inner: Transaction<'a>
}
impl<'a> MyTransaction<'a> {
async fn new() -> MyTransaction<'a> {
// ... fetch a new client object
let pin = Box::pin(client);
let pointer = &*pin as *const Client as *mut Client;
// convert raw pointer to mutable reference
// to create new Transaction
let inner = unsafe {
&mut *pointer
}.transaction().await;
Self {
inner,
client
}
}
}
When I run this code however, my test fails with the following error message:
error: test failed
Caused by:
process didn't exit successfully: `/path/to/test` (signal: 11, SIGSEGV: invalid memory reference)
This somewhat surprised me because I thought that by pinning the client
object I'd ensure it won't be moved. Thus, I reasoned, a pointer/reference to it shouldn't become invalid as long as it doesn't outlive the client. I though that this could not be the case since the client is only dropped when MyTransaction
is dropped, which will also drop the Transaction
which holds the reference. I also though that moving MyTransaction
should be possible.
What did I do wrong? Thanks in advance!
The problem here is the implicit drop order placed on the fields, in Rust that order is the declaration order meaning your client
gets dropped before inner
so when inner
is dropped and uses it's reference to client
that reference is dangling and causes the SIGSEGV you see.
A simple fix is to just reorder them:
struct MyTransaction<'a> {
inner: Transaction<'a>
client: Pin<Box<Client>>,
}
For more complex scenarios see Forcing the order in which struct fields are dropped