-
Notifications
You must be signed in to change notification settings - Fork 1
derive Joined
to implement JoinedValue
and BufferMapLayout
#53
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
Conversation
Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
The latest version of #52 refactors the way
The trait implementation examples for these changes can be found here. You should find that it's overall much simpler than the previous implementation. I'd like to request a few additional features for the
|
…_joined Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
… buffer struct Signed-off-by: Teo Koon Peng <[email protected]>
…fer types instead Signed-off-by: Teo Koon Peng <[email protected]>
Signed-off-by: Teo Koon Peng <[email protected]>
In this implementation, instead of allowing to customize the buffer type, there is a
e.g. #[derive(Clone, JoinedValue)]
struct TestJoinedValue<T: Send + Sync + 'static + Clone> {
integer: i64,
float: f64,
string: String,
#[bevy_impulse(buffer_type = AnyBuffer)]
generic: T,
} this will generate #[derive(Clone)]
#[allow(non_camel_case_types)]
struct __bevy_impulse_TestJoinedValue_Buffers<T: Send + Sync + 'static + Clone> {
integer: ::bevy_impulse::Buffer<i64>,
float: ::bevy_impulse::Buffer<f64>,
string: ::bevy_impulse::Buffer<String>,
generic: AnyBuffer,
}
fn buffer_list(&self) -> ::smallvec::SmallVec<[AnyBuffer; 8]> {
use smallvec::smallvec;
smallvec![
self.integer.as_any_buffer(),
self.float.as_any_buffer(),
self.string.as_any_buffer(),
self.generic.as_any_buffer(),
]
} An easy fix is to impl
impl<T: Send + Sync + 'static + Clone> ::bevy_impulse::Joined
for __bevy_impulse_TestJoinedValue_Buffers<T>
{
type Item = TestJoinedValue<T>;
fn pull(
&self,
session: ::bevy_ecs::prelude::Entity,
world: &mut ::bevy_ecs::prelude::World,
) -> Result<Self::Item, ::bevy_impulse::OperationError> {
let integer = self.integer.pull(session, world)?;
let float = self.float.pull(session, world)?;
let string = self.string.pull(session, world)?;
let generic = self.generic.pull(session, world)?;
Ok(Self::Item {
integer,
float,
string,
generic,
})
}
} This fails as
// assume `generic_buffer` is a `AnyBuffer`
let user_friendly = __bevy_impulse_TestJoinedValue_Buffer {
...
generic: generic_buffer,
}
// assume `generic_output` is a `Output<T>`
let not_friendly = __bevy_impulse_TestJoinedValue_Buffer {
...
generic: generic_output.into_buffer().as_any_buffer(),
}
... Because the buffer type is fixed, a conversion needs to be done to use other types of buffers.
|
Signed-off-by: Teo Koon Peng <[email protected]>
Tried to implement customizing the generated struct visibility but it seems that it is not possible to change expose pub idents in a private span and vice versa (E0446). In derive macros, my guess is that it inherits the visibility of the target struct, attempting to generate structs with different visibility than the original will result in E0446. |
Can you show how your approach would work for the
I think this should be easy to fix by adding a field along the lines of
There's no reason we can't implement
This is exactly why I believe we need to allow the user to specify the buffer type for special cases. The original use case you showed of #[bevy_impulse(buffer_type = "AnyBuffer")]
generic: T, would be invalid and should produce a compilation error. The use case we actually want it for would be this: #[bevy_impulse(buffer_type = "AnyBuffer")]
any_message: AnyMessageBox, In this example, the
No, this is not the case. |
macros/src/buffer.rs
Outdated
|
||
impl #impl_generics #struct_ident #ty_generics #where_clause { | ||
fn select_buffers( | ||
builder: &mut ::bevy_impulse::Builder, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not have builder
as an argument. I think we should leave it to users to call output.into_buffer(builder)
from outside of select_buffers
and have select_buffers
just take in the specific buffer types that are expected. This will especially matter for supporting specialized buffer types like AnyBuffer
and JsonBuffer
.
…s with json buffers Signed-off-by: Teo Koon Peng <[email protected]>
I thought the idea is to auto downcast
I didn't think of this, yeah it seems to be a good catch-all solution. |
It's actually the opposite. The idea is if a user wants to obtain an Like you said, trying to pull a concrete message from an
I expect this won't be a very common use case, and only advanced users would even be interested in obtaining an I do think the |
…to AnyBuffer limitations Signed-off-by: Teo Koon Peng <[email protected]>
src/buffer/buffer_map.rs
Outdated
let buffer_generic = | ||
JsonBuffer::from(builder.create_buffer::<String>(BufferSettings::default())); | ||
let buffer_any = | ||
JsonBuffer::from(builder.create_buffer::<()>(BufferSettings::default())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may want to move this test into the json_buffer.rs
module since JsonBuffer
requires the diagram
feature, but buffer_map.rs
doesn't.
There is another problem with different buffer types. The 2 ways Another problem is that |
src/buffer/buffer_map.rs
Outdated
struct TestJoinedValue<T: Send + Sync + 'static + Clone> { | ||
integer: i64, | ||
float: f64, | ||
string: String, | ||
generic: T, | ||
#[buffers(buffer_type = AnyBuffer)] | ||
#[allow(unused)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make sense to pass a real value through this AnyBuffer
and test that it comes out correctly on the other side instead of leaving it unused.
I don't follow what the issue is here. There's a reference implementation of this for a struct that contains a I understand that procedural macros are easiest to write when everything follows a very consistent pattern, but I believe it should still be possible to generate a |
This is to be expected. We can't assume that all message types implement clone, and the uncloneability of some message types is accounted for throughout this library. I think we can save it for a future PR to support a |
Signed-off-by: Teo Koon Peng <[email protected]>
37ee9b9 not sure if this is the right place to add it. But this does fix |
Signed-off-by: Teo Koon Peng <[email protected]>
Did you try doing it? If so can you show me what you tried and what went wrong with it? I believe it will be able to work just like how The impl will exist within the same module as the struct that the derive is associated with, so whatever tokens exist in that scope should be usable by the macro. If a user does: use bevy_impulse::JsonBuffer;
#[derive(Clone, JoinedValue)]
struct TestJoinedValueJson {
integer: i64,
float: f64,
#[bevy_impulse(buffer = "JsonBuffer")]
json: JsonMessage,
} then the derive macro should be able to expand that into #[derive(Clone)]
struct TestJoinedValueJsonBuffers {
integer: Buffer<i64>,
float: Buffer<f64>,
json: JsonBuffer,
}
impl ::bevy_impulse::BufferMapLayout for TestJoinedValueJsonBuffers {
fn try_from_buffer_map(buffers: &::bevy_impulse::BufferMap) -> Result<Self, ::bevy_impulse::IncompatibleLayout> {
let mut compatibility = ::bevy_impulse::IncompatibleLayout::default();
let integer = compatibility.require_buffer_type::<::bevy_impulse::Buffer<i64>>("integer", buffers);
let float = compatibility.require_buffer_type::<::bevy_impulse::Buffer<f64>>("float", buffers);
let json = compatibility.require_buffer_type::<JsonBuffer>("json", buffers);
let Ok(integer) = integer else {
return Err(compatibility);
};
let Ok(float) = float else {
return Err(compatibility);
};
let Ok(json) = json else {
return Err(compatibility);
};
Ok(Self {
integer,
float,
json,
})
}
} In fact the same logic works for all the types of the fields within the struct. The macro doesn't know the full path for any of those types but can still use them in the implementation. |
I hadn't thought of doing that, but it makes enough sense in order to fully generalize the |
I don't agree with this statement and in fact we should support downstream specialized buffers. Everything else in the way that I've implemented |
Something else that occurred to me, if we rule out the case of #[bevy_impulse(buffer = "AnyBuffer")]
field: T, then we no longer need to worry about introducing an _ignore: ::std::marker::PhantomData<fn( #generics )>, field to the Simply put, I think we can totally forget about the need for |
What I mean is that if we do special case for |
Yeah, in fact the latest code already does exactly that, though I left the code there because this logic of converting from generic tokens to a TypePath of PhantomData could be useful in the future? bevy_impulse/macros/src/buffer.rs Lines 63 to 77 in a00f09f
|
Some would say it's bad practice to leave dead code hanging around, but I don't mind letting it linger for a while just in case. I would just recommend making it |
I don't agree with this statement and I'll need you to concretely demonstrate a problem with the approach before I can believe it. You should be able to take the tokens Please give that a try and then show me how it's unable to work. Users will be able to use this exactly as-is with downstream / third-party specialized buffers types. |
What you're describing here doesn't fit the use case of the buffer specialization, as far as I can figure. Can you demonstrate how your approach would work for this test case? |
…ow impl some traits like Copy, Send, Sync Signed-off-by: Teo Koon Peng <[email protected]>
Actually I just did a review of the current code and it looks like you're already doing exactly what I've been asking for, so I'm now even more confused about why you've been saying it can't work. |
After looking back over our conversation, I think I may have misunderstood what you think of as a "special case". In my mind the "special case" is that we have to apply the I guess what you were actually saying is that we shouldn't have special treatment for internal specialized buffer types, and that's something I fully agree with. I never wanted us to treat I'll do a full review of the PR now that it looks like everything is being supported the way that I had been hoping for. |
type FooBuffer = Buffer<Foo>;
#[derive(JoinedValue)]
struct TestWithCustomBuffer {
normal_data: String,
#[buffers(buffer_type = JsonBuffer)]
json_data: JsonMessage,
#[buffers(buffer_type = FooBuffer)]
foo_data: Foo,
}
// somewhere in generated code
fn try_from_buffer_map(buffers: &::bevy_impulse::BufferMap) -> Result<Self, ::bevy_impulse::IncompatibleLayout> {
// there is no customization of the buffer type, so the buffer type is `Buffer<String>`, `IncompatibleLayout::require_buffer_type`
// cannot downcast to `Buffer<T>`, so we need to use `IncompatibleLayout::require_message_type`.
let normal_data = if let Ok(buffer) = compatibility.require_message_type::<String>("normal_data", buffers) {
buffer
} else {
return Err(compatibility);
};
// A custom buffer type is given, it is `JsonBuffer` so we assume that we can use `IncompatibleLayout::require_buffer_type`.
let json_data = if let Ok(buffer) = compatibility.require_buffer_type::<JsonBuffer>("json_data", buffers) {
buffer
} else {
return Err(compatibility);
};
// A custom buffer type is given, it is `FooBuffer`, we don't know what `FooBuffer`, but
// `IncompatibleLayout::require_buffer_type` can convert between "any" special buffers, so we should be able to use it.
// However, this fails because `FooBuffer` is actually a `Buffer<Foo>` and we need to use `IncompatibleLayout::require_message_type`.
let foo_data = if let Ok(buffer) = compatibility.require_buffer_type::<FooBuffer>("foo_data", buffers) {
buffer
} else {
return Err(compatibility);
};
} The main problem is that In order to support any buffers, I think we need to avoid generating code based on the buffer type, we must consider any |
Yeah, maybe I should have made it more clear. I think that if we treat |
I'm glad this has come out so nicely. It works exactly the way I was hoping it would. The My only points of feedback are on some of the attribute names. I realize that attribute naming doesn't have any rigorous objective standards so it's going to be a very subjective topic, but I'd suggest a few tweaks to the current naming:
|
Signed-off-by: Teo Koon Peng <[email protected]>
I think most of these makes sense, only thing is that I think we shouldn't use "strings" in the attributes, e.g. |
…_joined Signed-off-by: Teo Koon Peng <[email protected]>
I agree that what you're suggesting looks nicer and probably allows code completion to work better, but I think the #[joined(buffer = CustomBuffer<A, B>)] When the parser splits the attributes based on the comma, I think it'll get #[joined(buffer = "CustomBuffer<A, B>")] will definitely get parsed correctly. |
I've tried this out now, and there's actually no parsing issue at all. It seems I'll push a change that adds a unit test for a buffer with multiple generic arguments. |
Signed-off-by: Michael X. Grey <[email protected]>
Signed-off-by: Michael X. Grey <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In b110e87 I added a test to make sure that everything works for buffer types with multiple generic arguments, and in fact it does.
At the same time I fixed how Clone
is implemented for the generated buffer structs because the existing implementation would not have worked if any generic argument did not have the Clone
trait, even though buffers can be cloned regardless of any generic arguments.
Also all of our internal buffer types implement Copy
so I think we should implement Copy
for the generated buffer structs by default. I added a noncopy_buffer
attribute e.g. #[joined(buffer = CustomBufferType, noncopy_buffer)]
, which will disable generating the Copy trait whenever it's applied to at least one field in the struct.
Thanks for the improvements! |
Status:
Updated to the new
join
api. Added derive helper attributes, but the only config now is to change the generated buffer struct name.select_buffers
there should no longer be a need to change the buffer type. Allowing to customize the types also lead to other issues as explained in deriveJoined
to implementJoinedValue
andBufferMapLayout
#53 (comment).For now only struct with named fields is supported, tuple struct or newtype struct doesn't make sense as a buffer map layout. enums are questionable but they will require more logic to validate and select which branch to use.