At first: currently we already have &dyn
and &mut dyn
kinds, they handle known cases of dynamic dispatch, and from them object safety rules arose;\
Given that the goal we are pursuing is allowing static existential types (impl Trait
) to be coercible to dynamic existential types (dyn Trait
) we just need to get "owning dyn types".
In Niko's blog post dyn*
types are, essentially, fat pointer:
Creating a dyn*
To coerce a value of type T
into a dyn* Trait
, two constraints must be met:
- The type
T
must be pointer-sized or smaller.
- The type
T
must implement Trait
In the real world, however, what will actually be coerced to dyn* Trait
is an owning smart pointer, such as Box<T,_>
or StaticRc<1,1>
. Thus, given vtable's, drops and custom allocators I think we should take a different approach: concrete layout of dyn* Trait
type incorporates all these notions and we get a trait to construct dyn* Trait
from appropriate types.
(Warning: I used generic traits concept a lot)
My preferred design is therefore:
/// this trait is implemented for everything that can be coerced into a `dyn*` type
/// this is unsafe, because the only types that should implement this must logically own their values (like `Box`)
unsafe trait CoerceDynStar<trait Trait>
where Self: Deref, <Self as Deref>::Target: Trait
{
//this is called on `dyn* Trait` value drop - it frees memory owned by the value
fn drop(&mut self);
//this is called for creating a `dyn*` value from a container
fn create(self) -> dyn* Trait;
}
///`dyn *` types are, in fact, this:
struct DynStar<trait Trait>{
data: Unique<[u8]> //owning data pointer (1)
vtable: VTable<Trait> //vtable ref
drop: fn(&mut self) //drop routine, shouldn't be a part of `vtable` field (see below) (2)
allocator: &dyn Allocator //reference to an allocator which we got memory from*
}
* I know that currently allocator api isn't object safe (in old sense), but hope that this will change.
(1) This is the source of the lifetime 'x
here dyn* Trait + 'x
(2) Moreover, this function pointer is only valid for the lifetime of allocator, which we have taken the memory from.
Thus, our API get more lifetimes:
//imaginary syntax warning
unsafe trait CoerceDynStar<trait Trait>
where Self: Deref, <Self as Deref>::Target: Trait
{
fn drop(&mut self);
fn create(self) -> dyn* Trait; //see below
}
struct DynStar<'d,'a: 'd,trait Trait>{
data: Unique<[u8]> + 'd,
vtable: VTable<Trait>,
drop: fn(&mut self),
allocator: &'a dyn Allocator
}
Since the structure exists only during 'd
lifetime, not extending to 'a
, we have only one lifetime we care about.
Given we are designing new kind of types I wonder if it'd be better if apply the same lifetime elision rules we have for output references to that single lifetime of dyn*
kind? It'd allow writing (and transforming to ) fn method(&self) -> dyn* Trait
instead of fn method(&self) -> dyn* Trait + '_
An example:
impl<O,'a,F: Future<Output = O>> CoerceDynStar<Future> for Box<F,&'a dyn Allocator> { //bad thing about generic trait happened
fn drop(&mut self) {
std::mem::drop_in_place(self);
}
fn create(self) -> dyn* Future<Output = O> { // (+ 'a) is unspecified
DynStar::<'_,'_,Future>{
data: self.ptr,
vtable: VTable<Future>::for::<F>(), //this return vtable pointer\reference
drop: <F as Drop>::drop,
allocator: self.alloc, //alloc reference, hence attached lifetime
}
}
}
Dropping of any dyn* Trait
type is done in two steps: call (self.drop)(self.data)
and call self.alloc.dealloc(self.data)
.
P.S. Are there currently any cases that are not Box
?