chkn
chkn

Reputation: 717

Learning Rust by porting from C

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

Answers (2)

Matthieu M.
Matthieu M.

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

Francis Gagn&#233;
Francis Gagn&#233;

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

Related Questions