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!