@Yandros, thank you very much for your tour de force answer. I am researching many aspects and plan to respond to the design recommendations later.
Ugh. I'm extremely unhappy to have left this landmine in the code for you to disarm. I showed this to San Diego Rust at the virtual meetup last night and they jumped on it too.
An earlier iteration used calloc and free:
@@ -27,7 +26,7 @@
let offset_of_buffers = mem::align_of::<Buffer>();
let size = offset_of_buffers + num_buffers as usize * mem::size_of::<Buffer>();
let mut buffer_list = BufferList {
- inner: Vec::with_capacity(size).as_mut_ptr() as *mut BufferListInternal,
+ inner: unsafe { libc::calloc(size, 1) as *mut BufferListInternal },
};
unsafe {
(*buffer_list.inner).num_buffers = num_buffers;
@@ -51,6 +50,12 @@
}
}
+impl Drop for BufferList {
+ fn drop(&mut self) {
+ unsafe { libc::free(self.inner as *mut c_void) }
+ }
+}
+
That might be goofy, but it passed Miri (after removing a #[derive(Copy, Clone)] that's incompatible with Drop). Furthermore, because it's not idiomatic Rust, it draws attention to itself as something to review — and it's an appropriate "tell" for my background as a reasonably experienced C programmer but a relative newcomer to Rust.
This misadventure with pointers illustrates why I want to design BufferList to work with &, &mut and friends, in order to spare users from dealing with * mut and * const as much as possible. Technically the unsound code only happens at the site of the pointer dereference, but the "safe" code is where you set yourself up for failure.
As a data point, as I've learned to work with Rust's borrow checker, I've found the distinctions between pointers and references hard to grok: what's actually being passed around underneath, when implicit dereferencing happens automagically and when explicit dereferencing is needed, the difference between &(*pointer) and &{ *pointer }, what the guarantees are for lifetime management. That confusion is how my mistake originated, although there were at least two missed opportunities to avert disaster:
- I should have stuck with
calloc/freebecause I was uncertain about usingVecand I knew that I got it wrong, the failure mode would be extremely severe. - At the language level, it's a bummer that it's so easy to create a dangling pointer in safe code that may be very far away from the
unsafeblock that ultimately performs the dereference. In a perfect world, theseas_ptrandas_mut_ptrroutines wouldn't be such footguns.
Back in the day, I used to script valgrind checks for my C projects. Since I'll be continuing to work with unsafe, it's clear that I'll need to do something similar while working with Rust, using Miri or other tools.