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.
Struct
Deallocation callback
Reference constructor
Owned constructor
The solution is leaving the parameter
'a
as under-determined.Operation
Reference test
Owned test
A downside is that, when writing APIs, I need to be fully aware of lifetime elision rules, otherwise it might silently allow bad usages.