applejag/Newtonsoft.Json-for-Unity

Bug: [IL2CPP] ExecutionEngineException on deserializing array of struct

SergeOxe opened this issue ยท 4 comments

Expected behavior

Successfully deserializing json with array of nested struct.
{"classProp":"this is classProp Value", "myArray":[{"someProp":"someProp Value"}]}

Actual behavior

Getting ExecutionEngineException.

ExecutionEngineException: Attempting to call method 'System.Collections.Generic.List`1[[MyClass+NestedStruct, Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]::.cctor' for which no ahead of time (AOT) code was generated.

Steps to reproduce

  • New project
  • Import jillejr.newtonsoft.json-for-unity via UPM
  • Add following script to scene:
public class MyClass
{
    public struct NestedStruct
    {
        public string someProp;
    }
        
    public string classProp;
    public NestedStruct[] myArray;
}
public class Main : MonoBehaviour
{
    private string JsonToParse = "{\"classProp\":\"this is classProp Value\"," +
                                 "\"myArray\":[{\"someProp\":\"someProp Value\"}]}";
    void Start()
    {
        var d = JsonConvert.DeserializeObject<MyClass>(JsonToParse);
        Debug.Log(d.classProp);
        Debug.Log(d.myArray.Length);
    }
}
  • Run the game

Details

Please Note that if I changing the struct to class, it will work as expected.

Host machine OS running Unity Editor ๐Ÿ‘‰ MacOS Catalina

Unity build target ๐Ÿ‘‰ Android

Newtonsoft.Json-for-Unity package version ๐Ÿ‘‰ 12.0.301

I was using Unity version ๐Ÿ‘‰ 2018.4.26

Checklist

  • Shutdown Unity, deleted the /Library folder, opened project again in Unity, and problem still remains.
  • Checked to be using latest version of the package.

Hi @SergeOxe! Thanks for making such a clear issue! This is actually a known issue with the package, and I apologize for not being clear about it back.

The issue here is about some AOT generation. Funny, System.Collections.Generic.List<T> is one of the more difficult types to get an AOT error about :)

I have written some wiki pages, a bit hidden, over 'ere: https://github.com/jilleJr/Newtonsoft.Json-for-Unity/wiki/Fix-AOT-compilation-errors, and in this case, I would suggest looking into using AotHelper. Especially the AotHelper.EnsureList<T>

To fix this, you need to add AotHelper.EnsureList<NestedStruct>() to a file that won't be stripped (such as a class that inherits from MonoBehaviour, a type with the [Preserve] attribute, or a type that's preserved via a link.xml file). The code does not need to run, nor does it need to be added to a GameObject in a scene; it only need to be included in the build. Example:

using UnityEngine;
using Newtonsoft.Json.Utility;

public class AotEnforcements : MonoBehaviour
{
    public void Awake()
    {
        AotHelper.EnsureList<NestedStruct>();
    }
}

Hope that resolves it for you! :)

For reference, the reason it works when it's a class and not when it's a struct is because the IL2CPP compiler does some generics reuse in its code generation.

The Unity engine already has some references to some List<T> types here and there, and the IL2CPP compiler can take the code it generates for example for a List<GameObject> and reuse the exact same implementation for List<NestedClass>. This is because both GameObject and NestedClass in this example are reference types. I.e. they are just pointers. It doesn't matter if one's a pointer to a GameObject and one's to a NestedClass, they both are just the same size of a .NET pointer.

In the background, a List<T> stores its items in an array of T[]. To restate: GameObject[] and NestedClass[] arrays both look the exact same in memory, and IL2CPP can abuse this. Example memory of such array:

0: int32, array length (4 in this case)
1: int64, pointer to GameObject at index 0
2: int64, pointer to GameObject at index 1
3: int64, pointer to GameObject at index 2
4: int64, pointer to GameObject at index 3
0: int32, array length (4 in this case)
1: int64, pointer to NestedClass at index 0
2: int64, pointer to NestedClass at index 1
3: int64, pointer to NestedClass at index 2
4: int64, pointer to NestedClass at index 3

Structs have this particular property of being embedded wherever they're used. For example, if we had the following struct:

struct Vector2 {
    public int x;
    public int y;
}

Then the memory of an Vector2[] would look like so:

0: int32, array length (4 in this case)
1: int32, Vector2.x of Vector2 at index 0
2: int32, Vector2.y of Vector2 at index 0
3: int32, Vector2.x of Vector2 at index 1
4: int32, Vector2.y of Vector2 at index 1
5: int32, Vector2.x of Vector2 at index 2
6: int32, Vector2.y of Vector2 at index 2
7: int32, Vector2.x of Vector2 at index 3
8: int32, Vector2.y of Vector2 at index 3

Because structs behaves like this, IL2CPP does not even try to reuse generic types where the generic type argument is a struct.

More about this here: https://blogs.unity3d.com/2015/06/16/il2cpp-internals-generic-sharing-implementation/

@jilleJr Thanks for your quick response and detailed explanation! You are doing a great job ๐Ÿ’ช. I missed the wiki page, thanks!

@SergeOxe Heh thanks :) I got into writing mood so may have written too much detail /shrug. Tell me if AotHelper fixed the problem! I do find joy in resolving these issues