Reference to opaque type

I'm wrapping a third-party C library with an opaque type, let's call it Foo. So I have a definition like the below, which I understand is the current idiomatic way to do this:

#[repr(C)]
struct Foo { _private: [u8; 0] };

extern "C" {
    fn get_foo() -> *mut Foo;           // Get a pointer to Foo
    fn release_foo(*mut Foo);           // Release the pointer to Foo 
    fn get_foo_value(*mut Foo) -> u32;  // Get value stored in Foo
    fn do_work();                       // Algorithm implementation
}

On the C side, the Foo type is a reference-counted singleton. The do_work() function may modify the contents of the singleton. As a result, the function get_foo_value() may return a different value after every time I call do_work().

My question is whether I can safely cast the *mut Foo pointer to a Rust reference, either &T or &mut T. I'd like to make the following "safe" API to do away with the pointers:

impl Foo {
    fn value(&self) -> u32 {
        unsafe { get_foo_value(self as *const _ as _) }
    }
}

fn with_foo<T>(func: impl Fn(&Foo) -> T) {
    let foo = get_foo();
    (func)(unsafe { &*foo });
    release_foo(foo);
}

Now that I've got this API, I plan to use it as below:

fn main() {
    let (start, end) = with_foo(|foo| {
        let start = foo.value();
        do_work();                  // <-- this may mutate Foo internally.
        let end = foo.value();
        (start, end)
    });
    println!("{}, {}", start, end);
}

My worry is that because the compiler sees the immutable &Foo as the type, it may decide as an optimisation it is free to rearrange the start, do_work() call and end initialization as it pleases. Maybe to this, for example:

let start = foo.value();
let end = foo.value();      // <-- This call got reordered earlier because the compiler believed it was working with an immutable reference.
do_work();                  // <-- subsequent internal mutation to Foo never observed

Is this something I should be concerned about? If so, how should I change my code to avoid this potential issue?

You should create a wrapper type e.g.:

struct FooWrapper {
    ptr: *mut Foo,
}
impl FooWrapper {
    pub fn value(&self) -> u32 {
        unsafe { get_foo_value(&self.ptr as *const Foo) }
    }
}

Thanks, I was thinking a wrapper struct was a possible solution. Would you be prepared to elaborate why this is the way to go? Should I conclude that casting a C pointer to a reference should never be done?

Just don't do it. Rust references have all sort of weird guarantees that you just don't have to worry about with raw pointers, so it's just a much better idea to always use a raw pointer when dealing with foreign objects.

1 Like