I am trying to implement a self-referential struct in rust.
Background
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!