1
votes

I have an Image struct that can be constructed from a Vec<u8> or a &[u8]. It represents an image object in C library (ffi module).

struct Image { ptr: *mut c_void };

impl Image {
    fn from_vec(vec: Vec<u8>) -> Image {
        // transfer ownership to gobject system
        let ptr = unsafe {
            ffi::new(
                vec.as_ptr() as *const c_void,
                vec.len(),
                ..
            )
        };
        std::mem::forget(vec);
        Image { ptr }
    }
    fn from_ref(data: &[u8]) -> Image {
        // gobject doesn't free data on Drop
        let ptr = unsafe {
            ffi::new_ref(
                data.as_ptr() as *const c_void,
                data.len(),
                ..
            )
        };
        Image { ptr }
    }

    fn resize(&self, ..) -> Image {
        let new_ptr = unsafe { ffi::resize(self.ptr) };
        Image { new_ptr }
    }
}

impl Drop for Image {
    fn drop(&mut self) {
        unsafe {
            ffi::g_object_unref(self.ptr as *mut c_void);
        }
    }
}

The Image struct has only raw pointer and no borrow, so the compiler puts no lifetime constraint on the output of resize operation.

with a vector, this is ok:

let img1 = Image::from_vec(pixels); // consume pixels
let img2 = img1.resize(..);
return img2;
// when img2 is released, gobject system will release pixels as well

However, with a reference, this is a problem:

let pixels = Vec::new(..);
let img1 = Image::from_ref(&pixels);
let img2 = img1.resize(..)
return img2;
// danger: img2's gobject has a raw pointer to pixels

The compiler doesn't complain, but to prevent this case, I want the compiler to complain by adding a lifetime.

A working solution I know is to have two versions of Image, owned and borrowed. (like String/&str). However I don't want to repeat the same code which differs only in return type:

impl OwnedImage {
    fn resize(..) -> OwnedImage {
        let new_ptr = unsafe { ffi::resize(self.ptr) };
        OwnedImage{ptr:new_ptr}
    }
}

// ScopedImage needs a PhantomData.
struct ScopedImage<'a> { ptr: *mut c_void, marker: PhantomData<&'a ()> }
impl<'a> ScopedImage<'a> {
    fn resize(..) -> ScopedImage<'a> {
        let new_ptr = unsafe { ffi::resize(self.ptr) };
        ScopedImage{ptr:new_ptr, PhantomData}
    }
}

let pixels = Vec::new(..);
let img1 = ScopedImage::from_ref(&pixels);
let img2 = img1.resize(..);
return img2; // error, as I intended.

Unlike &str/String, two types differ only in whether the compiler complains or not for some cases.

My question is if it is possible to incorporate two types into one with lifetime parameter.

My first idea was having two lifetimes 'a and 'b, where 'a represents self's scope and 'b represents the scope of returned objects. For reference image, I want to enforce 'a == 'b but I am not sure how to achieve that.

    // for vec, 'a!='b. for ref, 'a=='b

    struct Image<'a, 'b> { ptr, ?? }

    // this type parameter relationship is
    //    enforced at the construction

    from_vec(..) -> Image<'a,'a>
    from_ref<'b> (&'a data) -> Image<'a,'b>

    resize<'b>(&self, ..) -> Image<'b>

Or with one lifetime:

    type R = (Image:'a  or Image:'b);
    resize(&self, ..) -> R // R: return type, decided on construction

Or split into two structs, OwnedImage and ScopedImage and implement operations in a trait:

    trait ImageTrait<'a> {
        type OutputImage: 'a;

        fn resize(..) -> Self::OutputImage {
            ..
        }
    }

    impl<'a> ImageTrait<'a> for OwnedImage {
        type OutputImage = OwnedImage;
    }

    impl<'a, 'b> ImageTrait<'b> for ScopedImage {
        type OutputImage = ScopedImage;
    }

Or, searching 'rust lifetime as type association' gives me this RFC: https://github.com/rust-lang/rfcs/pull/1598 (I am reading this. Is this applicable to my case?)

This is the first time I am writing a serious Rust code with complex generics and lifetimes. I am not actually asking which is better (though I wonder their pros/cons and which is idiomatic), I just don't even know which of these options are possible.

1
I'm not sure I'm reading this correctly but you may want to look at owning_ref.squiguy
from_vec is definitely not safe. The Vec will be dropped when the function returns, invalidating the pointer. You need to at least mem::forget(vec) to make it worktrentcl
I don't think your resize function can exist, because you don't retain any information about the provenance of your pointer. You want the function to return an owned Image when self is owned, and a borrowed one when self is borrowed, but you aren't keeping track of any way to distinguish them. What does ffi::resize do?trentcl
On the contrary, I think this is all about managing FFI objects. How can fn resize(..) -> OwnedImage be safe, if the resized image may reference the input pixel data?trentcl
Also, I think you don't realize that the Vec you called mem::forget on can never be freed. Rust's allocator has to free the data and, unless you save the vector's original capacity and call Vec::from_raw_parts to reconstitute it, that Vec is leaked forever. Passing it to C's free or to g_object_unref may do nothing, or may corrupt data, but it won't free the slice.trentcl

1 Answers

3
votes

Struct

pub struct Image<'a> {
    pub c: *mut ffi::Image,
    marker: PhantomData<&'a()>,
}

Deallocation callback

pub unsafe extern "C" fn cleanup(ptr: *mut ffi::Image, user_data: *mut c_void) {
    let b: Box<Box<[u8]>> = Box::from_raw(user_data as *mut Box<[u8]>);
    println!(" >>>> releasing slice of len {}", b.len());
    drop(b);
}

Reference constructor

impl<'a> Image<'a> {
    pub fn from_memory_reference(buf: &'a [u8] /* ... */) -> Result<Image, Box<Error>> {
        let c = unsafe {
            ffi::image_new_from_memory(
                buf.as_ptr() as *const c_void,
                // ...
            )
        };

        Ok(Image {
            ptr: c,
            PhantomData,
        })
    }
}

Owned constructor

The solution is leaving the parameter 'a as under-determined.

impl<'a> Image<'a> {
    pub fn from_memory(buf: Vec<u8> /* ... */) -> Result<Image<'a>, Box<Error>> {
        let b: Box<[_]> = buf.into_boxed_slice();
        let c = unsafe {
            ffi::image_new_from_memory(
                b.as_ptr() as *const c_void,
                // ...
            )
        };

        let bb: Box<Box<_>> = Box::new(b);
        let raw: *mut c_void = Box::into_raw(bb) as *mut c_void;

        unsafe {
            let callback: unsafe extern "C" fn() = ::std::mem::transmute(cleanup as *const ());

            ffi::g_signal_connect_data(
                c as *mut c_void,
                "close_signal\0".as_ptr() as *const c_char,
                Some(callback),
                raw,
                None,
                ffi::GConnectFlags::G_CONNECT_AFTER,
            );
        };

        Ok(Image {
            ptr: c,
            PhantomData,
        })
    }
}

Operation

fn resize(&self, scale: f64) -> Result<Image, Box<Error>> {
    // ...
}

Reference test

let _img: Image = {
    let pixels = vec![0; 256 * 256 * 3];
    Image::from_memory_reference(&pixels, /* ... */).unwrap()
    //~^ ERROR `pixels` does not live long enough
};

Owned test

let _img: Image = {
    let pixels = vec![0; 256 * 256 * 3];
    Image::from_memory(pixels, /* ... */).unwrap()
}; // Ok

A downside is that, when writing APIs, I need to be fully aware of lifetime elision rules, otherwise it might silently allow bad usages.