Monday, April 28, 2008

The C# Null-Coalescing Operator, PEVerify and ILMerge

It seems I errantly blamed ILMerge for those PEVerify errors. At worst it may be a C# compiler optimization gone awry or it may be that PEVerify is not smart enough.

The Source

Consider this code:

public IList<ITestCommand> Children
    get { return children ?? (IList<ITestCommand>)EmptyArray<ITestCommand>.Instance; }

The left-hand side of the null-coalescing operator has type: List<ITestCommand>.

The right-hand side of the null-coalescing operator has type: ITestCommand[] before the cast.

The result has type: IList<ITestCommand>.

So we need a little silent cast in there to make the types match up on either side.  Really, it's just providing a compatible upper bound to which both sides can be converted.  At runtime, this operation is a no-op.  Technically it's something that the compiler itself could have inferred if it were smart enough.

The Unoptimized IL

Let's take a quick look at the IL that the C# compiler generates for a standard unoptimized build.

Notice that the castclass operator appears in the listing.  There is also quite a bit of jiggery-pokery involving a compiler generated temporary local variable.

.method public hidebysig specialname newslot virtual final instance class [mscorlib]System.Collections.Generic.IList`1<class Gallio.Model.Execution.ITestCommand> get_Children() cil managed
    .maxstack 2
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.IList`1<class Gallio.Model.Execution.ITestCommand> CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldfld class [mscorlib]System.Collections.Generic.List`1<class Gallio.Model.Execution.ITestCommand> Gallio.Model.Execution.ManagedTestCommand::children
    L_0007: dup 
    L_0008: brtrue.s L_0015
    L_000a: pop 
    L_000b: ldsfld !0[] Gallio.Collections.EmptyArray`1<class Gallio.Model.Execution.ITestCommand>::Instance
    L_0010: castclass [mscorlib]System.Collections.Generic.IList`1<class Gallio.Model.Execution.ITestCommand>
    L_0015: stloc.0 
    L_0016: br.s L_0018
    L_0018: ldloc.0 
    L_0019: ret 

The Optimized IL

When generating optimized code, the C# compiler realizes that the castclass is unnecessary.  So it drops it.  It also drops the unnecessary load/stores to a local variable and we end up with the following code.

.method public hidebysig specialname newslot virtual final instance class [mscorlib]System.Collections.Generic.IList`1<class Gallio.Model.Execution.ITestCommand> get_Children() cil managed
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<class Gallio.Model.Execution.ITestCommand> Gallio.Model.Execution.ManagedTestCommand::children
    L_0006: dup 
    L_0007: brtrue.s L_000f
    L_0009: pop 
    L_000a: ldsfld !0[] Gallio.Collections.EmptyArray`1<class Gallio.Model.Execution.ITestCommand>::Instance
    L_000f: ret 

PEVerify Has a Conniption

Without the castclass in the optimized code, the IL is now missing a crucial piece of information: what is the type of the value of the top of the stack when we reach the ret instruction?

In one case, we may have duplicated the value produced by the ldfld instruction so the top has type List<ITestCommand>.

In the other case, we have discarded the original value and instead loaded an array from somewhere else using ldsfld so the top has type ITestCommand[].

Uh oh!

[IL]: Error: [C:\Source\MbUnit\v3\src\Gallio\Gallio\bin\Gallio.dll : Gallio.Model.Execution.ManagedTestCommand::get_Children][offset 0x0000000F][found ref 'System.Collections.IEnumerable'][expected ref 'System.Collections.Generic.IList`1[Gallio.Model.Execution.ITestCommand]'] Unexpected type on the stack.

Without the castclass PEVerify has to do some rather more complicated work to figure out whether the types are compatible.  Apparently, it fails.  But it does get far enough to realize that one of the values can be converted to IEnumerable.

Is This a Problem?

I don't know.  Apparently we have been running this code in Gallio release builds for many months now and not seen any InvalidProgramException errors.  So even though PEVerify is confused, the CLR at least does not choke on the code.

In any case, it's definitely not ILMerge's fault.  (Sorry Mike!)

Technorati Tags:

No comments: