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

fix: Cecil serialization inheritance cutoff with complex base types #2457

Merged
merged 2 commits into from
Sep 22, 2024

Conversation

Eideren
Copy link
Collaborator

@Eideren Eideren commented Sep 19, 2024

PR Details

The binary serializer can lose serialized data when round-tripped with a specific inheritance setup;

[DataContract]
public class D : C<AnyType>{ public bool bill; }

public class C<T> : B<T>{}
public class B<T> : A{}

[DataContract(Inherited = true)]
public class A : EntityComponent{}

The following logic fails to capture B<T>

SerializableTypeInfo parentSerializableTypeInfo;
var parentType = ResolveGenericsVisitor.Process(type, type.Resolve().BaseType);
if (!parentType.ContainsGenericParameter() && (parentSerializableTypeInfo = GenerateSerializer(parentType, false, profile)) != null &&
parentSerializableTypeInfo.SerializerType != null)
{
serializableTypeInfo.ComplexSerializerProcessParentType = true;
}

Which in turn means that all serialized data contained in its base types would be discarded, most obvious with the component Id inherited from EntityComponent, which right now shows up as a new Id on every save caused by the asset collision Id de-duplication using binary serialization through the Clone function:
public static void Clean(Package package, ICollection<AssetItem> inputItems, ICollection<AssetItem> outputItems, AssetResolver assetResolver, bool cloneInput, bool removeUnloadableObjects)
{
if (inputItems == null) throw new ArgumentNullException(nameof(inputItems));
if (outputItems == null) throw new ArgumentNullException(nameof(outputItems));
if (assetResolver == null) throw new ArgumentNullException(nameof(assetResolver));
// Check that all items are non-null
if (inputItems.Any(item => item == null))
{
throw new ArgumentException("List cannot contain null items", nameof(inputItems));
}
var items = inputItems;
if (cloneInput)
{
items = inputItems.Select(item => item.Clone(flags: removeUnloadableObjects ? AssetClonerFlags.RemoveUnloadableObjects : AssetClonerFlags.None)).ToList();

Related Issue

Kind of hard to tell

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

Copy link
Member

@manio143 manio143 left a comment

Choose a reason for hiding this comment

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

Nice solution. Just a question - the check on generic type inside the for loop where we continue - what scenario is that skipping?

@Kryptos-FR
Copy link
Member

Nice solution. Just a question - the check on generic type inside the for loop where we continue - what scenario is that skipping?

Would be nice to add a few comments on those lines that explain what the algorithm is doing.

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 21, 2024

Added a couple of comments to clarify.
@manio143 from what I can tell, this is a fallback for when ResolveGenericsVisitor.Process failed to resolve, which afaict happens when trying to serialize B as an open type with an inheritance like the following:

class A<T>{}
class B<T> : A<T>{}

Not 100% sure as I haven't tested it, here's the source for that method:

public static TypeReference Process(TypeReference context, TypeReference type)
{
if (type == null)
return null;
var parentContext = context;
GenericInstanceType genericInstanceTypeContext = null;
while (parentContext != null)
{
genericInstanceTypeContext = parentContext as GenericInstanceType;
if (genericInstanceTypeContext != null)
break;
parentContext = parentContext.Resolve().BaseType;
}
if (genericInstanceTypeContext == null || genericInstanceTypeContext.ContainsGenericParameter())
return type;

@xen2
Copy link
Member

xen2 commented Sep 21, 2024

Would it be easy to add a test (in https://github.com/stride3d/stride/blob/master/sources/core/Stride.Core.Tests/TestSerialization.cs) so that we make sure to not break it in the future? (i.e. if we switch to Roslyn)
If not trivial, no problem.

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 21, 2024

Looked into it - in that particular test context, cecil fails to generate a serializer for the class even with the exact same type definitions I used at edit and runtime to validate the changes in this PR. I don't have a lot of time to dig too deep into why the test fails while the real one passes, so I'll have to leave it as is for now

@xen2
Copy link
Member

xen2 commented Sep 22, 2024

Thanks for giving it a try, all good then!

@Eideren Eideren merged commit b6f16b8 into stride3d:master Sep 22, 2024
2 checks passed
@Eideren Eideren deleted the cecil_basetypes_fix branch September 22, 2024 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants