Search Unity

Is it possible that IL2CPP broke `== null` but not ReferenceEquals(x, null) ?

Discussion in 'Editor & General Support' started by 5argon, Jul 4, 2019.

  1. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,266
    I notice that my game when build to real iOS device, some of my null check (instance == null or != null) suddenly stopped working. So it should not be null, but it returns null only on the real device. Is this a common occurence? This null check is used against a ScriptableObject instance loaded from Resources.

    What's really weird is that I could use its field even if null check says it is null. For example, a class A with one field `string a`. Supposed that an instance of this class is called `aInstance` :

    Unity Editor (MBP Early 2015)

    aInstance == null false
    aInstance != null true
    ReferenceEquals(aInstance, null) false
    aInstance.a this is usable and didn't throw

    iOS (iPhone SE)


    aInstance == null true
    aInstance != null false
    ReferenceEquals(aInstance, null) false
    aInstance.a this is usable and didn't throw

    So it is strange that the instance is null yet its field is accessible. It do make my game going haywire as I used == for several ifs in the game. (Which I will have to replace with ReferenceEquals to ensure it works 100%) What could possibly cause this in theory? As I failed to strip out and reproduce it. The version is 2019.1.8f1

    Additional information, this is the generated IL2CPP of the code around that

    Code (CSharp):
    1. IL_0505:
    2.     {
    3.         // var game = mm.CurrentGame;
    4.         ResultScreen_t0CF19DDBEC79B9AF237A836CCD40F3075F388737 * L_130 = __this->get_U3CU3E4__this_2();
    5.         NullCheck(L_130);
    6.         MatchManager_t3BB81878F86DC8374C31235AE92A7EB2C6CA5EDF * L_131 = ResultScreen_get_mm_m975D4A7A2BE234AC9B5905E22E628823D48594A6(L_130, /*hidden argument*/NULL);
    7.         NullCheck(L_131);
    8.         GameInfo_t7DC997CBAD726187D947D941AAC4500605E58E66 * L_132 = MatchManager_get_CurrentGame_mAC1D9E21D2A51B22E5A32675839A8D95E4BFD664(L_131, /*hidden argument*/NULL);
    9.         __this->set_U3CgameU3E5__4_6(L_132);
    10.         // var a = game != null;
    11.         GameInfo_t7DC997CBAD726187D947D941AAC4500605E58E66 * L_133 = __this->get_U3CgameU3E5__4_6();
    12.         IL2CPP_RUNTIME_CLASS_INIT(Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0_il2cpp_TypeInfo_var);
    13.         bool L_134 = Object_op_Inequality_m31EF58E217E8F4BDD3E409DEF79E1AEE95874FC1(L_133, (Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 *)NULL, /*hidden argument*/NULL);
    14.         __this->set_U3CaU3E5__5_7(L_134);
    15.         // var b = game == null;
    16.         GameInfo_t7DC997CBAD726187D947D941AAC4500605E58E66 * L_135 = __this->get_U3CgameU3E5__4_6();
    17.         bool L_136 = Object_op_Equality_mBC2401774F3BE33E8CF6F0A8148E66C95D6CFF1C(L_135, (Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 *)NULL, /*hidden argument*/NULL);
    18.         __this->set_U3CbU3E5__6_8(L_136);
    19.         // var c = object.ReferenceEquals(game, null);
    20.         GameInfo_t7DC997CBAD726187D947D941AAC4500605E58E66 * L_137 = __this->get_U3CgameU3E5__4_6();
    21.         __this->set_U3CcU3E5__7_9((bool)((((RuntimeObject*)(GameInfo_t7DC997CBAD726187D947D941AAC4500605E58E66 *)L_137) == ((RuntimeObject*)(RuntimeObject *)NULL))? 1 : 0));
    22.         // var d = game.systemGameName;
    23.         GameInfo_t7DC997CBAD726187D947D941AAC4500605E58E66 * L_138 = __this->get_U3CgameU3E5__4_6();
    24.         NullCheck(L_138);
    25.         String_t* L_139 = L_138->get_systemGameName_4();
    26.         __this->set_U3CdU3E5__8_10(L_139);
    27.         // Debug.Log($"Game {game} ABCD {a} {b} {c} {d}");
    In real device :
    Printing `game` : its .ToString override got invoked
    a : is false. It is null.
    b : is true. It is null.
    c : is false. It is actually not null??
    d : the string field is usable and printable.
     
    Last edited: Jul 5, 2019
  2. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    4,547
    5argon likes this.
  3. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,266
    This instance is of a ScriptableObject, so I think it is probably related somehow..

    (It's a reverse of 2., the underlying C++ object is there but Unity's custom == returns false already)
     
    Last edited: Jul 5, 2019
  4. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,266
    Cool, I know who's the culprit now. It's Addressables Asset System. By just :

    - Removing the package entirely
    - Replace Addressables.LoadSceneAsync with SceneManager
    - Replace prevent activation + .ActivateScene() with old AsyncOperation.allowSceneActivation = true

    Null check is correct again. No other changes related to that null check code.

    I was only try to add AAS and didn't migrate everything in yet, in fact the biggest clue is, there are only scenes in the packed group and no other assets at all. (So they got removed from build list automatically, and to load them I use Addressables.LoadSceneAsync instead) I should not think I could get away with preview packages! In fact I do think a bit and thought just some scenes-in-an-AAS couldn't possibly hurt anything, but it did.

    I used clean + rebuild AAS asset every time before building the game.

    The scene where this problem occur is inside a scene which is currently in AAS, therefore in an asset bundle. I think IL2CPP do something by scanning all available scenes in the build list and thus missed this "patching" of the null check?

    I tried reproducing in a clean project with this deduction, but I was not successful. Currently I can't waste more time making a submit case knowing that removing AAS fix it though, maybe just don't try to use it until out of preview.
     
    Last edited: Jul 5, 2019
  5. AlkisFortuneFish

    AlkisFortuneFish

    Joined:
    Apr 26, 2013
    Posts:
    664
    There is no patching involved, Equals, operator == etc. are just overridden in the UnityEngine.Object to call CompareBaseObjects. Besides, if that was the problem it would result in dead objects returning != null (since the pointer is not null), not live objects returning == null.
     
  6. AlkisFortuneFish

    AlkisFortuneFish

    Joined:
    Apr 26, 2013
    Posts:
    664
    Thinking about this, this is very possibly because you disallowed scene activation. There's some potentially relevant information here:
    https://forum.unity.com/threads/add...d-by-async-scene-loading.670822/#post-4498021
     
  7. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,266
    Hmmm, I know that prevent activation is kinda global flag (bad design..) but in my game it was very brief and not the scene that I encountered the problem. My title screen uses it, so while my intro is playing a title loads in the background. The gameplay scene where bug occurs was using just Addressables.LoadSceneAsync with no delay to get activated, start the scene as soon as it loads.

    I am still leaning towards that IL2CPP do something specifically for scene list in the build list popup and forgot to do the same to the one in AAS prebundled .bundle. (Like the ScriptableObject definition is not available, therefore Unity's custom == returns false, or something?)

    By the way, I tried following the equality test method and this is that. V_0 stay `false` when I get the error.

    Code (CSharp):
    1. // System.Boolean UnityEngine.Object::op_Equality(UnityEngine.Object,UnityEngine.Object)
    2. IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR bool Object_op_Equality_mBC2401774F3BE33E8CF6F0A8148E66C95D6CFF1C (Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * ___x0, Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * ___y1, const RuntimeMethod* method)
    3. {
    4.     static bool s_Il2CppMethodInitialized;
    5.     if (!s_Il2CppMethodInitialized)
    6.     {
    7.         il2cpp_codegen_initialize_method (Object_op_Equality_mBC2401774F3BE33E8CF6F0A8148E66C95D6CFF1C_MetadataUsageId);
    8.         s_Il2CppMethodInitialized = true;
    9.     }
    10.     bool V_0 = false;
    11.     {
    12.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_0 = ___x0;
    13.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_1 = ___y1;
    14.         IL2CPP_RUNTIME_CLASS_INIT(Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0_il2cpp_TypeInfo_var);
    15.         bool L_2 = Object_CompareBaseObjects_mE918232D595FB366CE5FAD4411C5FBD86809CC04(L_0, L_1, /*hidden argument*/NULL);
    16.         V_0 = L_2;
    17.         goto IL_000e;
    18.     }
    19.  
    20. IL_000e:
    21.     {
    22.         bool L_3 = V_0;
    23.         return L_3;
    24.     }
    25. }
    Then finally, the comparison logic

    Code (CSharp):
    1. // System.Boolean UnityEngine.Object::CompareBaseObjects(UnityEngine.Object,UnityEngine.Object)
    2. IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR bool Object_CompareBaseObjects_mE918232D595FB366CE5FAD4411C5FBD86809CC04 (Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * ___lhs0, Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * ___rhs1, const RuntimeMethod* method)
    3. {
    4.     static bool s_Il2CppMethodInitialized;
    5.     if (!s_Il2CppMethodInitialized)
    6.     {
    7.         il2cpp_codegen_initialize_method (Object_CompareBaseObjects_mE918232D595FB366CE5FAD4411C5FBD86809CC04_MetadataUsageId);
    8.         s_Il2CppMethodInitialized = true;
    9.     }
    10.     bool V_0 = false;
    11.     bool V_1 = false;
    12.     bool V_2 = false;
    13.     {
    14.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_0 = ___lhs0;
    15.         V_0 = (bool)((((RuntimeObject*)(Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 *)L_0) == ((RuntimeObject*)(RuntimeObject *)NULL))? 1 : 0);
    16.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_1 = ___rhs1;
    17.         V_1 = (bool)((((RuntimeObject*)(Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 *)L_1) == ((RuntimeObject*)(RuntimeObject *)NULL))? 1 : 0);
    18.         bool L_2 = V_1;
    19.         if (!L_2)
    20.         {
    21.             goto IL_001e;
    22.         }
    23.     }
    24.     {
    25.         bool L_3 = V_0;
    26.         if (!L_3)
    27.         {
    28.             goto IL_001e;
    29.         }
    30.     }
    31.     {
    32.         V_2 = (bool)1;
    33.         goto IL_0055;
    34.     }
    35.  
    36. IL_001e:
    37.     {
    38.         bool L_4 = V_1;
    39.         if (!L_4)
    40.         {
    41.             goto IL_0033;
    42.         }
    43.     }
    44.     {
    45.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_5 = ___lhs0;
    46.         IL2CPP_RUNTIME_CLASS_INIT(Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0_il2cpp_TypeInfo_var);
    47.         bool L_6 = Object_IsNativeObjectAlive_m683A8A1607CB2FF5E56EC09C5D150A8DA7D3FF08(L_5, /*hidden argument*/NULL);
    48.         V_2 = (bool)((((int32_t)L_6) == ((int32_t)0))? 1 : 0);
    49.         goto IL_0055;
    50.     }
    51.  
    52. IL_0033:
    53.     {
    54.         bool L_7 = V_0;
    55.         if (!L_7)
    56.         {
    57.             goto IL_0048;
    58.         }
    59.     }
    60.     {
    61.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_8 = ___rhs1;
    62.         IL2CPP_RUNTIME_CLASS_INIT(Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0_il2cpp_TypeInfo_var);
    63.         bool L_9 = Object_IsNativeObjectAlive_m683A8A1607CB2FF5E56EC09C5D150A8DA7D3FF08(L_8, /*hidden argument*/NULL);
    64.         V_2 = (bool)((((int32_t)L_9) == ((int32_t)0))? 1 : 0);
    65.         goto IL_0055;
    66.     }
    67.  
    68. IL_0048:
    69.     {
    70.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_10 = ___lhs0;
    71.         Object_tAE11E5E46CD5C37C9F3E8950C00CD8B45666A2D0 * L_11 = ___rhs1;
    72.         bool L_12 = il2cpp_codegen_object_reference_equals(L_10, L_11);
    73.         V_2 = L_12;
    74.         goto IL_0055;
    75.     }
    76.  
    77. IL_0055:
    78.     {
    79.         bool L_13 = V_2;
    80.         return L_13;
    81.     }
    82. }
    `___lhs0` here is my instance which is usable in other lines except == null which it decided to be `false`. So it must be some `false` in here. So, I suspect the "is native object alive" has some problem.
     
    Last edited: Jul 5, 2019
  8. AlkisFortuneFish

    AlkisFortuneFish

    Joined:
    Apr 26, 2013
    Posts:
    664
    Well, yeah, that's what I would imagine. The code is:

    [Code language="csharp"]
    static bool CompareBaseObjects(UnityEngine.Object lhs, UnityEngine.Object rhs)
    {
    bool lhsNull = ((object)lhs) == null;
    bool rhsNull = ((object)rhs) == null;

    if (rhsNull && lhsNull) return true;

    if (rhsNull) return !IsNativeObjectAlive(lhs);
    if (lhsNull) return !IsNativeObjectAlive(rhs);

    return lhs.m_InstanceID == rhs.m_InstanceID;
    }
    [/code]

    I doubt that IsNativeObjectAlive is actually broken, I suspect that there are situations where it will return false, based on exactly what initialisation state the object is. One thing that I am aware of is that a missing component script (and that includes ScriptableObjects, not just actual components) will == null. That is how you can check for missing scripts on prefabs, you GetComponent<Component> and then check whether any are null. So you have probably fallen victim of stripping indeed.