-
Notifications
You must be signed in to change notification settings - Fork 210
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
serde support for core types & VariantDispatch #743
Conversation
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.
Wonderful. Thanks for the PR! Before you go ahead and get to the TODOs, some design concerns to consider:
Maybe a bit general, and toasteater already mentioned it to some extent: What is the use case, or rather the exact problem we're trying to solve? Games usually have a specialized serialization mechanism (for the game logic data), so it might need some thinking which parts of godot-rust could fall under that. |
The biggest benefit I see from including this in the library is deriving |
After giving it a bit more thought I think I agree with Bromeon that implementing serde traits for even |
I was thinking it might be nice to be able to tell the difference between a serialized EDIT: I think for [de]serialization purposes, using |
It's working now. The
This RON test string has been deserializing fine for me: { Dictionary: {
"pos": { Vector2: (x: 64.0, y: 42.0) },
87: false,
{ Rid: () }: 847,
{ Object: None }: { Color: (r: 0.5, g: 0.0, b: 1.0, a: 1.0) },
[
2349,
019.0018,
true,
{ Rect2: (
position: (x: 0, y: 8),
size: (y: 9, x: 99),
)},
(),
]: 256
}} GDScript prints it as:
and I checked the |
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.
The design looks good to me now. External tagging isn't friendly to a lot of formats, but some way to discern variants has to be here, and the alternatives aren't very good either. There's one argument for using internal tagging for a number of types, though, in that objects like the following one:
{
"type": "Vector2",
"x": 1.0,
"y": 2.0
}
...would be possible to deserialize either as a Variant
or directly as a Vector2
(where the type
field will just get ignored).
For consistency, adjacent tagging can be considered for collections, although for them there's no real benefit over external tagging, beside looking slightly more natural in JSON:
{
"type": "Dictionary",
"value": { "foo": "bar }
}
I have no strong feelings either way, though. I'm fine with the design as-is.
let mut ser = ser.serialize_map(Some(1))?; | ||
ser.serialize_entry(&VariantType::Vector2, &v)?; | ||
ser.end() |
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.
Consider using serialize_newtype_variant
which is specifically for externally-tagged enums. This would perform better with non-self-describing (binary) formats (e.g. Bincode) since they won't have to serialize the keys as strings.
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 don't think non-self-describing formats can directly deserialize Variant
due to the use of deserialize_any()
anyway... But they should be able to [de]serialize them as VariantDispatch
, and as I said in the comment below, my most recent changes should allow most self-describing formats to deserialize a VariantDispatch
directly as a Variant
anyway.
Oh, and I also made it so [de]serializing VariantDispatch::Dictionary
and VariantDispatch::VariantArray
will keep all of the elements as VariantDispatch
to ensure that non-self-describing formats can still use them.
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 see. Perhaps this is worth noting in the type-level docs for Variant
?
I realized it was actually possible to use an untagged representation for |
af4e0d7
to
15757f3
Compare
Ron is also failing to deserialize a Also want to add some tests for deserializing sequences as structs, e.g. One big question I have left is if there are any well-known formats that are self-describing, but store field names as integers. Maybe Cbor? Theoretically they should still work with |
Isn't this ambiguous? The same sequence can also be an array.
The MsgPack implementation does that by default. Also moving the discussion about
I forgot whether there was something I was specifically aiming for when I wrote that. Might just be an oversight. |
Yes, and and it's worth discussing the pros & cons of this approach. It was easy to implement, and will be easy to remove if we decide against it. I personally like the ability to do this with hand-written Ron, and I feel like it's what the Godot developers would have done if they were implementing this (I think it's actually how the JSON singleton works as well). But it could also be surprising, and result in failed calls to My initial feeling is that using Variants is already accepting some amount of magic, and if you desire more explicitness there's still
I've been trying to make this implementation robust to any potential change in the type of |
For context, if it helps inform any decisions made on this: My use case is having some data that I mostly want to use from Rust, but also sometimes access it from GDScript and the editor (possibly even editing it in the editor, if I make a custom Also, I think I want to be able to have some "common" data between the client & server, while also letting each have their own specific data, and have different modules access subsets of the data or unions of separate pieces of data. This is where a dynamic type like Variant can come in handy. |
So Overall it seems like it'll be impossible to come up with a single solution that will be perfect for all, or even most, formats. Perhaps more dedicated structs would be necessary with specific |
Just tried.
I think it's fine to match how the Another option is to leave the
My sentiment is that JSON is a must. We can just let the serializer error out on non-string keys, though. Types containing floating point numbers aren't useful as keys in general. Struct keys aren't supported in most common formats, anyway. I don't see how extra effort is required here? |
Ah, right, but then
This seems logical, but also, the I think the bigger argument for deserializing sequences as structs may be that it already works to deserialize sequences as concrete structs. The derived impls provide an implementation of
The problem is that We could just serialize all The other option I see is to use an adjacently tagged representation for |
It's really annoying that types need to decide how they should be serialized, when the formats are what have the idiosyncrasies that make them incompatible with each other... But I guess that's what newtypes are for. |
Beats me.
Have you considered the opposite situation? What about serializing a struct that contains a
I see. With so many design issues surrounding the untagged |
Yes, I guess my point was that the cases where that could go wrong would be limited to sequences of exactly 2, 3, or 4 numbers. You did express some interest in the possibility of deserializing the same data as either a
I agree, although I'm actually kind of surprised at how well the untagged I was surprised, however, that Nevertheless, if there's not one clear "best" default way to handle |
I was making the Glad I followed through with it and tested it on |
I doubt this is perfectly ready to merge, but I think it's close enough that I'm ready for more feedback. |
bors try |
tryBuild failed: |
I think the other reason I thought of using |
I will remove |
Sure, but that assumes the number is needed at all. As mentioned, Btw, previous CI run was aborted by me. I just merged the new CI workflow! 🚀 bors try |
tryBuild succeeded: |
Passed with the new CI 🎉 You don't have to rebase this PR, bors does it automatically. |
Yeah, it's definitely hard to predict, especially considering there is serialization support built into Godot, it's hard to know for what all use cases that may be insufficient. Perhaps the most generally useful benefit of this feature is just enabling For me, RON support was a big motivator. I have moved the |
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.
Apart from a few minor questions, it should be good to merge from my side. It also looks like the tests are run as expected.
@@ -2,6 +2,7 @@ use crate::sys; | |||
|
|||
/// Error codes used in various Godot APIs. | |||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | |||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
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.
This should simply serialize as the enumerator's name, right?
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.
It actually generates code for vist_str
, visit_bytes
, and visit_u64
and lets the Serializer
/Deserializer
implementations choose how to represent it via serialize_unit_variant
/deserialize_identifier
. The u64
representation is generated sequentially in source code order, ignoring any custom discriminants. So binary formats don't have to store unnecessary strings, but it generates more code than strictly necessary, and the serialized values don't line up with their values in code (which is of course only really relevant if you're trying to debug a binary format).
https://gist.github.com/Waridley/5fc1faa65260aef4b55a95a864ec767d
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.
Thank you for the elaboration! 👍🏼
Ugh, that expanded code reminds me why we have macros, but also why their compilation takes so long 😬
bors try |
tryBuild succeeded: |
Great! Anything left on your side, or is it ready to merge? |
@Bromeon Nothing I can think of. Go ahead! |
bors r+ |
Build succeeded: |
@Waridley Thank you very much for your contribution! 👍🏼 |
I got curious about how hard it would be to add serde support, and ended up basically fully implementing it so I figured I should start a PR 😂
There are no tests yet, and honestly I haven't really even tested it in my own project yet. I'll be doing so over the next few days probably, but for now I'll open this as a draft to allow for comments, feedback, and opinions.
The implementations for
Object
types is a bit hacky, but I really like the idea of being able to replace Godot's resource files with something more Rust-y. Since I had to wrap the references in newtypes, I suppose they could be part of a separate crate anyway, unless anyone has a better idea of how to get around the orphan rules. Even if we can implement serde's traits directly ongdnative_core
types, there should probably be some discussion about whether it's actually desirable to skip properties that are set to their defaults (this would end up serializing a LOT of properties otherwise).Would close #9