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

Custom derive for Component trait #192

Merged
merged 7 commits into from
Jul 22, 2017
Merged

Custom derive for Component trait #192

merged 7 commits into from
Jul 22, 2017

Conversation

ebkalderon
Copy link
Member

@ebkalderon ebkalderon commented Jun 12, 2017

This PR introduces an optional specs_derive crate containing a custom derive macro for defining new components in a less verbose way. The ergonomics loosely resemble the derivative crate.

Before

#[derive(Debug)]
struct Pos(f32, f32, f32);

impl Component for Pos {
    type Storage = VecStorage<Pos>;
}

After

#[derive(Component, Debug)]
struct Pos(f32, f32, f32);

The macro will store components in VecStorages by default. To specify a different storage type, you may use the #[component] attribute.

#[derive(Component, Debug)]
#[component(HashMapStorage)]
struct Pos(f32, f32, f32);

I also included a revised basic.rs example called basic_derive.rs to demonstrate its usage, but this can probably be omitted from the PR if people ultimately prefer implementing Component the explicit way.

EDIT: Use DenseVecStorage by default.

@torkleyy torkleyy added the ready label Jun 13, 2017
@torkleyy torkleyy self-requested a review June 13, 2017 03:42
Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

Great to see you hear, @ebkalderon!

Looks great, but I would use another storage for the default. Options would be BTreeStorage and DenseVecStorage. The latter one was a bit faster when I benchmarked it IIRC.

use quote::Tokens;

#[proc_macro_derive(Component, attributes(component))]
pub fn component(input: TokenStream) -> TokenStream {
Copy link
Member

Choose a reason for hiding this comment

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

Would be nice to add a small doc comment here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, will do.

_ => None,
})
.cloned()
.unwrap_or(Ident::new("::specs::VecStorage"));
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if VecStorage should be the default. Maybe DenseVecStorage would be better here?

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 that's indeed the case, I'll switch it to use DenseVecStorage.


