applejag/Newtonsoft.Json-for-Unity

Suggestion: Attributes?

hhhmmmmmm opened this issue · 5 comments

It'd be more in line with how JSON.NET works currently to have the option of using an attribute, in the mold of [JsonConverter] or [JsonProperty], rather calling AotHelper.EnsureType directly. Could something like this work?

[AotHelp(typeof(AotEnsure<SomeClass>))]
public class SomeClass
{
    // ensure this class can be in list, dictionary, etc
}

public class AotEnsure<T> where T : new()
{
    public AotEnsure()
    {
        AotHelper.EnsureType<T>()
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Enum, AllowMultiple = false)]
public class AotHelp : Attribute
{
    public AotHelp(Type ensureType)
    {

    }
}

Hi @hhhmmmmmm! Thanks for giving a thought on this. Your suggestion is a very novel approach but it doesn't actually ensure any generic type generation just from the typeof(AotEnsure<SomeClass>) reference as the compiler doesn't generate the constructor from just that, so the AotHelper.EnsureType<T>() call is actually never generated.

I was on my seat, hoping that I was about to learn something new that typeof() would generate the constructor, but I ran som tests on the subject with the following code: https://gist.github.com/jilleJr/73d300529a6515dbd5c50db5530fba57
and via the Test Runner in Unity in a built player with IL2CPP I got the following results:

TestEnsuredWithAttr Failed:Error
System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation.
  ----> System.TypeInitializationException : The type initializer for 'System.Collections.Generic.List<Tests.Issue63.EnsuredWithAttr>' threw an exception.
  ----> System.ExecutionEngineException : Attempting to call method 'System.Collections.Generic.List`1[[Tests.Issue63+EnsuredWithAttr, Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]::.cctor' for which no ahead of time (AOT) code was generated.

TestEnsuredWithPreservedAttr Failed:Error
System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation.
  ----> System.TypeInitializationException : The type initializer for 'System.Collections.Generic.List<Tests.Issue63.EnsuredWithPreservedAttr>' threw an exception.
  ----> System.ExecutionEngineException : Attempting to call method 'System.Collections.Generic.List`1[[Tests.Issue63+EnsuredWithPreservedAttr, Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]::.cctor' for which no ahead of time (AOT) code was generated.

I'm sad to say that your suggestion has to be discarded as it doesn't work. If you have other ideas of how this could be accomplished then please tell. I myself cannot come up with a solution of using attributes to do this job, given the way the compiler works today.


Off topic: It's a fun experiment though, and this idea finally led me to this beautiful blog post about the IL2CPP's generics sharing implementation (https://blogs.unity3d.com/2015/06/16/il2cpp-internals-generic-sharing-implementation/), which has had me troubled for so long and finally I got a better grasp about why using List<> sometimes works anyways in the AOT environment. Also it's explained in this blog post why I chose to use struct instead of class in the tests.

Drat. Thanks for running it down. I'm still hoping there's some way to trick it so a more convenient usage pattern opens up. What I had been doing is have a single spot where all the EnsureType calls live, which is non-ideal. A style or usage pattern that's more per-class feels better, which made me think, "Attributes?"

It just occurring to me now: What about using the static ctor?

    public class SomeClass
    {
        static SomeClass()
        {
            Newtonsoft.Json.Utilities.AotHelper.EnsureType<SomeClass>();
        }
    }

That's about as convenient as an Attribute, is shorter than the attribute, it's on the class, (in my usage) at the top so it's at the top of the file. But on the other hand it'll actually be executed once.

Your note here though maybe thinks it could be unreliable if this is some non-Unity type?

This script does not need to be added to a GameObject. It just needs to be compiled. Inheriting from UnityEngine.MonoBehaviour or UnityEngine.ScriptableObject will ensure to always be compiled.

The only thing that's required is for the method to be preserved. All methods and fields on types inheriting from MonoBehaviours or ScriptableObject are preserved. That's why I added that note to the wiki.

What you can do is to add a [UnityEngine.Scripting.PreserveAttribute] attribute on either the entire class, on the static constructor, or on a different method you chose to add. Could even be a private instance method, as long as it's preserved.

For example:

using Newtonsoft.Json.Utilities;
using UnityEngine.Scripting;

public class SomeClass
{
    [Preserve]
    private void DoTheAotEnsurence()
    {
        AotHelper.EnsureType<SomeClass>();
    }
}

Also, just using the AotHelper.EnsureType<T>() on a non-generic type is the equivalent to just using the [Preserve] attribute to begin with. The AotHelper comes in handy when you need to use the type inside an IList<T> or when the type itself it generic to force compilation of certain generic variants.

So if you want to use attributes then maybe you can get away with only the preserve attribute?

I want to update the wiki about this preservation instead of misguiding people to only depend on MonoBehaviours. I knew less when I wrote it.

Great! That pattern is much better than what I was doing.

Glad you found a solution! I'm closing this as I consider it resolved. If you have more thoughts don't fray to reopen the issue so we can discuss more :)