Code generated by IL2CPP is different than the original C# code when development build is closed

Hello, everyone.
I’m in Unity 2021.3.23f1, Windows platform, and met the problem specified in the title. The problem can be reproduced in 2022.3.39f1, too.

Here is the mini-reproducable C# code

using System;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IntPtr nativeHandle = new IntPtr(12345986);
        ReleaseNative(ref nativeHandle);

        int data = 12;
        ReleaseNative2(ref data);
    }

    static void fun(IntPtr ptr)
    {
        Debug.Log("test pointer" + ptr);
    }
    
    static void ReleaseNative(ref System.IntPtr nativeObjectPtr)
    {
        if (nativeObjectPtr != System.IntPtr.Zero)
        {
            var tmp = nativeObjectPtr;
            nativeObjectPtr = System.IntPtr.Zero;
            fun(tmp);
        }
    }
    
    static void fun2(int num)
    {
        Debug.Log("test int " + num);
    }
    
    static void ReleaseNative2(ref int data)
    {
        if (data != 0)
        {
            var tmp = data;
            data = 0;
            fun2(tmp);
        }
    }
}

and the code generated by il2cpp when development is closed is listed below

// System.Void Test::ReleaseNative(System.IntPtr&)
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void Test_ReleaseNative_m7B00509CB5DA0522106A58FEBC5118E987BF1998 (intptr_t* ___0_nativeObjectPtr, const RuntimeMethod* method) 
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&IntPtr_t_il2cpp_TypeInfo_var);
        s_Il2CppMethodInitialized = true;
    }
    {
        // if (nativeObjectPtr != System.IntPtr.Zero)
        intptr_t* L_0 = ___0_nativeObjectPtr;
        intptr_t L_1 = ((IntPtr_t_StaticFields*)il2cpp_codegen_static_fields_for(IntPtr_t_il2cpp_TypeInfo_var))->___Zero_1;
        bool L_2;
        L_2 = IntPtr_op_Inequality_m90EFC9C4CAD9A33E309F2DDF98EE4E1DD253637B(((*(L_0))), L_1, NULL);
        if (!L_2)
        {
            goto IL_001c;
        }
    }
    {
        // var tmp = nativeObjectPtr;
        intptr_t* L_3 = ___0_nativeObjectPtr;
        // nativeObjectPtr = System.IntPtr.Zero;
        intptr_t* L_4 = ___0_nativeObjectPtr;
        intptr_t L_5 = ((IntPtr_t_StaticFields*)il2cpp_codegen_static_fields_for(IntPtr_t_il2cpp_TypeInfo_var))->___Zero_1;
        *((intptr_t*)L_4) = (intptr_t)L_5;
        // fun(tmp);
        Test_fun_mAA5F3D0820F1DADB63841141BBDA24CAF0E9DE0D(((*(L_3))), NULL);
    }

IL_001c:
    {
        // }
        return;
    }
}

and the code generated by il2cpp when development is open is listed below

// System.Void Test::ReleaseNative(System.IntPtr&)
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void Test_ReleaseNative_m7B00509CB5DA0522106A58FEBC5118E987BF1998 (intptr_t* ___0_nativeObjectPtr, const RuntimeMethod* method) 
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&IntPtr_t_il2cpp_TypeInfo_var);
        s_Il2CppMethodInitialized = true;
    }
    bool V_0 = false;
    intptr_t V_1;
    memset((&V_1), 0, sizeof(V_1));
    {
        // if (nativeObjectPtr != System.IntPtr.Zero)
        intptr_t* L_0 = ___0_nativeObjectPtr;
        intptr_t L_1 = ((IntPtr_t_StaticFields*)il2cpp_codegen_static_fields_for(IntPtr_t_il2cpp_TypeInfo_var))->___Zero_1;
        bool L_2;
        L_2 = IntPtr_op_Inequality_m90EFC9C4CAD9A33E309F2DDF98EE4E1DD253637B(((*(L_0))), L_1, NULL);
        V_0 = L_2;
        bool L_3 = V_0;
        if (!L_3)
        {
            goto IL_0024;
        }
    }
    {
        // var tmp = nativeObjectPtr;
        intptr_t* L_4 = ___0_nativeObjectPtr;
        V_1 = ((*(L_4)));
        // nativeObjectPtr = System.IntPtr.Zero;
        intptr_t* L_5 = ___0_nativeObjectPtr;
        intptr_t L_6 = ((IntPtr_t_StaticFields*)il2cpp_codegen_static_fields_for(IntPtr_t_il2cpp_TypeInfo_var))->___Zero_1;
        *((intptr_t*)L_5) = (intptr_t)L_6;
        // fun(tmp);
        intptr_t L_7 = V_1;
        Test_fun_mAA5F3D0820F1DADB63841141BBDA24CAF0E9DE0D(L_7, NULL);
    }

IL_0024:
    {
        // }
        return;
    }
}

The parameter passed to the function fun will be Inptr.Zero when the development build is open, which is undesirable.
And if the type is int instead of System.Inptr, the problem is missing.

Does anyone know what happens behind the IL2CPP? How does development build affect the code generation in il2cpp?

The difference is code generation between development and non-development is coming from the C# compiler. In development mode the C# compiler is in the debug configuration and in non-development configuration. In development mode the store to tmp is emitted:

// var tmp = nativeObjectPtr;
intptr_t* L_4 = ___0_nativeObjectPtr;
V_1 = ((*(L_4)));

But in non-development mode the C# compiler optimizes the local variable out, since’s there’s no need for an explicit local.

// var tmp = nativeObjectPtr;
intptr_t* L_3 = ___0_nativeObjectPtr;
// A read of *L_3 should have happened here!

This should be fine; IL2CPP should not have delayed the load of ___0_nativeObjectPtr’s value. This bug should have been fixed in 2021.3.31f1 so 2021.3.39f1 should not have this problem. And when I run this example in 2021.3.39f1 IL2CPP does the load correctly:

// var tmp = nativeObjectPtr;
intptr_t* L_4 = ___0_nativeObjectPtr;
intptr_t L_5 = *((intptr_t*)L_4); // *** Corrected load ***
// nativeObjectPtr = System.IntPtr.Zero;
intptr_t* L_6 = ___0_nativeObjectPtr;
intptr_t L_7 = ((IntPtr_t_StaticFields*)il2cpp_codegen_static_fields_for(IntPtr_t_il2cpp_TypeInfo_var))->___Zero_1;
*((intptr_t*)L_6) = (intptr_t)L_7;
// fun(tmp);
Test_fun_mAA5F3D0820F1DADB63841141BBDA24CAF0E9DE0D(L_5, NULL); // *** Non-zero'd value passed here

If you can still reproduced this in 2021.3.39f1, please submit a bug with your repro project.

1 Like

Thanks for the responce and the important info. After the test, I found the code generated in 2021.3.39f1 and 2022.3.39f1 are both correct.
By the way, I found that the cpp code generated by 2022.3.39f1 doesn’t contain the related C# code in comment any more. Is it designed deliberately and is there any way to view the C# code as in 2021.3.23f1?

Thanks for verifying that this is fixed.

We did stop generating comments to reduce the amount of data we were writing. You can re-enable the comments by passing the --emit-comments flag to IL2CPP using il2cpp additional arguments.