jeffcampbellmakesgames / Entitas-Redux

An entity-component framework for Unity with code generation and visual debugging
MIT License
103 stars 13 forks source link

[RESEARCH] Investigate alternative object cloning libraries for copying components, blueprints #18

Closed jeffcampbellmakesgames closed 3 years ago

jeffcampbellmakesgames commented 4 years ago

Is your feature request related to a problem? Please describe.

I'd like for copying components, blueprints to be simpler and easier for developer to solve without needing to implement ICloneable for custom reference types to determine how a deep clone should occur (or at least make that optional for potential performance improvements).

Describe the solution you'd like

There are some third-party libraries I could add as a dependency to simplify deep clones of component members, but they would likely need:

Describe alternatives you've considered I'd rather not duplicate the efforts at writing another homebrew deep-cloning solution for C# if reasonable alternative exist that are either maintained or that I could fork and maintain for Unity/C#.

Additional context ~This is something worthwhile to look into, but nothing I would hold up the blueprints feature for on a release.~ On second thought I would like to add this to blueprints on the first release if possible because it would remove many of the cloning edge cases a developer would need to take into account and simplify the implementation.

jeffcampbellmakesgames commented 4 years ago

Potential options include:

jeffcampbellmakesgames commented 3 years ago

As I've investigated this further, DeepCopy seems to me to be one of the more robust, performant options for adding this capability for component copying. There are a few challenges to overcome, particularly for il2cpp platforms.

Preventing Code Stripping This is one of the easier areas as adding the [Preserve] attribute throughout the forked library and adding a link.xml file to prevent aggressive code stripping from removing required functionality have resolved this.

Ahead-Of-Time (AOT) Support This is a challenging area; the crux of it is that for il2cpp to support generic methods it needs to know what typed variants of these there are prior to generation so that it can create the corresponding c++ code to support them. Because DeepCopy largely uses expression trees to generate the copy code and generic methods are not called directly, these variants need to be discovered and created in a way where il2cpp can discover them, even if the generated code used for il2cpp discovery is never used. An example of this might be something like this.

[Preserve]
public class AOTSupport
{
    private void Foo()
    {
        GenericMethodOne<int>();
        GenericMethodTwo<int[]>();
        GenericMethodOne<ExampleStruct>();
        GenericMethodOne<ExampleClass>();
    }
}

At the moment, my thought for how to accomplish AOT support would be to execute a pre-build processor for discovery and code-gen with the following steps.

jeffcampbellmakesgames commented 3 years ago

Unfortunately, I am going to place this on the backburner until new avenues of AOT support open up for generics or I get some fresh inspiration on how to attack this. I've gotten this fairly close to working in all circumstances, but there are a few areas I don't see a way to work around for now.

Providing AOT support for generic usage of private types To be able to call generic methods via expression trees in an il2cpp Unity app, you need to have AOT support such that Unity knows to compile that specific generic variant ahead of time. I was able to add a level of support for this using Roslyn to discover 99% of all types, including member types and generate the appropriate generic variants like so:

// Class Generic Calls
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ArrayTests.AC>();
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ConstructorsTests.C3>();
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ConstructorsTests.C4>();
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ConstructorsTests.ClonableClass>();
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ConstructorsTests.ExClass>();
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ConstructorsTests.T1>();
JCMG.DeepCopyForUnity.DeepClonerGenerator.GetClonerForValueType<JCMG.DeepCopyForUnity.PlayModeTests.ConstructorsTests.T2>();

However, one area that seems very difficult if not impossible to overcome is when a member type that might need to be copied is a nested private type like so:

public class Foo
{
    private class Bar    {    }

    private Bar _bar;
}

Generic AOT support can't be provided in this same way alone as there isn't access to that private type and thus when an expression tree is generated that would create a generic method variant to copy this field it will result in an AOT exception. This example comes from the private Dictionary<T, TV>.Entry struct.

System.ExecutionEngineException : Attempting to call method 'JCMG.DeepCopyForUnity.DeepClonerGenerator::Clone1DimArraySafeInternal<System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]>' for which no ahead of time (AOT) code was generated.

Generic Function Generation at Runtime Another issue with this particular library is that internally it makes use of cached expression trees compiled as generic methods. While issues don't seem to exist for classes, which internally all share a signature of Func<object, DeepCloneState, object> and make use of typed casting when passing in parameters, getting return values, value types like structs use specific typed variants like Func<StructOne, DeepCloneState, StructOne> which because this method is generated at runtime there isn't a way to provide AOT support for beforehand. I'd be curious to learn more about why this works for classes and not for structs; I believe it may have something to do with the class variants sharing a method signature, but I'm not sure (here for more details).

Summary Overall this is a worth goal to pursue and with additional Unity Mono/il2cpp upgrades this may become more possible to do without so much AOT support, but for now I am tabling this in pursuit of other features, improvements.

jeffcampbellmakesgames commented 3 years ago

Closing this for now until new developments arise to solve some of these issues.