Description
Consider the case where I want to take any type T
and convert it into some Opaque
struct as so, comparing just the function pointers would give me a meaningful way to determine that this Opaque
was created by the opaquify
function. To ensure that generics that alias because of similar implementations (as shown below) don't cause undefined behavior the type_name
field is used.
use std::ffi::c_void;
#[derive(Debug)]
#[repr(C)]
struct Opaque {
fptr: extern "C" fn(),
type_name: &'static str,
inner: *mut c_void,
}
fn opaquify<T>(val: T) -> Opaque {
dbg!(std::any::type_name::<T>());
dbg!(ffi::<T> as *const c_void);
Opaque {
fptr: ffi::<T>,
type_name: std::any::type_name::<T>(),
inner: Box::into_raw(Box::new(val)).cast(),
}
}
fn unopaquify<T>(val: &Opaque) -> Option<&T> {
dbg!(val);
if val.fptr == ffi::<T> && val.type_name == std::any::type_name::<T>() {
unsafe { Some(val.inner.cast::<T>().as_ref().unwrap_unchecked()) }
} else {
None
}
}
extern "C" fn ffi<T>() {
/* ... */
}
pub fn main() {
let t1 = opaquify(1_u32);
let t2 = opaquify(1_usize);
assert!(unopaquify::<usize>(&t1).is_none());
assert!(unopaquify::<u32>(&t2).is_none());
assert_eq!(unopaquify::<u32>(&t1), Some(&1));
assert_eq!(unopaquify::<usize>(&t2), Some(&1));
}
This above program executes as expected on debug and release however something weird happens when I introduce miri
into the mix. Below is the comparisons between outputs on different modes.
Debug builds
[src/main.rs:12] std::any::type_name::<T>() = "u32"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055be50049d20
[src/main.rs:12] std::any::type_name::<T>() = "usize"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055be50049d10
[src/main.rs:22] val = Opaque {
fptr: 0x000055be50049d20,
type_name: "u32",
inner: 0x000055be50421ba0,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000055be50049d10,
type_name: "usize",
inner: 0x000055be50421bc0,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000055be50049d20,
type_name: "u32",
inner: 0x000055be50421ba0,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000055be50049d10,
type_name: "usize",
inner: 0x000055be50421bc0,
}
Here we can see that generic fptr
s don't alias and behaves as expected.
Release mode
[src/main.rs:12] std::any::type_name::<T>() = "u32"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055613651b6c0
[src/main.rs:12] std::any::type_name::<T>() = "usize"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055613651b6c0
[src/main.rs:22] val = Opaque {
fptr: 0x000055613651b6c0,
type_name: "u32",
inner: 0x0000556137583ba0,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000055613651b6c0,
type_name: "usize",
inner: 0x0000556137583bc0,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000055613651b6c0,
type_name: "u32",
inner: 0x0000556137583ba0,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000055613651b6c0,
type_name: "usize",
inner: 0x0000556137583bc0,
}
Here functions do alias because of presumably an LLVM pass that merges functions but because of the type check unopaquify
knows when it's safe to convert.
Miri
Run with: MIRIFLAGS="-Zmiri-ignore-leaks" cargo miri run
[src/main.rs:12] std::any::type_name::<T>() = "u32"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000000000002f170
[src/main.rs:12] std::any::type_name::<T>() = "usize"
[src/main.rs:13] ffi::<T> as *const c_void = 0x0000000000046572
[src/main.rs:22] val = Opaque {
fptr: 0x000000000003d3a3,
type_name: "u32",
inner: 0x000000000003d508,
}
[src/main.rs:22] val = Opaque {
fptr: 0x00000000000548f9,
type_name: "usize",
inner: 0x0000000000054a90,
}
[src/main.rs:22] val = Opaque {
fptr: 0x000000000003d3a3,
type_name: "u32",
inner: 0x000000000003d508,
}
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `None`,
right: `Some(1)`', src/main.rs:41:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The dbg
logs from opaquify
suggests that fptr
do NOT alias and should follow the same branches as debug build. However, the first unopaquify
call has an fptr which has the value completely different than any of the ones returned. I'm not sure what 0x000000000003d3a3
or 0x00000000000548f9
point to but based on the initial logs it's neither ffi::<usize>
nor ffi::<u32>
. And hence the tests fail.
Is this an expected false positive from miri and can be safely ignored, or is this actual UB that I'm failing to understand?