This is something of a follow-up to a previous question of mine. TL;DR: I tried to create a self-referencing struct by heap allocating the self-referencee using a Box. It was pointed out that I can't rely on the pointer provenance not changing (it is currently undecided).
This led to me trying to implement a sort of custom Box, PinnedClient, which allocates to the heap when created and deallocates on Drop:
struct PinnedClient(pub *mut Client);
impl PinnedClient {
unsafe fn new(client: Client) -> PinnedClient {
// Allocate memory on the heap
let layout = Layout::new::<Client>();
let pointer = alloc(layout) as *mut Client;
// Make sure it worked
if pointer.is_null() {
handle_alloc_error(layout);
}
// Move the client object to the heap
pointer.write(client);
// Return a `PinnedClient` object with a pointer
// to the underlying client.
PinnedClient(pointer)
}
}
impl Drop for PinnedClient {
fn drop(&mut self) {
// Deallocate the previously allocated when
// wrapper is dropped.
unsafe {
dealloc(self.0 as *mut u8, Layout::new::<Client>());
}
}
}
Then I include a PinnedClient in my struct and use the raw pointer to create the self-reference:
pub struct MyTransaction<'a> {
transaction: Transaction<'a>,
_client: PinnedClient
}
impl<'a> MyTransaction<'a> {
async fn from_client<'this>(client: Client) -> Result<MyTransaction<'a>, Error> {
let client = unsafe { PinnedClient::new(client) };
let transaction = unsafe {
// Convert `*mut Client` to `&mut Client`
// This shouldn't fail since the pointer in PinnedCliend
// is guaranteed not to be null.
&mut *client.0
}.transaction().await?;
Ok(
MyTransaction { _client: client, transaction }
)
}
}
Now I am wondering:
- is it undefined behaviour to "hand out" a mutable reference to the
Clientand - is it it actually guaranteed that the memory gets deallocated (does
deallocget called in all scenarios)?
I am somewhat anxious since self-referential structs are supposed to be hard and I feel like I am missing something.
No it is not (of course, assuming you keep all aliasing rules and do not hand out two such references).
However, as pointed out in your previous questions, you need to swap the fields, as currently the
Transactiongets dropped after theClientbut it may access it during its drop, causing a use-after-free.Yes and no.
No, because in Rust, destructors are not guaranteed to be called (barring special requirements of
Pin). If the destructor ofMyTransactiondoes not run (because ofstd::mem::forget()or because of aRccycle or because of whatever), then the memory will not be deallocated.But also yes, because the answer for the question you've probably meant to ask is yes: every time the memory would be deallocated if the pointer was
Box, is will also be deallocated with your smart pointer.Boxalso usesDrop, it is not special (it is special in other ways, though).However...
You missed it again. Your code still leaks resources.
The backing memory for the
Clientwill be deallocated, but theClientitself won't be dropped. This means that if your client owns some resources (heap memory, file descriptors, network connections, etc.) they won't be released.Even if currently
Clientdoesn't have any drop glue, if it is coming from a library you don't control, nothing promises it will continue to not have drop glue in the future. Adding aDropimpl is not considered a breaking change. So you still need to drop it, to be future-proof.Fortunately, this is easy:
You asked two questions here, you failed subtley twice to write UB-free code. Is this not hard enough for you?