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

Document and lock down bevy_ecs::component APIs #6750

Closed
wants to merge 20 commits into from

Conversation

james7132
Copy link
Member

@james7132 james7132 commented Nov 25, 2022

Objective

Document and lock down bevy_ecs::component.

Addresses #3362 for bevy_ecs::component.

Solution

  • Make mutative APIs on Components pub(crate). Redirect users to the APIs on World instead of Components.
  • Remove the Components: Default implementation. Replace it with a pub(crate) new function. Forces Components to be initialized in the context of a World.
  • Rename Components::get_resource_id to resource_id to make it consistent with Components::component_id
  • Make ComponentId a fully opaque ID.
  • Make ComponentDescriptor constructors private except for the user-facing case for making components for foreign types.
  • Make ComponentDescriptor more of a parameter type than a storage type by removing it's accessors. Force users to use ComponentInfo and getting it via Components's fetch APIs.
  • Document as many of the undocumented types and APIs in the module as possible.

Changelog

Changed: Components::get_resource_id has been renamed to Components::resource_id.
Changed: ComponentDescriptor::new_with_layout has replaced ComponentDescriptor::new.
Removed: Components's Default implementation.
Removed: Components::init_component, Components::init_resource, Components::init_non_send, Components::init_with_component_descriptor.
Removed: ComponentId::new and ComponentId::index.
Removed: ComponentDescriptor::new_resource, ComponentData::type_id, ComponentDescriptor::name, ComponentDescriptor::storage_type.

@james7132 james7132 added C-Docs An addition or correction to our documentation A-ECS Entities, components, systems, and events M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Nov 25, 2022
@james7132 james7132 added this to the 0.10 milestone Nov 25, 2022
crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
@@ -231,12 +271,13 @@ pub struct ComponentId(usize);

impl ComponentId {
#[inline]
pub const fn new(index: usize) -> ComponentId {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to be able to round-trip ComponentId through some sort of shareable identifier.
Is there a reason for disallowing this?

Maybe we can have ComponentId::from_bits/ComponentId::into_bits (or s/bits/raw or similiar) to clearly document that they can only be created from each other and aren't stable across runs?

Copy link
Member Author

@james7132 james7132 Nov 25, 2022

Choose a reason for hiding this comment

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

The primary one is to avoid users creating their own ComponentIds arbitrarily and converting them into something that might be saved for the exact same reasons you've enumerated: it's not stable across runs and shouldn't be easy to convert to something where you can easily manipulate the representation and reconstruct a valid ComponentId out of it, nor allow users to serialize it to a file or over the network.

Is the use case here to communicate this across runtime boundaries (i.e. scripting)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we need to be able to pass component IDs to and from scripts to query components from an entity.

Copy link
Member Author

Choose a reason for hiding this comment

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

If the point is to cross a potential FFI boundary for scripting, we can set it to repr(transparent) if need be, is that sufficient?

Copy link
Contributor

Choose a reason for hiding this comment

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

repr(transparent) seems to be strictly worse to me because it's way less obvious to use (I think you have to transmute?) and it gives you no obvious place to list the limitations of round tripping through usize.

}

#[inline]
pub fn init_resource<T: Resource>(&mut self) -> ComponentId {
pub(crate) fn init_resource<T: Resource>(&mut self) -> ComponentId {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be left public, or be replaced with init_resource_with_descriptor.

I see no harm in having

  • Components::init_component_with_descriptor required for dynamic types
  • Components::init_resource_with_descriptor required for dynamic types
  • Components::init_component<T>
  • Components::init_resource<T>
    The latter two are useful even externally, because it is impossible to use insert_resource_by_id with a non-initialized resource.

So if you have a resource that isn't automatically inserted, it would be impossible to insert that resource from a script.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually init_component_with_descriptor and init_resource_with_descriptor do the same thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to have these methods not on Components but on World, then we need to expose all of them. Currently, only init_component exists, but it has only internal usages. Also I don't like the name on World, because init_resource means "insert the default value", so init_component should not sound as similar as it does.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is going to be tough. If we want to do this on World, we also want to make storages_mut, which is a giant hole for unsoundness if left open.

We could make ComponentsMut<'a> wrap a &'a mut ComponentsInternal and &'a mut Storages, but that might be a bit overkill for just this.

crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
@alice-i-cecile
Copy link
Member

@jakobhellermann's comments are excellent: I'm in agreement with all of those :)

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

More documentation feedback :)

crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
james7132 and others added 10 commits November 25, 2022 12:17
Co-authored-by: Jakob Hellermann <jakob.hellermann@protonmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
#[derive(Debug, Default)]
/// The metadata store for all components within a [`World`].
///
/// A public read-only API is accessbile. To initialize a new component,
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
/// A public read-only API is accessbile. To initialize a new component,
/// A public read-only API is accessible. To initialize a new component,

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 a little confused by what you mean here. This sentence implies the existence of a public read-only API, but I'm not sure how that relates to the components struct.

///
/// A public read-only API is accessbile. To initialize a new component,
/// use [`World::init_component`], [`World::init_resource`], [`World::init_non_send_resource`] or
/// [`World::init_component_with_descriptor`]. Initialized components cannot be removed or
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ What is the difference between an initialized vs uninitialized component?

Copy link
Member

Choose a reason for hiding this comment

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

IIRC components have to be assigned a ComponentId on a per-world basis, which occurs during initialization.

Would be good to expland on here though.

indices: std::collections::HashMap<TypeId, usize, fxhash::FxBuildHasher>,
resource_indices: std::collections::HashMap<TypeId, usize, fxhash::FxBuildHasher>,
ids: HashMap<TypeId, ComponentId, fxhash::FxBuildHasher>,
resource_ids: HashMap<TypeId, ComponentId, fxhash::FxBuildHasher>,
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ Sorry if this is a super basic question, but I always assumed resources were something totally different from components. But here we are storing resource ids in the components struct, which makes me think they are the same thing.

Are resources just components?

Copy link
Member

Choose a reason for hiding this comment

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

We're lacking a good word for components + resources :) They share a lot of the same infrastructure, but the naming uses Component in a few places :(

Copy link
Member Author

Choose a reason for hiding this comment

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

They still have the same IDs, ArchetypeComponentIds, etc. for controlling access and identification, even if the underlying methodology and storage is separated.

@@ -410,34 +493,43 @@ impl Components {
index
}

/// Gets the total number of components registered.
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ What is a registered component? Is it the same thing as an initialized component?

Copy link
Contributor

@Carter0 Carter0 left a comment

Choose a reason for hiding this comment

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

Thanks again, digging into this stuff always fascinates me :)

