What is the better way to wrap a FFI struct that o

2019-09-02 12:30发布

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.

标签: rust lifetime
1条回答
聊天终结者
2楼-- · 2019-09-02 13:11

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.

查看更多
登录 后发表回答