Ideas for constructing a fat (dyn) pointer

I have the following code, that works fine

use dyn_clone::*;

trait MyTrait<V>: DynClone {}

#[derive(Clone)]
struct MyStruct<V> (core::marker::PhantomData<V>);

impl<V: Clone> MyTrait<V> for MyStruct<V> {}

#[repr(transparent)]
struct SmartPtr<V>(std::sync::Arc<dyn MyTrait<V> + 'static>);

impl<V> SmartPtr<V> {
    pub fn borrow_dyn(&self) -> &dyn MyTrait<V> {
        &*self.0
    }
}

However, I am trying to make the fat pointer into a thin pointer for performance reasons, and I have gotten into a pickle. Below I changed SmartPointer to something else... (For simplicity in this example, I'm just assuming it can only be one type. In reality it's an enum, but the same problem happens)

use dyn_clone::*;

trait MyTrait<V>: DynClone {}

#[derive(Clone)]
struct MyStruct<V> (core::marker::PhantomData<V>);

impl<V: Clone> MyTrait<V> for MyStruct<V> {}

struct SmartPtr<V>(Box<MyStruct<V>>);

impl<V> SmartPtr<V> {
    // I need to call this from a `Drop` impl, so a bound is no good
    // However I know that `SmartPtr<V>` cannot be created unless V: Clone
    pub fn borrow_dyn(&self) -> &dyn MyTrait<V> {
        &*self.0 as &dyn MyTrait<V>
    }
}

The issue with bounding the SmartPtr impl explicitly is that I can't use these methods in Drop. So I want to achieve the same implicit bounding as the Arc-based implementation.

I think the Arc-based impl actually constructs the vtable pointer when the V type is bounded, and stores that vtable pointer, which can be retrieved without the bound. OTOH, my Box-based impl doesn't have a vtable pointer until it's too late...

So somehow I need to make sure the vtable gets built in advance, and stash the metadata somewhere. But I'm hesitant to go too low-level until I've explored what can be done in the less error-prone corners of the language.

Thanks in advance for any ideas.

did check out thin-trait-object? seems what you might need.

1 Like

Thanks for letting me know about this crate. It's definitely barking up the same tree that I am. It's a very cool crate. Unfortunately it won't work for me in its unmodified form.

I'm hoping that there is a solution that doesn't involve quite so much reimplementation. I.e. a way to use the language features that already exist.

One of the issues is that this crate stores the vtable pointer in the referent structure. Which isn't an option in my case.

Do you know is it is possible to stash some data at compile time that is associated with each concrete type, that can be made available in a table at runtime? Basically a static mapping between TypeId and a dispatch table?

unfortunately, such features dosn't exist (yet? or ever?). I think maybe the dyn-star experiment could open the possibility to introduce such features to the language, but no, not now.

also, there's the unstable #![feature(thin_box)], but I believe its implementation is similar to the thin-trait-object crate.

the vptr must go somewhere, you either put it in the pointer, i.e. rust style "fat" pointer, or put it in the pointee, i.e. C++ style "thin" pointer. you cannot do dynamic dispatch without storing the vtable address.

how do you get the TypeId of an erased type at runtime, without an vtable attached to the pointer (i.e. dyn Any)? isn't it a chicken and egg problem?

1 Like

Thanks for all these links to read. These each provide a few more tidbits of context...

how do you get the TypeId of an erased type at runtime, without an vtable attached to the pointer (i.e. dyn Any)? isn't it a chicken and egg problem?

In my case there is only a small set of implementors for MyTrait, (and only one in this simplified example) therefore the mapping only needs to be based on V plus some easily available data, And V is a known type parameter of the SmartPtr<V> type.

I see.

my point is, there's must be some type information available in order to do dynamic dispatch, be it in the form of a vtable, or the discrimenant of an enum, or, like in your case, some implicit information that can be queried at runtime.

I don't know the details of your use case, but I would imagine it could look like this:

trait Reflection {
	fn get_runtime_type(this: *const ()) -> TypeId;
}

trait MyTrait<V> {}

struct Foo<V>(PhantomData<V>);
struct Bar<V>(PhantomData<V>);
impl<V> MyTrait<V> for Foo<V> { }
impl<V> MyTrait<V> for Bar<V> { }

struct SmartPtr<V: Reflection>(*const (), PhantomData<V>);

impl<V: Reflection + 'static> SmartPtr<V> {
	fn new<T: MyTrait<V>>(value: T) -> Self {
		// this is a thin pointer
		let this = Box::into_raw(Box::new(value));
		Self(this.cast(), PhantomData)
	}
	fn borrow_dyn(&self) -> &dyn MyTrait<V> {
		let this = self.0;
		let type_id = V::get_runtime_type(this);
		if type_id == TypeId::of::<Foo<V>>() {
			unsafe { &*this.cast::<Foo<V>>() }
		} else if type_id == TypeId::of::<Bar<V>>() {
			unsafe { &*this.cast::<Bar<V>>() }
		} else {
			panic!()
		}
	}
}

a slight variant of this is, instead of returning a TypeId of Reflection, it can return a &dyn MyTrait<Self> directly, and the SmartPtr just delegates to this.

but the problem remains: how could you actually implement the Reflection trait???

it must make some assumption about the this pointer, which means you don't actually use a completely erased pointer (the *const () type in this example), you must use a pointer from which some form of runtime type information can be extracted, or as you put it, "plus some easiliy available data".

if you think about it, it's serves same purpose as a C++ base class, which, contains a vptr in it. so we are in full circle now.

the only situation that I can think of that doesn't require to store type information inside the object, nor does it require a "real" fat pointer (i.e. double the size of a thin pointer), is to store the type tag in the low bits of the pointer itself, taking advantage of pointer alignments.

1 Like

The rule for Drop is that any bounds have to match the requirements on the struct. Does adding the Clone bound to the struct as well not work for you?

1 Like