fn impl_component(ast: &MacroInput) -> Tokens {
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Copy link
Member

@torkleyy torkleyy Jun 13, 2017

Choose a reason for hiding this comment

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

Didn't even know that syn has this method. Nice!

Copy link
Member Author

Choose a reason for hiding this comment

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

It's very nice! I learned about it from the Procedural Macros chapter of the Rust book.

@@ -1,7 +1,7 @@
use std;
use std::cell::UnsafeCell;

use hibitset::{BitSetAnd, BitIter, BitSetLike, BitProducer};
use hibitset::{BitIter, BitProducer, BitSetAnd, BitSetLike};
Copy link
Member

Choose a reason for hiding this comment

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

Whoops, forgot to reorder this one.

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

Btw, I really like the derive for components, so maybe we could update the README with it?

@ebkalderon
Copy link
Member Author

@torkleyy Sure, I would be happy to update the README! But if we're using #[derive(Component)] by default, should I remove the basic_derive.rs example and update the existing examples to use the macro?

@torkleyy
Copy link
Member

@ebkalderon

should I remove the basic_derive.rs example and update the existing examples to use the macro?

Yes, I think we should do that. And we could just show both ways of implementing Component in the basic example to explain what it desugars to and then use the #[derive(Component)] everywhere we want.

@ebkalderon
Copy link
Member Author

ebkalderon commented Jun 13, 2017

@torkleyy Another question: would you like me to re-export these macros under the top-level specs crate, or would you prefer users explicitly specify #[macro_use] extern crate specs_derive in their projects?

EDIT: If so, under that same logic, couldn't the shred_derive macros also be re-exported under specs too?

@torkleyy
Copy link
Member

@ebkalderon Is it possible to reexport custom-derive macros?

@ebkalderon
Copy link
Member Author

ebkalderon commented Jun 13, 2017

@torkleyy I'm pretty sure you can. I can quickly check on my end to verify, once I'm done revising the PR.

EDIT: Yes we can! 🎉 Would you like me to do that for both specs_derive and shred_derive?

@ebkalderon
Copy link
Member Author

@torkleyy I successfully re-exported specs_derive and compiled the examples with only the specs crate, but I can't do the same with shred_derive since #[derive(SystemData)] has a line which expands to ::shred::SystemData. Oh well, at least specs_derive re-exports fine.

@ebkalderon
Copy link
Member Author

Finally done! I have updated the README, book, examples, and and as many doc comments as I could find. Let me know if there are any nits to take care of.

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

Great, thanks for addressing all of my comments. Only one nit left.

README.md Outdated
@@ -33,20 +33,19 @@ Unlike most other ECS libraries out there, it provides
## Example

```rust
#[macro_use]
extern crate specs;
Copy link
Member

Choose a reason for hiding this comment

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

Let's not have any extern crates or imports in the README, it's irrelevant for the API usage.

Copy link
Member Author

Choose a reason for hiding this comment

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

I understand, I'll fix that.

/// #[derive(Component, Debug)]
/// #[component(HashMapStorage)]
/// struct Pos(f32, f32, f32);
/// ```
Copy link
Member

Choose a reason for hiding this comment

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

That's great, thanks 👍

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

LGTM, let's let @kvark glance over it before merging.

@torkleyy torkleyy requested a review from kvark June 13, 2017 17:07
@torkleyy torkleyy modified the milestone: 0.9 Jun 13, 2017
examples/full.rs Outdated

// -- Components --
// A component exists for 0..n
// entities.

#[derive(Clone, Debug)]
// `VecStorage` is meant to be used for components that are in almost every
// entity tend to be highly contested.
Copy link
Member Author

Choose a reason for hiding this comment

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

Whoops, forgot the word "and" here.

@kvark
Copy link
Member

kvark commented Jun 14, 2017

I'm sorry but I don't find this useful enough. It's saving just a few lines of user code, at the cost of more lines in our code (specs_derive) but, more importantly, another layer of indirection to think about. I'd rather want people to implement Component and understand the storage types, which is arguably easier than applying magic attributes.

@ebkalderon
Copy link
Member Author

@kvark Regardless, people will need to understand the storage types in order to use the attribute properly; it's just presented more concisely from the user's side. But still, you raise some good points. In the end, it's your call! 😀

@malleusinferni
Copy link
Contributor

I'm using specs in a codebase with nearly 50 different component types (and climbing). I currently implement Component via a macro, just to cut down on the number of places where I can make a mistake. While I can't speak for other users, I would personally prefer to use #[derive] for this purpose. If this change is rejected, I'll roll my own implementation.

@kvark
Copy link
Member

kvark commented Jun 14, 2017

@malleusinferni thanks for your input! What's wrong with the macro you are using now?
Also, do @csherratt, @Aceeri and @WaDelma have strong opinions?

@csherratt
Copy link
Contributor

My first instinct was exactly what @kvark said. It saves a few lines per component, and comes at a cost of some indirection which can confuse new users. That being said, I'm not strong in my position.

I think we could probably strike a balance, if @malleusinferni finds it useful. There are bound to be other users who want to use the more terse form. Since this must be published as a secondary crate anyhow, maybe we can show the old form in the examples. We can include a quick passage in the book/readme so that people can find this crate.

@torkleyy
Copy link
Member

torkleyy commented Jun 14, 2017

Please don't forget: while it's great to be beginner-friendly (which we are; we got a book, nice examples and a Gitter channel to ask for help), the people dealing with these components and the Specs API will do that for many, many hours (like @malleusinferni). The basic example explains what is implemented and AFAIK also tells which storage is used.

Using the right storage for the right situation isn't any different: now, beginners would just copy the example code, then they'd copy #[derive(Component)] #[component(VecStorage)]. I don't think that it would be bad for beginners.

@ebkalderon
Copy link
Member Author

Just FYI, for all participants, specs_derive doesn't need to be published to crates.io given that it's a hard dependency and the macro is re-exported from within the specs crate.

Also, if you haven't noticed, the crate itself is pretty darn small (just one source file, with only two functions inside). Nearly all of the changes included in this PR are updating docs, examples, doc comments, the relevant book chapters, and the README. Both the book and the basic example clearly explain what the desugared API boils down to, and I've done my best to document all that I could.

Personally, I think that the reduced user-side boilerplate and the lessening of possible copy-paste errors is worth the effort, but those are just my two cents.

@Aceeri
Copy link
Member

Aceeri commented Jun 14, 2017

Im impartial about this, but I have one concern: Does this support wrapper storages like FlaggedStorage?

@torkleyy
Copy link
Member

@Aceeri Maybe we could use DenseVecStorage as a default there?

@WaDelma
Copy link
Member

WaDelma commented Jun 14, 2017

Doesn't crates.io demand that all dependencies are in crates.io?

I would like to have derive as an option as it basically doesn't remove any relevant information and just reduces boilerplate.

It's shame that it cannot have syntax like this though: #[derive(Component(VecStorage))].

Copy link
Member

@WaDelma WaDelma left a comment

Choose a reason for hiding this comment

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

Also with custom derives, compiletest would be useful :P

let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

let storage = ast.attrs.first()
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this fail if there is more that one attribute and the component attribute isn't first?

Copy link
Member Author

Choose a reason for hiding this comment

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

Whoops, you're right! I forgot to change that while I was refactoring.

@kvark
Copy link
Member

kvark commented Jun 14, 2017

So, let me recap:
Positive: @ebkalderon, @WaDelma (?)
Negative: @kvark
Balanced (needs partial code revert): @csherratt , @torkleyy
Abstained: @Aceeri

Edit: I'm fine with a separate crate being published that provides the derives. I don't find it particularly useful, given that a user can just throw a 4 line macro to do the same thing in an even shorter form, but I'm fine with it if it's not propagating to specs and just builds on top of it.

@torkleyy
Copy link
Member

Proposing compromise:

  • Merge this crate and leave it in-tree
  • Point to it in README, full example and book; code examples should implement it explicitly, only the book contains a single and short example of how to use #[derive(Component)]

@torkleyy
Copy link
Member

Okay, I think we found a consensus here (I proposed, @kvark upvoted and @csherratt suggested pretty much the same earlier). @ebkalderon Would it be okay for you to move on like this?

@ebkalderon
Copy link
Member Author

ebkalderon commented Jun 14, 2017

Yes! That sounds reasonable enough.

EDIT: Unfortunately, I don't have time to work on it tonight, but expect the PR to be revised sometime tomorrow when I'm off work.

@ebkalderon
Copy link
Member Author

Okay, I've finally had some spare time to rebase and update this PR to address the concerns raised. Please feel free to review!

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

Nice to see progress here!

Could we use specs-derive instead of specs_derive like I did it with shred and shred-derive?

use specs::{Component, DenseVecStorage, DispatcherBuilder, Entities, Entity, Fetch,
HashMapStorage, Join, NullStorage, ReadStorage, RunningTime, System, VecStorage,
World, WriteStorage};
use specs::{DenseVecStorage, DispatcherBuilder, Entities, Entity, Fetch, HashMapStorage, Join,
Copy link
Member

Choose a reason for hiding this comment

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

We still need that export.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, thank you.

benches/world.rs Outdated
@@ -3,11 +3,12 @@
extern crate cgmath;
extern crate rand;
extern crate rayon;
#[macro_use]
Copy link
Member

Choose a reason for hiding this comment

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

Not necessary anymore

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I missed that.

benches/world.rs Outdated
extern crate specs;

extern crate test;

use specs::{Component, HashMapStorage, Join, ParJoin, VecStorage, World};
use specs::{HashMapStorage, Join, ParJoin, VecStorage, World};
Copy link
Member

Choose a reason for hiding this comment

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

Missing import

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, forgot to add it back in after rebasing.

@@ -20,7 +21,7 @@ extern crate specs;
Let's start by creating some data:

```rust,ignore
use specs::{Component, VecStorage};
use specs::VecStorage;
Copy link
Member

Choose a reason for hiding this comment

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

Missing import

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you, I'll fix that.

(see [the storages chapter][sc] for more details), so we can associate some data
with an entity. Before doing that, we need to create a world, because this is
where the storage for all the components is located.
These will be our two component types. Optionally, `specs` includes a convenient
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure. I think we agreed we wouldn't want #[derive] shown in the book - except in one dedicated chapter.

Copy link
Member Author

@ebkalderon ebkalderon Jul 21, 2017

Choose a reason for hiding this comment

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

Do we need a dedicated chapter for this macro, though? It is mentioned here once as an optional thing, and then never used again in the book. Still, I can create a new chapter if you prefer.

Copy link
Member

Choose a reason for hiding this comment

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

No, we don't need to. If somebody wants it, it's not hard to do so.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good.

@@ -137,6 +157,7 @@ hello_world.run_now(&world.res);
Here the complete example of this chapter:

```rust,ignore
<<<<<<< HEAD
Copy link
Member

Choose a reason for hiding this comment

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

I think this was left when rebasing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Whoops, you're right. I'll go remove that.

@@ -102,6 +102,7 @@ dispatcher.dispatch(&mut world.res);
Here the code for this chapter:

```rust,ignore
<<<<<<< HEAD
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I messed up on that one.

src/lib.rs Outdated
@@ -167,17 +177,22 @@
//!
//!

#[allow(unused_imports)]
#[macro_use]
extern crate specs_derive;
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 think we need to include it here.

Copy link
Member Author

@ebkalderon ebkalderon Jul 21, 2017

Choose a reason for hiding this comment

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

I was hoping to allow specs_derive to re-export the procedural macro to the user, meaning they don't need to add extern crate specs_derive; to their project. If you prefer that users add it explicitly as a separate crate to their projects, I can remove it.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure. cc @kvark

Copy link
Member

Choose a reason for hiding this comment

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

Just discussed with @kvark, we decided not to reexport it here so it doesn't get included if it's unnecessary for the user.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, I'll take it out.

src/lib.rs Outdated
pub use storage::{BTreeStorage, CheckStorage, DenseVecStorage, DistinctStorage, FlaggedStorage,
HashMapStorage, InsertResult, NullStorage, ReadStorage, Storage,
UnprotectedStorage, VecStorage, WriteStorage};
pub use world::{Component, CreateIter, CreateIterAtomic, EntitiesRes, Entity, EntityBuilder,
Generation, LazyUpdate, World};

#[cfg(not(target_os = "emscripten"))]
pub use shred::{AsyncDispatcher};
Copy link
Member

Choose a reason for hiding this comment

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

Why move the re-export?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I must have made a mistake while copy-pasting sections around during the rebase.

@@ -12,6 +12,7 @@ specs = "0.9"
And add this to your crate root (`main.rs` or `lib.rs`):

```rust,ignore
#[macro_use]
Copy link
Member

Choose a reason for hiding this comment

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

No need for #[macro_use].

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, removed.

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

Great PR, thank you so much!

@torkleyy
Copy link
Member

Hold on, please add an entry to the changelog.

src/lib.rs Outdated
@@ -191,6 +202,7 @@ pub use shred::{Dispatcher, DispatcherBuilder, Fetch, FetchId, FetchIdMut,
#[cfg(not(target_os = "emscripten"))]
pub use shred::{AsyncDispatcher};

pub use specs_derive::*;
Copy link
Member

Choose a reason for hiding this comment

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

This line causes the build to fail.

@ebkalderon
Copy link
Member Author

@torkleyy Done! It should build correctly now, and I've updated the changelog.

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

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

Perfect, thank you so much!

@torkleyy
Copy link
Member

bors r+

bors bot added a commit that referenced this pull request Jul 22, 2017
192: Custom derive for Component trait r=torkleyy

This PR introduces an optional `specs_derive` crate containing a custom derive macro for defining new components in a less verbose way. The ergonomics loosely resemble the [`derivative`](https://github.com/mcarton/rust-derivative) crate.

### Before

```rust
#[derive(Debug)]
struct Pos(f32, f32, f32);

impl Component for Pos {
    type Storage = VecStorage<Pos>;
}
```

### After

```rust
#[derive(Component, Debug)]
struct Pos(f32, f32, f32);
```

The macro will store components in `VecStorage`s by default. To specify a different storage type, you may use the `#[component]` attribute.

```rust
#[derive(Component, Debug)]
#[component(HashMapStorage)]
struct Pos(f32, f32, f32);
```

I also included a revised `basic.rs` example called `basic_derive.rs` to demonstrate its usage, but this can probably be omitted from the PR if people ultimately prefer implementing `Component` the explicit way.

**EDIT**: Use `DenseVecStorage` by default.
@bors
Copy link
Contributor

bors bot commented Jul 22, 2017

Build succeeded

@bors bors bot merged commit 086f698 into amethyst:master Jul 22, 2017
xMAC94x pushed a commit to xMAC94x/specs that referenced this pull request Mar 10, 2021
193: Maintenance/192/ongoing code maintenance r=azriel91 a=azriel91

Closes amethyst#192.

Co-authored-by: Azriel Hoh <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants