Today we’ll look at the C++ code that IL2CPP outputs when we use iterator functions (those that yield), switch statements, and using blocks. What are you really telling the computer to do when you use these C# features? Read on to find out.

(Website Announcement: check out the new tags page to find articles by topic)

Iterator Functions

Let’s start by discussing iterator functions. These are functions that feature yield return or yield break statements in them and return IEnumerator, IEnumerator<T>, IEnumerable, or IEnumerable<T>. The most common usage of such functions in Unity is to create coroutines by returning IEnumerator, so let’s start by making a simple one of those:

public static class TestClass
{
    public static IEnumerator IteratorFunction(
        bool yieldTwice,
        string firstYield,
        string secondYield)
    {
        yield return firstYield;
        if (!yieldTwice)
        {
            yield break;
        }
 
        yield return secondYield;
    }
}

Now let’s use Unity 2018.1.0f2 to build for iOS and look at the resulting C++ code in Xcode 9.3:

extern "C"  RuntimeObject* TestClass_IteratorFunction_m3691477440 (RuntimeObject * __this /* static, unused */, bool ___yieldTwice0, String_t* ___firstYield1, String_t* ___secondYield2, const RuntimeMethod* method)
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_method (TestClass_IteratorFunction_m3691477440_MetadataUsageId);
        s_Il2CppMethodInitialized = true;
    }
    U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * V_0 = NULL;
    {
        U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * L_0 = (U3CIteratorFunctionU3Ec__Iterator0_t1787652012 *)il2cpp_codegen_object_new(U3CIteratorFunctionU3Ec__Iterator0_t1787652012_il2cpp_TypeInfo_var);
        U3CIteratorFunctionU3Ec__Iterator0__ctor_m4232682717(L_0, /*hidden argument*/NULL);
        V_0 = L_0;
        U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * L_1 = V_0;
        String_t* L_2 = ___firstYield1;
        NullCheck(L_1);
        L_1->set_firstYield_0(L_2);
        U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * L_3 = V_0;
        bool L_4 = ___yieldTwice0;
        NullCheck(L_3);
        L_3->set_yieldTwice_1(L_4);
        U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * L_5 = V_0;
        String_t* L_6 = ___secondYield2;
        NullCheck(L_5);
        L_5->set_secondYield_2(L_6);
        U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * L_7 = V_0;
        return L_7;
    }
}

First off, we get method initialization overhead for this iterator function. The culprit is the U3CIteratorFunctionU3Ec__Iterator0_t1787652012_il2cpp_TypeInfo_var that’s passed to il2cpp_codegen_object_new a few lines down. This is instantiating an instance of a C# class that was generated for us by the compiler to represent the state of the iterator function. Let’s go look at that U3CIteratorFunctionU3Ec__Iterator0_t1787652012 class:

struct  U3CIteratorFunctionU3Ec__Iterator0_t1787652012  : public RuntimeObject
{
public:
    // System.String TestClass/<IteratorFunction>c__Iterator0::firstYield
    String_t* ___firstYield_0;
    // System.Boolean TestClass/<IteratorFunction>c__Iterator0::yieldTwice
    bool ___yieldTwice_1;
    // System.String TestClass/<IteratorFunction>c__Iterator0::secondYield
    String_t* ___secondYield_2;
    // System.Object TestClass/<IteratorFunction>c__Iterator0::$current
    RuntimeObject * ___U24current_3;
    // System.Boolean TestClass/<IteratorFunction>c__Iterator0::$disposing
    bool ___U24disposing_4;
    // System.Int32 TestClass/<IteratorFunction>c__Iterator0::$PC
    int32_t ___U24PC_5;
 
public:
    // {many accessor functions removed}
};

We see here that the local variables from the function have been added as fields: ___firstYield_0, ___yieldTwice_1, and ___secondYield_2. Additionally, there’s a pointer to an object (___U24current_3), a flag “disposing” (___U24disposing_4), and “PC” (___U24PC_5). Next let’s look at the constructor for this class:

