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 }
}
}
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())
}
}
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.