Allocator parameter in `dyn Trait`

The problem

Methods that take type parameters are not object safe. Using the upcoming Allocator API requires adding allocator type parameters everywhere. Using custom allocators with object-safe traits is therefore a hassle. This issue has been brought up here, among other places.

To summarize, we'd like the following trait to be object-safe:

trait Foo {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A>;
}

Because of the A type parameter, this isn't possible today. And because A appears in the return type, it still wouldn't be possible under proposals for limited APIT in trait objects.

MVP workaround

One possible workaround is to use &dyn Allocator instead of a type parameter, like the below:

trait Foo {
    fn frobnify<'a>(&self, a: &'a dyn Allocator) -> Vec<i32, &'a dyn Allocator>;
}

Unfortunately, this will infect our entire API with &'a dyn Allocator and its associated lifetime. But we can do better. With into_raw_parts and from_raw_parts_in, we can switch between static and dynamic dispatch for Allocator. Combining this with a separate helper trait, we can greatly improve the situation:

#![feature(allocator_api)]
#![feature(vec_into_raw_parts)]
use std::alloc::Allocator;

// This is not object-safe...
trait Foo {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A>;
}

// But this is!
trait DynFoo {
    fn frobnify_dyn<'a>(&self, a: &'a dyn Allocator) -> Vec<i32, &'a dyn Allocator>;
}

impl<T: Foo> DynFoo for T {
    fn frobnify_dyn<'a>(&self, a: &'a dyn Allocator) -> Vec<i32, &'a dyn Allocator> {
        <Self as Foo>::frobnify(self, a)
    }
}

impl<'a> Foo for dyn DynFoo + 'a {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A> {
        let dyn_a: &dyn Allocator = &a;
        let v: Vec<i32, &dyn Allocator> = self.frobnify_dyn(dyn_a);
        let (ptr, len, cap) = v.into_raw_parts();

        // Safety: the vec was originally allocated with `dyn_a`,
        // which was derived from `a`
        unsafe { Vec::<i32, A>::from_raw_parts_in(ptr, len, cap, a) }
    }
}

// Usage example

use std::alloc::Global;

struct Bar;

impl Foo for Bar {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A> {
        let mut v = Vec::new_in(a);
        v.push(3);
        v
    }
}

fn main() {
    let dyn_foo: &dyn DynFoo = &Bar;
    let v: Vec<i32> = dyn_foo.frobnify(Global);
    dbg!(v);
}

This gets us all the functionality we want, but the ergonomics are not ideal.

Getting rid of unsafe

A closure-based API could help encapsulate the unsafe around from_raw_parts_in. For example:

#![feature(allocator_api)]
#![feature(vec_into_raw_parts)]

pub mod dyn_alloc_stuff {
    use std::{alloc::Allocator, marker::PhantomData};

    #[repr(transparent)]
    pub struct BrandedAlloc<'brand> {
        // For invariance
        brand: PhantomData<fn(&'brand ()) -> &'brand ()>,
        alloc: &'brand dyn Allocator,
    }

    // SAFETY: We just forward to the underlying dyn
    unsafe impl<'brand> Allocator for BrandedAlloc<'brand> {
        #[inline]
        fn allocate(
            &self,
            layout: std::alloc::Layout,
        ) -> Result<std::ptr::NonNull<[u8]>, std::alloc::AllocError> {
            self.alloc.allocate(layout)
        }

        #[inline]
        unsafe fn deallocate(&self, ptr: std::ptr::NonNull<u8>, layout: std::alloc::Layout) {
            self.alloc.deallocate(ptr, layout)
        }

        #[inline]
        fn allocate_zeroed(
            &self,
            layout: std::alloc::Layout,
        ) -> Result<std::ptr::NonNull<[u8]>, std::alloc::AllocError> {
            self.alloc.allocate_zeroed(layout)
        }

        #[inline]
        unsafe fn grow(
            &self,
            ptr: std::ptr::NonNull<u8>,
            old_layout: std::alloc::Layout,
            new_layout: std::alloc::Layout,
        ) -> Result<std::ptr::NonNull<[u8]>, std::alloc::AllocError> {
            self.alloc.grow(ptr, old_layout, new_layout)
        }

        #[inline]
        unsafe fn grow_zeroed(
            &self,
            ptr: std::ptr::NonNull<u8>,
            old_layout: std::alloc::Layout,
            new_layout: std::alloc::Layout,
        ) -> Result<std::ptr::NonNull<[u8]>, std::alloc::AllocError> {
            self.alloc.grow_zeroed(ptr, old_layout, new_layout)
        }

        #[inline]
        unsafe fn shrink(
            &self,
            ptr: std::ptr::NonNull<u8>,
            old_layout: std::alloc::Layout,
            new_layout: std::alloc::Layout,
        ) -> Result<std::ptr::NonNull<[u8]>, std::alloc::AllocError> {
            self.alloc.shrink(ptr, old_layout, new_layout)
        }
    }

