Reputation: 717
I'm trying to learn Rust by porting a project from C. The first data structure I've started with is a Buffer
that holds a pointer to some data, its length, and an optional destructor function. You create a Buffer
from an existing allocation, which the Buffer
then takes ownership of, calling your provided destructor function when the Buffer
is destroyed. For convenience, the C version also provided a function that would copy the data for you. I'm struggling with how this would map to Rust.
Here's a simplified excerpt of the C code (for brevity, I've removed error handling):
typedef void (*destruct_func)(void*);
typedef struct {
void* data;
size_t len;
destruct_func destructor; /**< destructor for data ptr, or null */
} buffer_t;
buffer_t* buffer_new_direct (void* data, size_t length, destruct_func destructor)
{
buffer_t* buf = malloc (sizeof (buffer_t));
buf->data = data;
buf->len = length;
buf->destructor = destructor;
return buf;
}
buffer_t* buffer_new_copy (void* data, size_t length)
{
void* copy = malloc (length);
return buffer_new_direct (memcpy (copy, data, length), length, &free);
}
And here's what I have so far in Rust:
pub struct Buffer<'a> {
data: *mut u8,
length: usize,
destructor: Option<Box<Fn(*mut u8) + 'a>>
}
impl<'a> Buffer<'a> {
pub fn new_direct<F: 'a>(data: *mut u8, length: usize, destructor: Option<F>) -> Buffer<'a>
where F: Fn(*mut u8)
{
Buffer {
data: data,
length: length,
destructor: destructor.map(|d| Box::new(d) as Box<Fn(*mut u8)>)
}
}
}
Is this idiomatic Rust? I still need to take existing allocations from C code, so I thought using a raw pointer for data
would be best. However, I would also like to have a method that creates a Buffer
from something like &[u8]
so I can create them more easily from Rust code. How would I do that? What would the destructor be in that case? What would the Rust equivalent of buffer_new_copy
be?
Thanks in advance for any help!
Upvotes: 0
Views: 426
Reputation: 300159
Note that your Rust code is more versatile than your C code: Fn
can be a closure (ie, a function accompanied by some data). To represent a function pointer, you can use fn
instead.
The problem of the Fn
trait is that it is !Sized
and therefore you need to Box
it (which induces a memory allocation). Using fn
directly is much more comfortable.
This lets us simplifying the current code greatly:
pub struct Buffer {
data: *mut u8,
length: usize,
destructor: Option<fn(*mut u8)>,
}
impl Buffer {
pub fn new_direct(data: *mut u8, length: usize, destructor: Option<fn(*mut u8)>) -> Buffer
{
Buffer {
data: data,
length: length,
destructor: destructor
}
}
}
Though as noted by @Francis Gagné, this constructor will ultimately lead to potential unsafety (typical, when interacting with C) so would be better off marked unsafe
.
I also wonder why the destructor
is optional, Rust has a slice type (&[T]
) which is generally used for non-owning; if you want to switch from owning to non-owning, the idiomatic way is to use std::borrow::Cow
as in std::borrow::Cow<'a, NonOwning>
. You would need a dedicated slice type, of course.
Now, in order to actually call the destructor automatically, you implement Drop
:
impl Drop for Buffer {
fn drop(&mut self) {
self.destructor.map(|d| d(self.data));
}
}
We can also implement conversion to a slice. This is done by implementing the Deref
and DerefMut
traits:
impl Deref for Buffer {
type Target = [u8];
fn deref(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.data, self.length) }
}
}
impl DerefMut for Buffer {
fn deref_mut(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.data, self.length) }
}
}
It gives us a lot of goodies (see what Vec
gains from it).
Finally, we can use the libc
library to provide raw allocation and de-allocation, however it's currently unstable so you'll need a nightly compiler or to use the library from crates.io (I'll go the unstable road here):
#![feature(libc)]
impl Clone for Buffer {
fn clone(&self) -> Buffer {
fn destroy(data: *mut u8) {
unsafe { libc::free(data as *mut _) }
}
let ptr = unsafe {
let ptr = libc::malloc(self.length) as *mut _;
std::ptr::copy_nonoverlapping(self.data, ptr, self.length);
ptr
};
Buffer {
data: ptr,
length: self.length,
destructor: Some(destroy),
}
}
}
Upvotes: 3
Reputation: 65832
It's as idiomatic as it can get... Like C++, Rust relies on RAII for resource management, so one usually doesn't invoke a destructor manually, but since you're interoperating with C, you'll have to do it anyway.
void*
would be best represented as *mut ()
or *mut libc::c_void
in Rust.
Your Buffer
struct should have an implementation of Drop
that invokes the destructor, so that you never need to call it manually. However, the destructor is likely to be "unsafe", since it will need to cast the data
pointer to something else, or call an unsafe FFI function (e.g. libc::free
). Unfortunately, Rust's function traits cannot be used for unsafe
functions, so you cannot encode that in the type system and the call to the destructor will appear to be safe, even though the destructor itself performs unsafe operations. What I'd do as a workaround is to mark new_direct
as unsafe
, since dropping a Direct
might not be memory safe if data
is invalidated before the destructor is run.
Upvotes: 2