How do I handle an FFI unsized type that could be

2020-04-11 03:32发布

c_strange_t is an opaque C type that is only seen behind a pointer. When wrapping this type, there are times when it is our responsibility to free memory using c_free_strange_t(*c_strange_t), and other times when we are not responsible for freeing the data, we are only responsible for accurately controlling the lifetime.

It would be ergonomic if this type could be mapped into 2 types in Rust that work in a similar way to str and String, where there is impl Deref<Target=str> for String. The borrowed type would need to be marked as only valid behind a reference.

Is this possible, and how would it be done?

1条回答
SAY GOODBYE
2楼-- · 2020-04-11 04:28

This appears to work, but it does require using a small unsafe block, so you should test under the normal tools like Miri and Valgrind. The primary assumption made here1 is that c_void cannot be constructed normally. #[repr(transparent)] is used to ensure that the FooBorrowed newtype has the same memory layout as a c_void. Everything should end up as "just a pointer":

use std::{ffi::c_void, mem, ops::Deref};

#[repr(transparent)]
struct FooBorrowed(c_void);
struct FooOwned(*mut c_void);

fn fake_foo_new(v: u8) -> *mut c_void {
    println!("C new called");
    Box::into_raw(Box::new(v)) as *mut c_void
}

fn fake_foo_free(p: *mut c_void) {
    println!("C free called");
    let p = p as *mut u8;
    if !p.is_null() {
        unsafe { Box::from_raw(p) };
    }
}

fn fake_foo_value(p: *const c_void) -> u8 {
    println!("C value called");
    let p = p as *const u8;
    unsafe {
        p.as_ref().map_or(255, |p| *p)
    }
}

impl FooBorrowed {
    fn value(&self) -> u8 {
        fake_foo_value(&self.0)
    }
}

impl FooOwned {
    fn new(v: u8) -> FooOwned {
        FooOwned(fake_foo_new(v))
    }
}

impl Deref for FooOwned {
    type Target = FooBorrowed;

    fn deref(&self) -> &Self::Target {
        unsafe { mem::transmute(self.0) }
    }
}

impl Drop for FooOwned {
    fn drop(&mut self) {
        fake_foo_free(self.0)
    }
}

fn use_it(foo: &FooBorrowed) {
    println!("{}", foo.value())
}

fn main() {
    let f = FooOwned::new(42);
    use_it(&f);
}

If the C library actually hands you a pointer, you would need to do some more unsafe:

fn fake_foo_borrowed() -> *const c_void {
    println!("C borrow called");
    static VALUE_OWNED_ELSEWHERE: u8 = 99;
    &VALUE_OWNED_ELSEWHERE as *const u8 as *const c_void
}

impl FooBorrowed {
    unsafe fn new<'a>(p: *const c_void) -> &'a FooBorrowed {
        mem::transmute(p)
    }
}

fn main() {
    let f2 = unsafe { FooBorrowed::new(fake_foo_borrowed()) };
    use_it(f2);
}

As you identified, FooBorrowed::new returns a reference with an unrestricted lifetime; this is pretty dangerous. In many cases, you can construct a smaller scope and use something that provides a lifetime:

impl FooBorrowed {
    unsafe fn new<'a>(p: &'a *const c_void) -> &'a FooBorrowed {
        mem::transmute(*p)
    }
}

fn main() {
    let p = fake_foo_borrowed();
    let f2 = unsafe { FooBorrowed::new(&p) };
    use_it(f2);
}

This prevents you from using the reference beyond when the pointer variable is valid, which is not guaranteed to be the true lifetime, but is "close enough" in many cases. It's more important to be too short and not too long!


1 — In future versions of Rust, you should use extern types to create a guaranteed opaque type:

extern "C" {
    type my_opaque_t;
}
查看更多
登录 后发表回答