    pub fn make_vec_with_dyn<T, A, F>(a: A, f: F) -> Vec<T, A>
    where
        A: Allocator,
        F: for<'brand> FnOnce(BrandedAlloc<'brand>) -> Vec<T, BrandedAlloc<'brand>>,
    {
        let brand_a: BrandedAlloc<'_> = BrandedAlloc {
            brand: PhantomData,
            alloc: &a,
        };
        let v = f(brand_a);
        let (ptr, len, cap) = v.into_raw_parts();

        // Safety: the vec was originally allocated with `brand_a`,
        // which was derived from `a`
        unsafe { Vec::<T, A>::from_raw_parts_in(ptr, len, cap, a) }
    }
}

use std::alloc::Allocator;

use dyn_alloc_stuff::*;

trait Foo {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A>;
}

trait DynFoo {
    fn frobnify_brand<'brand>(&self, a: BrandedAlloc<'brand>) -> Vec<i32, BrandedAlloc<'brand>>;
}

impl<T: Foo> DynFoo for T {
    fn frobnify_brand<'brand>(&self, a: BrandedAlloc<'brand>) -> Vec<i32, BrandedAlloc<'brand>> {
        <Self as Foo>::frobnify(self, a)
    }
}

impl<'a> Foo for dyn DynFoo + 'a {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A> {
        make_vec_with_dyn(a, |dyn_a| self.frobnify_brand(dyn_a))
    }
}

But one could end up needing many variants of make_vec_with_dyn, for all the different collection types (may be abstractable with GATs), and whether you are creating or mutating them. And maybe you are working with multiple collections at the same time... Perhaps you have a better idea?

Getting rid of DynFoo

Ideally dyn DynFoo would just be dyn Foo. But this would require language changes. Perhaps something like the below?

trait Foo {
    // Add an entry to the `dyn Foo` vtable for a particular monomorphization
    // of this function. It becomes an inherent method of `dyn Foo`.
    #[include_in_vtable(frobnify_dyn::<&dyn Allocator>)]
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A>;
}

// Provide the missing methods of the trait object.
impl<'a> Foo for dyn Foo + 'a {
   fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A> {
        let dyn_a: &dyn Allocator = &a;
        let v: Vec<i32, &dyn Allocator> = self.frobnify_dyn(dyn_a);
        let (ptr, len, cap) = v.into_raw_parts();
        unsafe { Vec::<i32, A>::from_raw_parts_in(ptr, len, cap, a) }
    }
}

Miscellaneous questions

  • How would a Storage API affect all this?
  • Is dynx/dyn* relevant here?

I like the idea of being able to include specific monomorphization of functions in the vtable of a trait object. I feel like this is probably useful for other traits too, not only allocators.

It would also be interesting to explore the alternatives. For example it would be nice if something like this were to compile:

trait Foo {
    fn frobnify<A: Allocator>(&self, a: A) -> Vec<i32, A> where Self: Sized;

    fn frobnify_dyn<'a>(&self, a: &'a dyn Allocator) -> Vec<i32, &'a dyn Allocator> {
        self.frobnify(a)
    }
}

Currently the Self: Sized is needed to make it object safe, but this breaks the self.frobnify(a) call in the frobnify_dyn function, and of course you don't want to make frobnify_dyn require Self: Sized because the goal is to use it through trait objects.

The desired semantics of this would be that every non-dyn implementor of Foo would implement frobnify_dyn using the default implementation, while dyn Foo and similar would be able to use the underlying implementation.

One problem I see with this is if Foo is implemented for e.g. [T], which is not Sized but should allow frobnify to be implemented. This is longstanding shortcoming of using where Self: Sized to exclude methods from trait objects and should be fixed sooner or later.

Note that this is unsound, there's nothing guaranteeing that the closure won't use another &'static dyn Allocator instead of the one provided to it.

3 Likes

This is very much related to the design work around async methods in dyn Trait as this is another scenario or use case where it is necessary to apply some transformation when reifying a trait to a trait object.

Niko has explored a design like that in one of his blog posts. I reckon that a general mechanism for this in needed - a user defined constructor syntax for creating a dyn Trait (Niko called these trait adaptors)

Could the GhostCell mechanism be used to force it to work with a unique lifetime?

I've edited the original post to make an attempt at this.

1 Like

Shameless plug: https://github.com/rust-lang/rfcs/pull/3319 would partially fix this.

I don't see how that would solve the problem though, since the generic method can't be included in the vtable anyway.

There is a workaround, although whether or not it actually works for you depends on your particular use case.

This trait is object safe:

trait Foo<A: Allocator> {
    fn frobnify(&self, a: A) -> Vec<i32, A>;
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.