Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify design for *Label types #4954

Closed
JoJoJet opened this issue Jun 7, 2022 · 5 comments
Closed

Simplify design for *Label types #4954

JoJoJet opened this issue Jun 7, 2022 · 5 comments
Labels
A-App Bevy apps and plugins A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change C-Performance A change motivated by improving speed, memory usage or compile times

Comments

@JoJoJet
Copy link
Member

JoJoJet commented Jun 7, 2022

Bevy currently has several different kinds of label type, all of which are essentially duplications of each other. For the sake of brevity, I will collectively refer to these as just Label.

What problem does this solve or what need does it fill?

The current design for Label is overly complex and "heavy". The design is centered around trait objects and boxing, which makes for poor ergonomics, and adds overhead to simple comparisons between labels. Since this is such a common operation, we should strive to make it as cheap as possible.

What solution would you like?

To guide the design, let's check out the current docs for Label:

Defines a set of strongly-typed labels for a class of objects

  1. It's a label, so it needs text.
  2. It's a set, so many different types must be coercible to it.
  3. It's strongly typed, so two distinct labels with the same text must not collide.

Label is now a data struct, copy-able. It keeps track of the type it was made with and stores a string reference.

use std::any::TypeId;

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Label(TypeId, &'static str);

impl Debug/Display for Label { /* ... */ }

pub trait IntoLabel: Copy + 'static {
    fn into_label(self) -> Label {
        let id = TypeId::of::<Self>();
        let text = self.as_str();
        Self(id, text)
    }
    fn as_str(&self) -> &'static str;
}

The 'static requirement for the text may be alarming to some, but I believe the tradeoff is well worth it. It greatly simplifies the implementation and leads to less code being generated and executed, since there's no generic magic, dynamic dispatch, or even branches. And the restriction shouldn't be a problem in most cases, since Labels are usually unit structs, c-style enums, and fn items -- all of these types can easily be represented by a string literal.

#[derive(Clone, Copy)]
pub enum SomeLabels {
    Foo,
    Bar
}

// result of #[derive(IntoLabel)]
impl IntoLabel for SomeLabel {
    fn as_str(&self) -> &'static str {
        match self {
            Self::Foo => "Foo",
            Self::Bar => "Bar",
        }
    }
}

fn some_system() {
    // ...
}

// as part of some giant macro to implement for all Fn types...
impl<F: FnMut() + Copy + 'static> IntoLabel for F {
    fn as_str(&self) -> &'static str {
        std::any::type_name::<F>()
    }
}

In cases when a user really needs runtime Labels, they could always just leak a string to get a 'static reference.

What alternative(s) have you considered?

Don't change the design. The old design may not be perfect but it has worked so far.

@JoJoJet JoJoJet added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled labels Jun 7, 2022
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change A-App Bevy apps and plugins C-Performance A change motivated by improving speed, memory usage or compile times and removed C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled labels Jun 7, 2022
@alice-i-cecile
Copy link
Member

The problem with this design is actually ecosystem level compatibility. What happens if you have two competing plugins in your tree that both implement a label that desugars into "Physics"? Label mangling will wreak havoc with system ordering, which is why I want to completely remove stringly-typed labels in #4409.

That said, I am interested in trying to reduce the complexity and improve performance. The types should remain distinct for clarity and type safety, but maybe there's an alternate design.

@alice-i-cecile
Copy link
Member

There's actually another problem here: stringly-typed labels can be impersonated, violating plugin privacy boundaries in painful and surprising ways.

@JoJoJet
Copy link
Member Author

JoJoJet commented Jun 7, 2022

The problem with this design is actually ecosystem level compatibility. What happens if you have two competing plugins in your tree that both implement a label that desugars into "Physics"? Label mangling will wreak havoc with system ordering, which is why I want to completely remove stringly-typed labels in #4409.

That said, I am interested in trying to reduce the complexity and improve performance. The types should remain distinct for clarity and type safety, but maybe there's an alternate design.

I don't think that would be an issue, since we're also storing the TypeId of whatever type implements IntoSystem. This way foo::Physics and bar::Physics remain distinct.

@JoJoJet
Copy link
Member Author

JoJoJet commented Jun 7, 2022

In this design, the Label type is sort of a specialized version of Box<dyn IntoLabel> that doesn't require trait objects. (apologies if the metaphor doesn't make sense)

@alice-i-cecile
Copy link
Member

Ah, I see! There's a TypeId stored in there. Okay, I'm on board: open a PR?

bors bot pushed a commit that referenced this issue Jul 14, 2022
# Objective

- Closes #4954 
- Reduce the complexity of the `{System, App, *}Label` APIs.

## Solution

For the sake of brevity I will only refer to `SystemLabel`, but everything applies to all of the other label types as well.

- Add `SystemLabelId`, a lightweight, `copy` struct.
- Convert custom types into `SystemLabelId` using the trait `SystemLabel`.

## Changelog

- String literals implement `SystemLabel` for now, but this should be changed with #4409 .

## Migration Guide

- Any previous use of `Box<dyn SystemLabel>` should be replaced with `SystemLabelId`.
- `AsSystemLabel` trait has been modified.
    - No more output generics.
    - Method `as_system_label` now returns `SystemLabelId`, removing an unnecessary level of indirection.
- If you *need* a label that is determined at runtime, you can use `Box::leak`. Not recommended.

## Questions for later

