Soundness of write-only pointers contravariant in the pointed type
⚓ Rust 📅 2026-06-08 👤 surdeus 👁️ 1Hi, I am fighting with lifetimes.
Due to a limit in an HRBT (see after for the X problem) I need to make a type that holds some data contravariant over the held type. I do believe that this is sound as the only thing you can do with the type is to write a value to it, so a function with signature fn(T).
I wrote a basic playground to show a simplified implementation of what I am writing, but am not enough at ease with raw pointer to be sure that's sound.
use std::ptr::NonNull;
use std::mem::MaybeUninit;
use std::marker::PhantomData;
/// Pointer that can be only written
///
/// Covariant in 'a, contravariant in T
pub struct ContravariantPtr<'a, T> {
/// Raw pointer or the compiler deduce covariance
ptr: NonNull<()>,
_phantom: PhantomData<&'a fn(T)>,
}
impl<'a, T> ContravariantPtr<'a, T> {
/// Create a new pointer that can be only written
pub fn new(slot: &'a mut MaybeUninit<T>) -> Self {
Self {
ptr: NonNull::from(slot).cast(),
_phantom: PhantomData
}
}
/// Write to the pointer
///
/// This will discard the previous written value, exactly like
/// [`std::mem::forget`]
pub fn write(&mut self, value: T) {
let ptr = self.ptr.cast();
unsafe {
// Safety:
// We know the slot is live as `self` is covariant in 'a, so:
// 'a_creation: 'a_now
//
// We also know that `value` is a valid value for the slot as
// `self` is controvariant in `T`, so:
// T_now: T_creation
ptr.write(MaybeUninit::new(value))
};
}
}
fn main() {
// Allocation to have a lifetime smaller than 'static
let finite_lived: Box<u8> = Box::new(13);
// Slot where to put something that's 'finite_lived or more
let mut slot: MaybeUninit<&u8> = MaybeUninit::new(&*finite_lived);
// Pointer to the slot, write only
let ptr: ContravariantPtr<'_, &u8> = ContravariantPtr::new(&mut slot);
// Force the pointer to point to a smaller type - we can do, as the pointer
// is write only
let mut ptr_with_narrowed_type: ContravariantPtr<'_, &'static u8> = ptr;
// Write a static to it
ptr_with_narrowed_type.write(&42);
// Safe to get it, as we only casted a type to a larger one:
// 'static: 'short_lived
assert_eq!(unsafe { slot.assume_init() }, &42)
}
It is sound? I am playing around passing miri over it and it does not seem to break using only the pub interface
X problem: need to pass a impl for<'a> Fn(&mut Trait::Associated<'a>) to a function and being &mut invariant, plus associated types too that requires that the type pointed to does live for static. At least I think. Anyway that nerdsniped me and now i am shaving this yak.
1 post - 1 participant
🏷️ Rust_feed