extern "C"  void U3CIteratorFunctionU3Ec__Iterator0__ctor_m4232682717 (U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * __this, const RuntimeMethod* method)
{
    {
        Object__ctor_m297566312(__this, /*hidden argument*/NULL);
        return;
    }
}

This constructor does nothing except call the base class (object) constructor. Looking back at the iterator function, we see a series of unnecessary NullCheck calls along with calls to assign all the local variables to the fields of the class. Once that’s done, the class is simply returned. C# creates an illusion that the function just “suspends” and “resumes,” but really a miniature state machine is created in a class. To see how that state machine works, let’s look at the MoveNext function that was generated:

extern "C"  bool U3CIteratorFunctionU3Ec__Iterator0_MoveNext_m3491766454 (U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * __this, const RuntimeMethod* method)
{
    uint32_t V_0 = 0;
    {
        int32_t L_0 = __this->get_U24PC_5();
        V_0 = L_0;
        __this->set_U24PC_5((-1));
        uint32_t L_1 = V_0;
        switch (L_1)
        {
            case 0:
            {
                goto IL_0025;
            }
            case 1:
            {
                goto IL_0045;
            }
            case 2:
            {
                goto IL_0075;
            }
        }
    }
    {
        goto IL_007c;
    }
 
IL_0025:
    {
        String_t* L_2 = __this->get_firstYield_0();
        __this->set_U24current_3(L_2);
        bool L_3 = __this->get_U24disposing_4();
        if (L_3)
        {
            goto IL_0040;
        }
    }
    {
        __this->set_U24PC_5(1);
    }
 
IL_0040:
    {
        goto IL_007e;
    }
 
IL_0045:
    {
        bool L_4 = __this->get_yieldTwice_1();
        if (L_4)
        {
            goto IL_0055;
        }
    }
    {
        goto IL_007c;
    }
 
IL_0055:
    {
        String_t* L_5 = __this->get_secondYield_2();
        __this->set_U24current_3(L_5);
        bool L_6 = __this->get_U24disposing_4();
        if (L_6)
        {
            goto IL_0070;
        }
    }
    {
        __this->set_U24PC_5(2);
    }
 
IL_0070:
    {
        goto IL_007e;
    }
 
IL_0075:
    {
        __this->set_U24PC_5((-1));
    }
 
IL_007c:
    {
        return (bool)0;
    }
 
IL_007e:
    {
        return (bool)1;
    }
}

The “PC” we saw earlier (get_U24PC_5) is used to keep track of the part of the function that should execute when the iterator is “resumed.” A simple switch is used to jump to the code that executes in that part of the function. We see in the first part of the function that firstYield is set to the “current” field of the class and then the “PC” moves on to either the end if the enumerator class was disposed (i.e. via Dispose) or the next part of the function otherwise. The “current” value is exposed via the Current property:

extern "C"  RuntimeObject * U3CIteratorFunctionU3Ec__Iterator0_System_Collections_IEnumerator_get_Current_m1823193357 (U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * __this, const RuntimeMethod* method)
{
    {
        RuntimeObject * L_0 = __this->get_U24current_3();
        return L_0;
    }
}

Lastly, let’s look at the generated Dispose method:

extern "C"  void U3CIteratorFunctionU3Ec__Iterator0_Dispose_m665316651 (U3CIteratorFunctionU3Ec__Iterator0_t1787652012 * __this, const RuntimeMethod* method)
{
    {
        __this->set_U24disposing_4((bool)1);
        __this->set_U24PC_5((-1));
        return;
    }
}

This simply sets the “disposing” flag that we saw before and also sets the “PC” to a value (-1) indicating that the enumerator has been disposed.

That’s about all there is to this iterator function, so let’s move on to the other variants starting with an iterator function that returns IEnumerator<T>:

public static class TestClass
{
    public static IEnumerator<string> GenericIteratorFunction(
        bool yieldTwice,
        string firstYield,
        string secondYield)
    {
        yield return firstYield;
        if (!yieldTwice)
        {
            yield break;
        }
 
        yield return secondYield;
    }
}

The C++ version of this looks almost identical to the non-generic IEnumerator version except that it instantiates a different class. I’ll omit it, and we’ll skip straight to the generated class:

struct  U3CGenericIteratorFunctionU3Ec__Iterator1_t1691036585  : public RuntimeObject
{
public:
    // System.String TestClass/<GenericIteratorFunction>c__Iterator1::firstYield
    String_t* ___firstYield_0;
    // System.Boolean TestClass/<GenericIteratorFunction>c__Iterator1::yieldTwice
    bool ___yieldTwice_1;
    // System.String TestClass/<GenericIteratorFunction>c__Iterator1::secondYield
    String_t* ___secondYield_2;
    // System.String TestClass/<GenericIteratorFunction>c__Iterator1::$current
    String_t* ___U24current_3;
    // System.Boolean TestClass/<GenericIteratorFunction>c__Iterator1::$disposing
    bool ___U24disposing_4;
    // System.Int32 TestClass/<GenericIteratorFunction>c__Iterator1::$PC
    int32_t ___U24PC_5;
 
public:
    // {many accessor functions removed}
};

The only difference here is that the “current” field is no longer a plain object but now a strongly-typed string. The generated constructor, MoveNext, and Current are all essentially the same, too.

Next, let’s try an iterator function that returns an IEnumerable instead of an IEnumerator:

public static class TestClass
{
    public static IEnumerable EnumerableIteratorFunction(
        bool yieldTwice,
        string firstYield,
        string secondYield)
    {
        yield return firstYield;
        if (!yieldTwice)
        {
            yield break;
        }
 
        yield return secondYield;
    }
}

Again, the generated C++ function and class are basically the same. One difference is that IEnumerable has a GetEnumerator method, so let’s look at it:

extern "C"  RuntimeObject* U3CEnumerableIteratorFunctionU3Ec__Iterator2_System_Collections_IEnumerable_GetEnumerator_m1849043942 (U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * __this, const RuntimeMethod* method)
{
    {
        RuntimeObject* L_0 = U3CEnumerableIteratorFunctionU3Ec__Iterator2_System_Collections_Generic_IEnumerableU3CobjectU3E_GetEnumerator_m2126724345(__this, /*hidden argument*/NULL);
        return L_0;
    }
}

This just calls another function, so we’ll look at that to find out what’s happening:

extern "C"  RuntimeObject* U3CEnumerableIteratorFunctionU3Ec__Iterator2_System_Collections_Generic_IEnumerableU3CobjectU3E_GetEnumerator_m2126724345 (U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * __this, const RuntimeMethod* method)
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_method (U3CEnumerableIteratorFunctionU3Ec__Iterator2_System_Collections_Generic_IEnumerableU3CobjectU3E_GetEnumerator_m2126724345_MetadataUsageId);
        s_Il2CppMethodInitialized = true;
    }
    U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * V_0 = NULL;
    {
        int32_t* L_0 = __this->get_address_of_U24PC_5();
        int32_t L_1 = Interlocked_CompareExchange_m3023855514(NULL /*static, unused*/, (int32_t*)L_0, 0, ((int32_t)-2), /*hidden argument*/NULL);
        if ((!(((uint32_t)L_1) == ((uint32_t)((int32_t)-2)))))
        {
            goto IL_0014;
        }
    }
    {
        return __this;
    }
 
IL_0014:
    {
        U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * L_2 = (U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 *)il2cpp_codegen_object_new(U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023_il2cpp_TypeInfo_var);
        U3CEnumerableIteratorFunctionU3Ec__Iterator2__ctor_m2964264911(L_2, /*hidden argument*/NULL);
        V_0 = L_2;
        U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * L_3 = V_0;
        String_t* L_4 = __this->get_firstYield_0();
        NullCheck(L_3);
        L_3->set_firstYield_0(L_4);
        U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * L_5 = V_0;
        bool L_6 = __this->get_yieldTwice_1();
        NullCheck(L_5);
        L_5->set_yieldTwice_1(L_6);
        U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * L_7 = V_0;
        String_t* L_8 = __this->get_secondYield_2();
        NullCheck(L_7);
        L_7->set_secondYield_2(L_8);
        U3CEnumerableIteratorFunctionU3Ec__Iterator2_t1515875023 * L_9 = V_0;
        return L_9;
    }
}

Here we have some method initialization overhead to start before moving on to the method’s work. The first chunk of code gets the “PC” and does an atomic operation via Interlocked.CompareExchange to check if the “PC” is its default value: -2. If that’s the case, the generated IEnumerator class itself is returned. That’s an optimization that allows us to skip allocating a new managed object in the most common case.

If, however, GetEnumerator is called after iteration has begun (i.e. MoveNext has been called) then we go into a slower chunk of code. The bottom part of the function looks just like the code in the iterator function itself. It allocates a new object of the same class and copies over all the fields from one object to another. Notably, the “PC” and “current” fields are not copied, which allows the two enumerators to enumerate independently.

Conclusion: Iterators are implemented in IL2CPP by generating a new state machine class, allocating and copying local variables to one when the iterator function is invoked, and using a switch to execute the right “state” of the function when “resumed” by MoveNext. The function does not simply “suspend” and “resume” as C# syntax makes it appear, but instead performs much more costly work such as creating garbage for the GC.

Switch

The lowly switch statement can be something of a mystery. Let’s find out for sure how they work by simply looking at the IL2CPP output:

public static class TestClass
{
    public static int SwitchInt(int x)
    {
        switch (x)
        {
            case 0: return 0;
            case 1: return 10;
            case 2: return 20;
            default: return -1;
        }
    }
}

Here’s the C++ for this:

extern "C"  int32_t TestClass_SwitchInt_m2164449242 (RuntimeObject * __this /* static, unused */, int32_t ___x0, const RuntimeMethod* method)
{
    {
        int32_t L_0 = ___x0;
        switch (L_0)
        {
            case 0:
            {
                goto IL_0017;
            }
            case 1:
            {
                goto IL_0019;
            }
            case 2:
            {
                goto IL_001c;
            }
        }
    }
    {
        goto IL_001f;
    }
 
IL_0017:
    {
        return 0;
    }
 
IL_0019:
    {
        return ((int32_t)10);
    }
 
IL_001c:
    {
        return ((int32_t)20);
    }
 
IL_001f:
    {
        return (-1);
    }
}

While this code is quite verbose due to the use of goto and unnecessary code blocks ({}), it’s still really simple. The C# switch simply turned into a C++ switch.

Now let’s try using string with switch:

public static class TestClass
{
    public static int SwitchString(string x)
    {
        switch (x)
        {
            case "0": return 0;
            case "1": return 10;
            case "2": return 20;
            default: return -1;
        }
    }
}
extern "C"  int32_t TestClass_SwitchString_m1552584162 (RuntimeObject * __this /* static, unused */, String_t* ___x0, const RuntimeMethod* method)
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_method (TestClass_SwitchString_m1552584162_MetadataUsageId);
        s_Il2CppMethodInitialized = true;
    }
    {
        String_t* L_0 = ___x0;
        if (!L_0)
        {
            goto IL_0043;
        }
    }
    {
        String_t* L_1 = ___x0;
        bool L_2 = String_op_Equality_m920492651(NULL /*static, unused*/, L_1, _stringLiteral3452614544, /*hidden argument*/NULL);
        if (L_2)
        {
            goto IL_003b;
        }
    }
    {
        String_t* L_3 = ___x0;
        bool L_4 = String_op_Equality_m920492651(NULL /*static, unused*/, L_3, _stringLiteral3452614543, /*hidden argument*/NULL);
        if (L_4)
        {
            goto IL_003d;
        }
    }
    {
        String_t* L_5 = ___x0;
        bool L_6 = String_op_Equality_m920492651(NULL /*static, unused*/, L_5, _stringLiteral3452614542, /*hidden argument*/NULL);
        if (L_6)
        {
            goto IL_0040;
        }
    }
    {
        goto IL_0043;
    }
 
IL_003b:
    {
        return 0;
    }
 
IL_003d:
    {
        return ((int32_t)10);
    }
 
IL_0040:
    {
        return ((int32_t)20);
    }
 
IL_0043:
    {
        return (-1);
    }
}

We’re using string literals, so we get method initialization overhead at the start of the function. After that, IL2CPP simply generates a series of if statements using the string equality (==) operator.

Conclusion: Using switch with an int simply generates a switch. Using a string generates a series of string comparisons and if checks.

Using Blocks

The using keyword has two meanings in C#, but here we’ll look at the version that creates a block of code to “use” an IDisposable variable:

public static class TestClass
{
    public static int UsingIDisposable(IDisposable x)
    {
        int ret = 0;
 
        using (x)
        {
            ret = 10;
        }
 
        ret = 20;
 
        return ret;
    }
}

Here’s what IL2CPP generates for this function:

extern "C"  int32_t TestClass_UsingIDisposable_m491513606 (RuntimeObject * __this /* static, unused */, RuntimeObject* ___x0, const RuntimeMethod* method)
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_method (TestClass_UsingIDisposable_m491513606_MetadataUsageId);
        s_Il2CppMethodInitialized = true;
    }
    int32_t V_0 = 0;
    RuntimeObject* V_1 = NULL;
    Exception_t * __last_unhandled_exception = 0;
    NO_UNUSED_WARNING (__last_unhandled_exception);
    Exception_t * __exception_local = 0;
    NO_UNUSED_WARNING (__exception_local);
    int32_t __leave_target = 0;
    NO_UNUSED_WARNING (__leave_target);
    {
        V_0 = 0;
        RuntimeObject* L_0 = ___x0;
        V_1 = L_0;
    }
 
IL_0004:
    try
    { // begin try (depth: 1)
        V_0 = ((int32_t)10);
        IL2CPP_LEAVE(0x19, FINALLY_000c);
    } // end try (depth: 1)
    catch(Il2CppExceptionWrapper&amp; e)
    {
        __last_unhandled_exception = (Exception_t *)e.ex;
        goto FINALLY_000c;
    }
 
FINALLY_000c:
    { // begin finally (depth: 1)
        {
            RuntimeObject* L_1 = V_1;
            if (!L_1)
            {
                goto IL_0018;
            }
        }
 
IL_0012:
        {
            RuntimeObject* L_2 = V_1;
            NullCheck(L_2);
            InterfaceActionInvoker0::Invoke(0 /* System.Void System.IDisposable::Dispose() */, IDisposable_t3640265483_il2cpp_TypeInfo_var, L_2);
        }
 
IL_0018:
        {
            IL2CPP_END_FINALLY(12)
        }
    } // end finally (depth: 1)
    IL2CPP_CLEANUP(12)
    {
        IL2CPP_JUMP_TBL(0x19, IL_0019)
        IL2CPP_RETHROW_IF_UNHANDLED(Exception_t *)
    }
 
IL_0019:
    {
        V_0 = ((int32_t)20);
        int32_t L_3 = V_0;
        return L_3;
    }
}

Previous experience with exceptions has prepared us for this output. There’s the usual method initialization overhead to start and then the code within the using block is essentially wrapped in a try block. No exceptions are caught, but there is essentially a finally block generated to call Dispose on the “used” object.

Conclusion: A using block results in a try block and a finally block with a Dispose call in it.