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?