Skip to content

A Static receiver type for dyn-trait methods that do not take values #603

Open
@197g

Description

@197g

Proposal

Problem statement

In some cases trait methods should not depend on the value of any implementation's types but rather just the type. These methods are called 'static' in some other programming languages. A few remarkable examples of such a method are std::any::type_name() -> &'static str or TypeId. Similarly, custom registries in an ECS require efficiently comparable tokens which depend only on the type. In reflection, we also query a lot more information from the type. In these situations common behavior could be abstracted as trait:

trait TypeName {
    fn type_name() -> &'static str;
}

However, such a trait is not 'dyn-compatible' as there is no receiver which would hold the metadata v-table to dispatch on in the first place. Solving this requires custom self types, unsize coercion, unsize, and dispatch to work hand-in-hand, all of which are unstable and somewhat tricky to justify correctly.

Motivating examples or use cases

With a new marker value that behaves similar to a smart-pointer to no data, we can augment the trait method above with a synthetic receiver value that makes the trait dyn-compatible.

use core::marker::Static;

trait TypeName {
    fn type_name(_: Static<Self>) -> &'static str;
}

fn call_on_dyn(val: Static<dyn TypeName>) {
    let name = val.type_name();
}

Consider ethernet frames. The header contains a magic number (Ethertype) indicating the kind of payload and a length. The payload can be readily abstracted as a byte slice, but in a typed system the magic number is a function of the type used to model the payload. But for parsing or efficient filters, we do not have any payload so the Ethertype should be available statically. Further, in embedded programming the code sizes is of large concern. It is thus uncomfortable to provide serialization as a polymorphic function over different frame types which might result in paying with code-size when all implementing types (a few hundreds) are being compiled.

A dyn-compatible trait would address these issues:

trait EthernetPayload {
    fn ether_type(_: Static<Self>) -> u16;
    fn bytes(&self) -> &[u8];
}

fn frame(buffer: &mut [u8], payload: &dyn EthernetPayload, device: &Phy) {
    let ty = Static::from(payload).ether_type();
    // … initialize the addresses
    buffer[12..14].copy_from_slice(&ty.to_be_bytes());
    buffer[14..].copy_from(payload.bytes());
}

/// configure the device to let some frames through, in hardware (e.g. eBPF)
/// Can be called with coercion: `add_prefilter(device, Static::<ipv4>::new())`
fn add_prefilter(device: &Phy, ty: Static<dyn EthernetPayload>) {
    // .. implementation detail omitted.
    device.exclude(ty.ether_type());
}

In image processing the data layout of color information in a file is often described in the metadata of an image format with either enumerated values or a stringified enum. To properly interpret and encode such color information the type used to access a buffer must match the dynamic value in a file or texture descriptor. The strictest way to achieve this binding is through the type system. Similar to the ethernet case, this is not dyn compatible. The result of this is again some code bloat, otherwise this case is very similar to the ethernet problem and could be solved similarly.

Solution sketch

// core::marker.rs
/// Can be used as a receiver value for static trait methods.
///
/// # Examples
///
/// This type can be coerced and trait methods with a suitably typed `self` parameter can be called
/// on such values:
///
/// ```
/// #![feature(static_dyn_dispatch)]
/// #![feature(arbitrary_self_types)]
///
/// use core::marker::Static;
///
/// trait PrintType {
///    fn type_name(self: Static<Self>) -> &'static str;
/// }
///
/// impl<T: 'static> PrintType for T {
///     fn type_name(self: Static<Self>) -> &'static str {
///         std::any::type_name::<T>()
///     }
/// }
///
/// let marker_u8 = Static::<u8>::new();
/// let marker_dyn: Static<dyn PrintType> = marker_u8;
/// eprintln!("This dyn marker refers to: {}", marker_dyn.type_name());
/// ```
#[unstable(feature = "static_dyn_dispatch", issue = "none")]
#[allow(missing_debug_implementations)]
pub struct Static<T: ?Sized>(*const T);

impl<T> Static<T> {
    /// Create a new [`Static`], which is equivalent to any other such value.
    #[unstable(feature = "static_dyn_dispatch", issue = "none")]
    pub const fn new() -> Self {
        Static(core::ptr::dangling())
    }
}

#[unstable(feature = "static_dyn_dispatch", issue = "none")]
impl<T: ?Sized> core::ops::Receiver for Static<T> {
    type Target = T;
}

#[unstable(feature = "static_dyn_dispatch", issue = "none")]
impl<T: ?Sized, U: ?Sized> core::ops::DispatchFromDyn<Static<U>> for Static<T> where
    T: core::marker::Unsize<U>
{
}

#[unstable(feature = "static_dyn_dispatch", issue = "none")]
impl<T: ?Sized, U: ?Sized> core::ops::CoerceUnsized<Static<U>> for Static<T> where
    T: core::marker::Unsize<U>
{
}

#[unstable(feature = "static_dyn_dispatch", issue = "none")]
impl<T> Default for Static<T> {
    fn default() -> Self {
        Self::new()
    }
}

#[unstable(feature = "static_dyn_dispatch", issue = "none")]
impl<T: ?Sized> From<&'_ T> for Static<T> {
    fn from(value: &'_ T) -> Self {
        // It is okay we ignore provenance here, the pointer is just for show.
        Static(value as *const T)
    }
}

#[unstable(feature = "static_dyn_dispatch", issue = "none")]
impl<T: ?Sized> From<&'_ mut T> for Static<T> {
    fn from(value: &'_ mut T) -> Self {
        // It is okay we ignore provenance here, the pointer is just for show.
        Static(value as *const T)
    }
}

Alternatives

We could make functions callable that do not mention any self parameter by having a form of silent parameter and some magic around the value on which to call it (e.g. PointerMetadata<T>). This would create implicit rules that must be correctly taken care of and tracked to avoid unsoundness of dyn trait objects.

Links and related work

Implementation in rustc is provided here: https://github.com/197g/rust/tree/dyn-dispatch-from-static

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions