Is there a way to make lower-bit tagged pointers to `?Sized` types without using nightly?

I'm working on a garbage collector, and part of that requires having tagged fat pointers. I need to be able to tag a pointer's lower bits. For now, I've used strict provenance with map_addr to conveniently edit the address part to achieve my needs. However, I'd like to be able to avoid using nightly Rust if possible. Is there a way to manipulate a *const T where T: ?Sized without using strict provenance?

For a concrete example, here's a stripped down version of what I'm doing:

struct MyPointer<T: ?Sized>(*const MyAlloc<T>);

#[repr(C)]
struct MyAlloc<T: ?Sized> {
    stuff: usize,
    value: T
}


const TAG_MASK: usize = std::mem::align_of::<MyAlloc<()>>() - 1;

impl<T: ?Sized> std::ops::Deref for MyPointer<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        // this is how I would do it with strict provenance.
        // how would I do it on stable Rust?
        unsafe { &self.0.map_addr(|x| x & !TAG_MASK).as_ref().value }
    }
}

Does the stable polyfill for the strict provenance API not work for you?

No, the issue is that I need to work with ?Sized (and sptr exclusively supports Sized).

Ahh I was looking right at the bound but not reading it, apparently

I'm not sure there's a good way to avoid that problem without at least theptr_metadata feature. If it's feasible to implement a trait manually for all the types you'll be using with MyPointer you could probably hack together something that approximated ptr_metadata by relying on the fact that rustc currently represents fat pointers as tuples with the data pointer followed by the metadata pointer. It wouldn't be terribly pretty though

Here's an incomplete (and mostly untested) sketch of that. No idea how sound it is though

#![allow(clippy::not_unsafe_ptr_arg_deref)]

use std::fmt::Display;

struct MyPointer<T: ?Sized>(*const MyAlloc<T>);

#[repr(C)]
struct MyAlloc<T: ?Sized> {
    stuff: usize,
    value: T,
}

const TAG_MASK: usize = std::mem::align_of::<MyAlloc<()>>() - 1;

impl<T: ?Sized> std::ops::Deref for MyPointer<T>
where
    *const MyAlloc<T>: Strict,
{
    type Target = T;
    fn deref(&self) -> &Self::Target {
        // this is how I would do it with strict provenance.
        // how would I do it on stable Rust?
        unsafe { &self.0.map_addr(|x| x & !TAG_MASK).as_ref().unwrap().value }
    }
}

pub trait Pointer {
    type Metadata;

    fn addr_and_meta(self) -> (usize, Self::Metadata);

    fn addr(self) -> usize
    where
        Self: Sized,
    {
        self.addr_and_meta().0
    }
}

pub trait Strict: Pointer {
    type Pointee: ?Sized;

    fn expose_addr(self) -> usize;

    fn with_addr(self, addr: usize) -> Self;

    fn map_addr(self, f: impl FnOnce(usize) -> usize) -> Self;
}

// Blanket impl for Sized types
impl<T> Pointer for *const T {
    type Metadata = ();

    fn addr_and_meta(self) -> (usize, Self::Metadata) {
        (unsafe { core::mem::transmute(self) }, ())
    }
}

// Specific impl for a single unsized type
// You might need to instead impl for `*const MyAlloc<T>` with a blanket sized and specific unsized impls
impl Pointer for *const dyn Display {
    type Metadata = *const ();

    fn addr_and_meta(self) -> (usize, Self::Metadata) {
        unsafe { core::mem::transmute(self) }
    }
}

impl<T: ?Sized> Strict for *const T
where
    *const T: Pointer,
{
    type Pointee = T;

    fn expose_addr(self) -> usize {
        self as *const () as usize
    }

    fn with_addr(self, addr: usize) -> Self {
        let (self_addr, meta) = self.addr_and_meta();
        let self_addr = self_addr as isize;
        let dest_addr = addr as isize;
        let offset = dest_addr.wrapping_sub(self_addr);

        unsafe { std::mem::transmute_copy(&((self as *mut u8).wrapping_offset(offset), meta)) }
    }

    fn map_addr(self, f: impl FnOnce(usize) -> usize) -> Self {
        self.with_addr(f(self.addr()))
    }
}

#[cfg(test)]
mod test {
    use super::{Pointer, Strict};
    use std::fmt::Display;

    #[test]
    fn display() {
        let a = "hi";
        let a: &dyn Display = &a;

        let b = "bye";
        let b: &dyn Display = &b;

        let b_again =
            unsafe { &*(a as *const dyn Display).with_addr((b as *const dyn Display).addr()) };

        assert_eq!(b_again.to_string(), "bye".to_string())
    }
}

I think this is analogous to creating NULL pointers from existing thin or wide pointers.

Ok, that's about what I had thought. Looks like I'll probably be stuck with nightly for the general case, which isn't ideal but is at least acceptable.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.