Skip to content
Closed
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aa5c8e1
Start procedural vtables RFC
oli-obk Aug 1, 2020
da9f3fc
First draft
hackmd-deploy Aug 1, 2020
446d1ad
Add PR id
hackmd-deploy Aug 1, 2020
5d1b767
Add future possibilty for C++-ish vtables
hackmd-deploy Aug 1, 2020
4938454
Add lots of full examples
hackmd-deploy Aug 1, 2020
0dbcf30
Correctly use `unsafe` in the examples
hackmd-deploy Aug 1, 2020
6157040
Discuss `dyn A + B`
hackmd-deploy Aug 1, 2020
4e73560
Split unsizing and other ops into separate functions
hackmd-deploy Aug 2, 2020
66c7438
Show how to use this for `[T]`
hackmd-deploy Aug 2, 2020
24f8f5a
Vtables have a const generic param for the length, not the trait tree
hackmd-deploy Aug 2, 2020
49f2b68
Rewrite the RFC to be more type safe
hackmd-deploy Aug 2, 2020
b7e9b92
Clarify some things and write a more extensive intro
hackmd-deploy Aug 2, 2020
41599c4
Fix up variable names
hackmd-deploy Aug 6, 2020
b4ecf47
Copy paste mistakes
hackmd-deploy Aug 6, 2020
45b844b
Generalize the scheme to allow unsizing from arbitrary types
hackmd-deploy Aug 8, 2020
e7a0ef7
Fix trait method argument types
hackmd-deploy Aug 8, 2020
70a224c
custom index operation results
hackmd-deploy Aug 8, 2020
85b9d36
Remove reference to old version of RFC
hackmd-deploy Aug 8, 2020
5f63195
Various smaller review changes
hackmd-deploy Aug 8, 2020
c8c9a87
Explain how unsized structs work
hackmd-deploy Aug 12, 2020
6c89ffe
Intermediate push (no projection into unsized fields for C++-vtables)
hackmd-deploy Sep 10, 2020
e0c8ca7
Address some review nits
hackmd-deploy Sep 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 377 additions & 0 deletions text/0000-procedural-vtables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
- Feature Name: `procedural-vtables`
- Start Date: 2020-08-01
- RFC PR: [rust-lang/rfcs#2967](https://github.com/rust-lang/rfcs/pull/2967)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

Building a wide pointer from a concrete pointer (and thus vtable generation)
can be controlled by choosing a custom wide pointer type for the trait.
The custom wide pointer must implement a trait that generates said wide pointer
by taking a concrete pointer and a generic description of a trait impl.
By default, if no vtable generator function is specified for a specific trait,
the unspecified scheme used today keeps getting used.

# Motivation
[motivation]: #motivation

The only way we're going to satisfy all users' use cases is by allowing users
complete freedom in how their wide pointers' metadata is built.
Instead of hardcoding certain vtable layouts in the language
(https://github.com/rust-lang/rfcs/pull/2955) we can give users the capability
to invent their own wide pointers (and thus custom dynamically sized types).

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

In order to mark a trait (`MyTrait`) as using a custom vtable layout, you implement the `CustomUnsized` trait for `dyn MyTrait`.

```rust
trait MyTrait: SomeSuperTrait {
fn some_fn(&mut self, x: u32) -> i32;
}

impl CustomUnsized for dyn MyTrait {
type WidePointer = MyWidePointer;
// details later in this RFC
}

```

`MyWidePointer` is the backing type that is going to be used for wide pointers to the given trait, so

* `&dyn MyTrait`
* `Box<dyn MyTrait>`
* `Arc<dyn MyTrait>`

and any other container types that can use wide pointers. Normally when unsizing from a concrete
pointer like `&MyStruct` to `&dyn MyTrait` a wide pointer (that essentially is
`(&MyStruct, &'static Vtable)`) is produced. This is currently done via compiler magic, but
further down in this section you can see how it theoretically could be done in user code.
Likely it will stay compiler magic out of compile-time performance reasons.

All `impl`s of `MyTrait` will now use `MyWidePointer` for generating the wide pointer.

You are actually generating the wide pointer, not a description of it.
Since your `impl`'s `from` function is being interpreted in the target's environment, all target specific information will match up.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A from function hasn't yet been introduced at this point.

Now, you need some information about the `impl` in order to generate your metadata (and your vtable).
You get this information directly from the type (the `T` parameter) by adding trait bounds on it.

As an example, consider the function which is what normally generates your metadata.

```rust
#[repr(C)]
struct Pointer<T> {
ptr: *mut T,
vtable: &'static VTable<T>,
}

/// If the `owned` flag is `true`, this is an owned conversion like
/// in `Box<T> as Box<dyn Trait>`. This distinction is important, as
/// unsizing that creates a vtable in the same allocation as the object
/// (like C++ does), cannot work on non-owned conversions. You can't just
Comment on lines +88 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// in `Box<T> as Box<dyn Trait>`. This distinction is important, as
/// unsizing that creates a vtable in the same allocation as the object
/// (like C++ does), cannot work on non-owned conversions. You can't just
/// in `Box<T> as Box<dyn Trait>`. This distinction is important, as
/// an unsizing that creates a vtable in the same allocation as the object
/// (like C++ does) cannot work on non-owned conversions because you can't

/// move away the owned object. The flag allows you to forbid such
/// unsizings by triggering a compile-time `panic` with an explanation
/// for the user.
unsafe impl<T: MyTrait> const CustomUnsize<Pointer> for T {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might just be the Unsize trait.

fn unsize<const owned: bool>(ptr: *mut T) -> Pointer<T> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, the const generic here feels super clunky. I feel like there has to be a better way to do this, like maybe an auto trait that lets you control whether these casts are allowed? I'm not sure if there are any other use cases that warrant this level of complexity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk, I haven't been able to come up with anything better so far

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried this doesn't actually work. ptr may be owned but not point to the start of the allocation. For example:

use core::ops::CoerceUnsized;

struct MyWrapper<T: ?Sized> {
    some_data: u32,
    object: T,
}

impl<T: CoerceUnsized<U>, U> CoerceUnsized<MyWrapper<U>> for MyWrapper<T> {}

fn bad() -> Box<MyWrapper<dyn MyTrait>> {
    Box::new(MyWrapper {
        some_data: 42,
        object: MyTraitImplementor,
    })
    // Undefined behavior: dealloc is called with a pointer 4 bytes offset from the allocation
} 

I think this might be solved this by making the CustomUnsize impl optional. I originally thought it had to be required but I suppose it should be possible for a user to customize the dyn Trait type via CustomUnsized without allowing safe Box::new(TraitImplementor) as Box<dyn Trait> conversion via CustomUnsize. So to do the C++ shenanigans you would instead specialize the unsizing to only sound cases like so:

pub fn boxed_class<T: MyTrait>(data: T) -> Box<dyn MyTrait> {
    let ptr = Box::into_raw(Box::new((mytrait_vtable::<T>(), data)));
    unsafe { Box::from_raw(std::mem::transmute(ptr)) }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For C++, I think you would need Pin<Box<dyn MyTrait>> since C++ assumes objects don't move.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ptr may be owned but not point to the start of the allocation

The CustomUnsize impl gets a pointer to the thing being unsized, which in this case is MyWrapper<i32>, so this would all work out. The only time you don't have a pointer to the base is when you manually play with allocations, which may indeed be a problem (so if you have a custom Box that prepends with some padding).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CustomUnsize impl gets a pointer to the thing being unsized

Ah, that explains my confusion around the slice example. I'll have to think on the implications of it a bit.

// We generate the metadata and put a pointer to the metadata into
// the field. This looks like it's passing a reference to a temporary
// value, but this uses promotion
// (https://doc.rust-lang.org/stable/reference/destructors.html?highlight=promotion#constant-promotion),
// so the value lives long enough.
Pointer {
ptr,
vtable: &default_vtable::<T>(),
}
}
}

/// DISCLAIMER: this uses a `Vtable` struct which is just a part of the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// DISCLAIMER: this uses a `Vtable` struct which is just a part of the
/// DISCLAIMER: `ptr.vtable` is a `Vtable` struct which is a part of the

/// default trait objects. Your own trait objects can use any metadata and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow what "default trait objects" means in this context.

/// thus "vtable" layout that they want.
unsafe impl Unsized for dyn MyTrait {
type WidePointer = Pointer;
fn size_of(ptr: Pointer) -> usize {
ptr.vtable.size
}
fn align_of(ptr: Pointer) -> usize {
ptr.vtable.align
}
}

impl Drop for dyn MyTrait {
fn drop(&mut self) {
unsafe {
// using a dummy concrete type for `Pointer`
let ptr = transmute::<&mut dyn Trait, Pointer<()>>(self);
let drop = ptr.vtable.drop;
drop(&raw mut ptr.ptr)
}
}
}

const fn default_vtable<T: MyTrait>() -> VTable<T> {
// `VTable` is a `#[repr(C)]` type with fields at the appropriate
// places.
VTable {
size: std::mem::size_of::<T>(),
align: std::mem::align_of::<T>(),
drop: <T as Drop>::drop,
some_fn: fn (&mut T, u32) -> i32,
}
}
```

Now, if you want to implement a fancier vtable, this RFC enables you to do that.

## Null terminated strings (std::ffi::CStr)

This is how I see all extern types being handled.
There can be no impls of `CStr` for any type, because the `Unsize`
trait impl is missing. See the future extension section at the end of this RFC for
ideas that could support `CString` -> `CStr` unsizing by allowing `CString` to implement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this parallels std::String, it would actually do CString -> &CStr via Deref just like it does today. The reason is that a *CString doesn't point to the data, it points to a pointer+len+capacity which points to the data.

I think real advantage is that some sized, no-indirection type on the stack like CStrArray<10> could coerce to a CStr without slice-pointer-transmute shenanigans?

`CStr` instead of having a `Deref` impl that converts.

```rust
pub trait CStr {}

impl CustomUnsized<CStrPtr> for dyn CStr {
type WidePointer = *mut u8;
fn size_of(ptr: *mut u8) -> usize {
unsafe { strlen(ptr) }
}
fn align_of(_: *mut u8) -> usize {
1
}
}
```

## `[T]` as sugar for a `Slice` trait

We could remove `[T]` (and even `str`) from the language and just make it desugar to
a `std::slice::Slice` (or `StrSlice`) trait.

```rust
pub trait Slice<T> {}

#[repr(C)]
struct SlicePtr<T> {
ptr: *mut T,
len: usize,
}

// This impl must be in the `vec` module, to give it access to the `vec`
// internals instead of going through `&mut Vec<T>` or `&Vec<T>`.
impl<T> CustomUnsize<SlicePtr<T>> for Vec<T> {
fn unsize<const owned: bool>(ptr: *mut Vec<T>) -> SlicePtr<T> {
SlicePtr {
ptr: vec.data,
len: vec.len,
}
}
}

impl<T> CustomUnsized for dyn Slice<T> {
type WidePointer = SlicePtr<T>;
fn size_of(ptr: SlicePtr<T>) -> usize {
ptr.len * std::mem::size_of::<T>()
}
fn align_of(_: SlicePtr<T>) -> usize {
std::mem::align_of::<T>()
}
}

impl Drop for dyn Slice<T> {
fn drop(&mut self) {
unsafe {
let wide_ptr = transmute::<&mut dyn Slice<T>, SlicePtr<T>>(self);
let mut data_ptr = wide_ptr.ptr;
for i in 0..wide_ptr.len {
std::ptr::drop_in_place(data_ptr);
data_ptr = data_ptr.offset(1);
}
Comment on lines +223 to +227
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut data_ptr = wide_ptr.ptr;
for i in 0..wide_ptr.len {
std::ptr::drop_in_place(data_ptr);
data_ptr = data_ptr.offset(1);
}
let mut data_ptr = wide_ptr.ptr as *mut T;
for i in 0..wide_ptr.len {
std::ptr::drop_in_place(data_ptr.offset(i));
}

}
}
}
```

## C++ like vtables

Most of the boilerplate is the same as with regular vtables.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth clarifying how this could coexist with the existing C++ vtables RFC.


```rust
unsafe impl<T: MyTrait> const CustomUnsize<dyn MyTrait> for T {
fn unsize<const owned: bool>(ptr: *mut T) -> *mut (Vtable, ()) {
unsafe {
let new = Box::new((default_vtable::<T>(), std::ptr::read(ptr)));
std::alloc::dealloc(ptr);

Box::into_ptr(new) as *mut _
}
}
}


unsafe impl CustomUnsized for dyn MyTrait {
type WidePointer = *mut (VTable, ());
fn size_of(ptr: Self::WidePointer) -> usize {
unsafe {
(*ptr).0.size
}
}
fn align_of(self: Self::WidePointer) -> usize {
unsafe {
(*ptr).0.align
}
}
}
```

## Slicing into matrices via the `Index` trait
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a cool example.


The `Index` trait's `index` method returns references. Thus,
when indexing an `ndarray::Array2` in order to obtain a slice
into part of that array, we cannot use the `Index` trait and
have to invoke a function. Right now this means `array.index(s![5..8, 3..])`
in order to obtain a slice that takes indices 5-7 in the first
dimension and all indices after the 3rd dimension. Instead of
having our own `ArrayView` type like what is returned by `Array2::index`
we can create a trait with a custom vtable and reuse the `Index` trait.
We keep using the `ArrayView` type as the type of the wide pointer.

```rust
trait Slice2<T> {}

unsafe impl<T> CustomUnsized for dyn Slice2<T> {
type WidePointer = ndarray::ArrayView<T, Ix2>;
fn size_of(ptr: WidePointer) -> usize {
ptr.len() * std::mem::size_of::<T>()
}
fn align_of(ptr: WidePointer) -> usize {
std::mem::align_of::<T>()
}
}

impl<'a, T, U, V> Index<&'a SliceInfo<U, V>> for Array2<T> {
type Output = dyn Slice2<T>;
fn index(&self, idx: &'a SliceInfo<U, V>) -> &dyn Slice2<T> {
unsafe {
// This can get a better impl, but for simplicity we reuse
// the existing function.
transmute(Array2::index(idx))
}
}
}
```

## Zero sized references to MMIO
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this example, what happens if I do this?

// Reads the data behind `r` as bytes. Sound if `T` has no padding between fields.
unsafe fn read_memory_behind<T>(r: &T) -> Vec<u8> {
    let len = std::mem::size_of_val(r) as isize;
    let ptr = r as *const T as *const u8;
    (0..len).map(|o| *ptr.offset(o)).collect()
}

let bank: &dyn MyRegisterBank = ...;
read_memory_behind(bank); // WTF

The natural assumption is that this should output the 4 bytes in the register bank. But it doesn't necessarily because despite there being 4 bytes behind r according to the Unsize impl, r doesn't actually point to anything. What is the value of ptr? Is it undefined?

The best solution I can come up with is for as *const u8 to call project(0) but if that is the case, it should probably be documented here.


Instead of having one type per MMIO register bank, we could have one
trait per bank and use a zero sized wide pointer format.

```rust
trait MyRegisterBank {
fn flip_important_bit(&mut self);
}

unsafe impl CustomUnsized for dyn MyRegisterBank {
type WidePointer = ();
fn size_of(():()) -> usize {
4
}
fn align_of(():()) -> usize {
4
}
}

impl MyRegisterBank for dyn MyRegisterBank {
fn flip_important_bit(&mut self) {
std::mem::volatile_write::<bool>(42, !std::mem::volatile_read::<bool>(42))
}
}
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

When unsizing, the `<dyn MyTrait as CustomUnsize>::unsize` is invoked.
The only reason that trait must be
`impl const CustomUnsize` is to restrict what kind of things you can do in there, it's not
strictly necessary. This restriction may be lifted in the future.

For all other operations, the methods on `<dyn MyTrait as CustomUnsized>` is invoked.

When obtaining function pointers from vtables, instead of computing an offset, the `MyTrait for dyn MyTrait` impl's
methods are invoked, allowing users to insert their own logic for obtaining the runtime function pointer.
Through the use of MIR optimizations (e.g. inlining), the final LLVM assembly is tuned to be exactly the same as today.
The above statement contains significant hand-waving, but I propose we block the stabilization of this RFC on the
relevant optimizations existing, which will then allow users to reproduce the performance of the built-in unsizing.

These types' and trait's declarations are provided below:

```rust
unsafe trait CustomUnsized {
type WidePointer: Copy;
fn size_of(ptr: WidePointer) -> usize;
fn align_of(ptr: WidePointer) -> usize;
}

unsafe trait CustomUnsize<DynTrait> where DynTrait: CustomUnsized {
fn unsize<const owned: bool>(t: *mut Self) -> DynTrait::WidePointer;
}
```

The

# Drawbacks
[drawbacks]: #drawbacks

* This may be a serious case of overengineering. We're basically taking vtables out of the language and making dynamic dispatch on trait objects a user definable thing.
* This may slow down compilation, likely entirely preventable by keeping a special case in the compiler for regular trait objects.
* This completely locks us into never adding multiple vtable formats for a single trait. So you can't use a trait both as a C++ like vtable layout in some situations and a Rust wide pointer layout in others.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

This is a frequently requested feature and as a side effect obsoletes `extern type`s which have all kinds of problems (like the inability to invoke `size_of_val` on pointers to them).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider mentioning that we could instead let the language handle just the data pointer, ridding ourselves of the project and unsize functions at the cost of not controlling the entire pointer layout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case we'd go with any of the other RFCs ;) Which is something I do list further down.

# Prior art
[prior-art]: #prior-art

I don't know of any prior art where a compile-time language has procedural vtable generation. You can see lots of similar tricks being employed in dynamic languages like ruby and python, where "types" are built by changing functions in objects at runtime. If this is just done at startup and not during actual program execution, it is essentially the same concept here, except that our "startup phase" is at compile-time.

## Other Custom DST RFCs

This list is shamelessly taken from [strega-nil's Custom DST RFC](https://github.com/rust-lang/rfcs/pull/2594):

- [mzabaluev's Version](https://github.com/rust-lang/rfcs/pull/709)
- [strega-nil's new version](https://github.com/rust-lang/rfcs/pull/2594)
- [strega-nil's Old Version](https://github.com/rust-lang/rfcs/pull/1524)
- [japaric's Pre-RFC](https://github.com/japaric/rfcs/blob/unsized2/text/0000-unsized-types.md)
- [mikeyhew's Pre-RFC](https://internals.rust-lang.org/t/pre-erfc-lets-fix-dsts/6663)
- [MicahChalmer's RFC](https://github.com/rust-lang/rfcs/pull/9)
- [nrc's Virtual Structs](https://github.com/rust-lang/rfcs/pull/5)
- [Pointer Metadata and VTable](https://github.com/rust-lang/rfcs/pull/2580)
- [Syntax of ?Sized](https://github.com/rust-lang/rfcs/pull/490)

This RFC differs from all the other RFCs in that it focusses on a procedural way to generate vtables,
thus also permitting arbitrary user-defined compile-time conditions by aborting via `panic!`.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- Do we want this kind of flexibility? With power comes responsibility...
- I believe we can do multiple super traits, including downcasts with this scheme and no additional extensions, but I need to make sure that's true.
- This scheme support downcasting `dyn A` to `dyn B` if `trait A: B` if you `impl CustomUnsize<TraitAWidePtrType> for dyn B`
* this scheme allows `dyn A + B`. By implemting `CustomUnsized` for `dyn A + B`
* Need to be generic over the allocator, too, so that reallocs are actually sound.
* how does this RFC (especially the `owned` flag) interact with `Pin`?

# Future possibilities
[future-possibilities]: #future-possibilities

* We can change string slice (`str`) types to be backed by a `trait StrSlice` which uses this scheme
to generate just a single `usize` for the metadata (see also the `[T]` demo).
* This scheme is forward compatible to adding associated fields later, but it is a breaking change to add such fields to an existing trait.