* Should we generate a `Debug` impl along with `#[derive(*Label)]`?
* Should we rename `as_str()`?
* Should we remove the extra derives (such as `Hash`) from builtin `*Label` types?
* Should we automatically derive types like `Clone, Copy, PartialEq, Eq`?
* More-ergonomic comparisons between `Label` and `LabelId`.
* Move `Dyn{Eq, Hash,Clone}` somewhere else.
* Some API to make interning dynamic labels easier.
* Optimize string representation
    * Empty string for unit structs -- no debug info but faster comparisons
    * Don't show enum types -- same tradeoffs as asbove.
@bors bors bot closed this as completed in c43295a Jul 14, 2022
inodentry pushed a commit to IyesGames/bevy that referenced this issue Aug 8, 2022
# Objective

- Closes bevyengine#4954 
- Reduce the complexity of the `{System, App, *}Label` APIs.

## Solution

For the sake of brevity I will only refer to `SystemLabel`, but everything applies to all of the other label types as well.

- Add `SystemLabelId`, a lightweight, `copy` struct.
- Convert custom types into `SystemLabelId` using the trait `SystemLabel`.

## Changelog

- String literals implement `SystemLabel` for now, but this should be changed with bevyengine#4409 .

## Migration Guide

- Any previous use of `Box<dyn SystemLabel>` should be replaced with `SystemLabelId`.
- `AsSystemLabel` trait has been modified.
    - No more output generics.
    - Method `as_system_label` now returns `SystemLabelId`, removing an unnecessary level of indirection.
- If you *need* a label that is determined at runtime, you can use `Box::leak`. Not recommended.

## Questions for later

* Should we generate a `Debug` impl along with `#[derive(*Label)]`?
* Should we rename `as_str()`?
* Should we remove the extra derives (such as `Hash`) from builtin `*Label` types?
* Should we automatically derive types like `Clone, Copy, PartialEq, Eq`?
* More-ergonomic comparisons between `Label` and `LabelId`.
* Move `Dyn{Eq, Hash,Clone}` somewhere else.
* Some API to make interning dynamic labels easier.
* Optimize string representation
    * Empty string for unit structs -- no debug info but faster comparisons
    * Don't show enum types -- same tradeoffs as asbove.
james7132 pushed a commit to james7132/bevy that referenced this issue Oct 28, 2022
# Objective

- Closes bevyengine#4954 
- Reduce the complexity of the `{System, App, *}Label` APIs.

## Solution

For the sake of brevity I will only refer to `SystemLabel`, but everything applies to all of the other label types as well.

- Add `SystemLabelId`, a lightweight, `copy` struct.
- Convert custom types into `SystemLabelId` using the trait `SystemLabel`.

## Changelog

- String literals implement `SystemLabel` for now, but this should be changed with bevyengine#4409 .

## Migration Guide

- Any previous use of `Box<dyn SystemLabel>` should be replaced with `SystemLabelId`.
- `AsSystemLabel` trait has been modified.
    - No more output generics.
    - Method `as_system_label` now returns `SystemLabelId`, removing an unnecessary level of indirection.
- If you *need* a label that is determined at runtime, you can use `Box::leak`. Not recommended.

## Questions for later

* Should we generate a `Debug` impl along with `#[derive(*Label)]`?
* Should we rename `as_str()`?
* Should we remove the extra derives (such as `Hash`) from builtin `*Label` types?
* Should we automatically derive types like `Clone, Copy, PartialEq, Eq`?
* More-ergonomic comparisons between `Label` and `LabelId`.
* Move `Dyn{Eq, Hash,Clone}` somewhere else.
* Some API to make interning dynamic labels easier.
* Optimize string representation
    * Empty string for unit structs -- no debug info but faster comparisons
    * Don't show enum types -- same tradeoffs as asbove.
ItsDoot pushed a commit to ItsDoot/bevy that referenced this issue Feb 1, 2023
# Objective

- Closes bevyengine#4954 
- Reduce the complexity of the `{System, App, *}Label` APIs.

## Solution

For the sake of brevity I will only refer to `SystemLabel`, but everything applies to all of the other label types as well.

- Add `SystemLabelId`, a lightweight, `copy` struct.
- Convert custom types into `SystemLabelId` using the trait `SystemLabel`.

## Changelog

- String literals implement `SystemLabel` for now, but this should be changed with bevyengine#4409 .

## Migration Guide

- Any previous use of `Box<dyn SystemLabel>` should be replaced with `SystemLabelId`.
- `AsSystemLabel` trait has been modified.
    - No more output generics.
    - Method `as_system_label` now returns `SystemLabelId`, removing an unnecessary level of indirection.
- If you *need* a label that is determined at runtime, you can use `Box::leak`. Not recommended.

## Questions for later

* Should we generate a `Debug` impl along with `#[derive(*Label)]`?
* Should we rename `as_str()`?
* Should we remove the extra derives (such as `Hash`) from builtin `*Label` types?
* Should we automatically derive types like `Clone, Copy, PartialEq, Eq`?
* More-ergonomic comparisons between `Label` and `LabelId`.
* Move `Dyn{Eq, Hash,Clone}` somewhere else.
* Some API to make interning dynamic labels easier.
* Optimize string representation
    * Empty string for unit structs -- no debug info but faster comparisons
    * Don't show enum types -- same tradeoffs as asbove.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-App Bevy apps and plugins A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change C-Performance A change motivated by improving speed, memory usage or compile times
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants