18

Apparently, Constrained Execution Region guarantees do not apply to iterators (probably because of how they are implemented and all), but is this a bug or by design? [See the example below.]

i.e. What are the rules on CERs being used with iterators?

using System.Runtime.CompilerServices;
using System.Runtime.ConstrainedExecution;

class Program
{
    static bool cerWorked;
    static void Main(string[] args)
    {
        try
        {
            cerWorked = true;
            foreach (var v in Iterate()) { }
        }
        catch { System.Console.WriteLine(cerWorked); }
        System.Console.ReadKey();
    }

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    unsafe static void StackOverflow()
    {
        Big big;
        big.Bytes[int.MaxValue - 1] = 1;
    }

    static System.Collections.Generic.IEnumerable<int> Iterate()
    {
        RuntimeHelpers.PrepareConstrainedRegions();
        try { cerWorked = false; yield return 5; }
        finally { StackOverflow(); }
    }

    unsafe struct Big { public fixed byte Bytes[int.MaxValue]; }
}

(Code mostly stolen from here.)

Community
  • 1
  • 1
user541686
  • 205,094
  • 128
  • 528
  • 886
  • 2
    For what its worth you seem to be the first person to notice this...at least as far as I could tell from googling for other references of it. – Brian Gideon Jul 28 '11 at 17:48
  • I did find this https://vmccontroller.svn.codeplex.com/svn/VmcController/VmcServices/DetectOpenFiles.cs snippet of code in which the unsuspecting author is not going to get the CER that he thinks he's getting. – Brian Gideon Jul 28 '11 at 19:28
  • @Brian: Lol, nice. I think it's something most people don't use very often, and those who do probably already know intuitively, without actually ever having thought about it. Just my guess, though. – user541686 Jul 28 '11 at 19:54
  • I don't know. Your discovery is pretty esoteric. The people who worked on CERs or iterators may not have thought of this edge case afterall. Otherwise you might expect a compiler error or warning like you get when you try to put a `yield return` in a `try-catch`. Just saying... – Brian Gideon Jul 28 '11 at 20:05

1 Answers1

15

Well, I do not know if this a bug or just a really weird edge case in which CERs were not designed to handle.

So here is the pertinent code.

private static IEnumerable<int> Iterate()
{
    RuntimeHelpers.PrepareConstrainedRegions();
    try { cerWorked = false; yield return 5; }
    finally { StackOverflow(); }
}

When this gets compiled and we attempt to decompile it into C# with Reflector we get this.

private static IEnumerable<int> Iterate()
{
    RuntimeHelpers.PrepareConstrainedRegions();
    cerWorked = false;
    yield return 5;
}

Now wait just a second! Reflector has this all screwed up. This is what the IL actually looks like.

.method private hidebysig static class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> Iterate() cil managed
{
    .maxstack 2
    .locals init (
        [0] class Sandbox.Program/<Iterate>d__1 d__,
        [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> enumerable)
    L_0000: ldc.i4.s -2
    L_0002: newobj instance void Sandbox.Program/<Iterate>d__1::.ctor(int32)
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: stloc.1 
    L_000a: br.s L_000c
    L_000c: ldloc.1 
    L_000d: ret 
}

Notice that there is, in fact, no call to PrepareConstrainedRegions despite what Reflector says. So where is it lurking? Well, it is right there in the auto generated IEnumerator's MoveNext method. This time Reflector gets it right.

private bool MoveNext()
{
    try
    {
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                RuntimeHelpers.PrepareConstrainedRegions();
                this.<>1__state = 1;
                Program.cerWorked = false;
                this.<>2__current = 5;
                this.<>1__state = 2;
                return true;

            case 2:
                this.<>1__state = 1;
                this.<>m__Finally2();
                break;
        }
        return false;
    }
    fault
    {
        this.System.IDisposable.Dispose();
    }
}

And where did that call to StackOverflow mysteriously move to? Right inside the m_Finally2() method.

private void <>m__Finally2()
{
    this.<>1__state = -1;
    Program.StackOverflow();
}

So lets examine this a little more closely. We now have our PrepareConstainedRegions call inside a try block instead of outside where it should be. And our StackOverflow call has moved from a finally block to a try block.

According to the documentation PrepareConstrainedRegions must immediatly precede the try block. So the assumption is that it is ineffective if placed anywhere else.

But, even if the C# compiler got that part right things would be still be screwed up because try blocks are not constrained. Only catch, finally, and fault blocks are. And guess what? That StackOverflow call got moved from a finally block to a try block!

Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • +1, nice answer, but what is `fault` block? **Edit:** never mind it is something related to [yield](http://startbigthinksmall.wordpress.com/2008/06/09/behind-the-scenes-of-the-c-yield-keyword/) – Jalal Said Jul 28 '11 at 17:53
  • 1
    @Jalal: No, it's *not* related to yield. It's pretty much just `catch { ... throw; }`, without the extra `throw` statement. (Since they're pretty much the same thing, it's not a feature of C#.) – user541686 Jul 28 '11 at 18:49
  • 1
    @Jalal A fault block is like a finally block, but only run when control leaves due to exception. [Here is a little bit more on that.](http://www.simple-talk.com/community/blogs/simonc/archive/2011/02/09/99250.aspx) The compiler _uses it_ in its implementation of an enumerable state machine, but it's not specific to the yield keyword. – Chris Hannon Jul 28 '11 at 18:54