james7132 and others added 2 commits November 30, 2022 03:24
Co-authored-by: Carter Weinberg <weinbergcarter@gmail.com>
Co-authored-by: Carter Weinberg <weinbergcarter@gmail.com>
@alice-i-cecile
Copy link
Member

I'm unclear of the comments from #6750 (comment) WRT the publicness of init_resouce have been addressed. I don't think it's acceptable to break / shut off dynamic resource types in this PR, but I'm not personally sure on the right path forward.

@inodentry
Copy link
Contributor

I get very worried and uneasy whenever I see PRs for "make things no longer pub, because things are excessively pub".

You always run the risk of shutting down more exotic usecases that you might not have thought anyone would want to do.

You risk causing pain to future users who have a clever idea for something they want to do, but can't, because you made the things they need private.

One of Bevy's great strengths is how easy it is for people to hack on, and how so many things can be implemented in 3rd-party crates.

Please don't "lock down" Bevy!

@james7132
Copy link
Member Author

james7132 commented Dec 26, 2022

Please don't "lock down" Bevy!

I've stated this on Discord but I'll reiterate it here for better logging of the rationale.

I'm in general agreement here about the use of pub APIs, within reason. The primary exception lies with bevy_ecs, where the unsafe APIs are riddled with implementation specific invariants. It's borderline impossible to use them safely outside of the crate, so the few ones we do expose should be well audited, well documented, damn near stable, and well justified as to why it should be public. Making them pub means we must take them into account inside bevy_ecs too, which can combinatorially expand the number of ways both first and third party crates and bevy_ecs itself can cause unsoundness.

Even outside of unsafe use of bevy_ecs, it's imperative we stay conscious of the API surface we're exposing. The modularity of the engine demands we expose certain APIs at crate boundaries, but that also means that we can't change it once we hit 1.0. Hyrum's Law states that if it can be observed, someone will depend on it. If we are to make more of the engine public, the interface should be more curated and deter users from footguns, especially when unsafe is involved.

@alice-i-cecile alice-i-cecile added the X-Controversial There is active debate or serious implications around merging this PR label Jan 8, 2023
@alice-i-cecile alice-i-cecile removed this from the 0.10 milestone Jan 8, 2023
@james7132 james7132 added this to the 0.11 milestone Feb 27, 2023
@alice-i-cecile
Copy link
Member

@james7132 I have agreement in principle with you on these choices, but there's some outstanding controversy. This is in the 0.11 milestone but things seem to have stalled: what would you like to do with this PR?

@james7132
Copy link
Member Author

Let's move it to 0.12. There's quite a few things that need to be addressed here and I don't think we can rush them out before the 0.11 deadline.

@james7132 james7132 modified the milestones: 0.11, 0.12 Jun 19, 2023
@alice-i-cecile alice-i-cecile removed this from the 0.12 milestone Sep 29, 2023
@james7132
Copy link
Member Author

Closing this out as there are other PRs that got to this beforehand.

@james7132 james7132 closed this Mar 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Docs An addition or correction to our documentation M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants