Sunday, September 22, 2013

.net under the covers — using/Using/use

Something I had occasion to look at the other day was exactly how the auto-disposal mechanism actually works for the out of the box .net languages: and there are some interesting quirks to be found. Take the equivalent sample code fragments

using (var r = new StringReader(text))
{
Console.WriteLine(r.ReadToEnd());
}
view raw gistfile1.cs hosted with ❤ by GitHub
Using r As New StringReader(text)
Console.WriteLine(r.ReadToEnd())
End Using
view raw gistfile1.vb hosted with ❤ by GitHub
use r = new StringReader(text)
printfn "%A" <| r.ReadToEnd()
view raw gistfile1.fs hosted with ❤ by GitHub

and for completeness, C++/CLI stack based disposal

{
StringReader r(text);
Console::WriteLine(r.ReadToEnd());
}
view raw gistfile1.cs hosted with ❤ by GitHub

All languages and configurations render this as a try {} finally { Dispose() } construction (apart from stack-based C++/CLI semantics, which generates a try {} catch { Dispose(); throw;} Dispose()), but there are devils in the details. In release mode, C# and VB compile to the same IL -- just dispose if the used value is not null

finally
{
IL_001a: ldloc.1
IL_001b: brfalse.s IL_0023
IL_001d: ldloc.1
IL_001e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0023: endfinally
} // end handler
view raw gistfile1.cs hosted with ❤ by GitHub

whereas F# compiles to

finally
{
IL_002c: ldloc.0
IL_002d: isinst [mscorlib]System.IDisposable
IL_0032: stloc.3
IL_0033: ldloc.3
IL_0034: brfalse.s IL_003f
IL_0036: ldloc.3
IL_0037: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003c: ldnull
IL_003d: pop
IL_003e: endfinally
IL_003f: ldnull
IL_0040: pop
IL_0041: endfinally
} // end handler
// which ILSpy decompiled to
finally
{
IDisposable disposable = r as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

which is unexpected, as the compiler enforces the subject of a use initialisation to be an IDisposable anyway -- but, importantly, it does not test for null on the original reference.


In debug mode, all three languages differ, C# flips the test around

finally
{
IL_001e: ldloc.1
IL_001f: ldnull
IL_0020: ceq
IL_0022: stloc.2
IL_0023: ldloc.2
IL_0024: brtrue.s IL_002d
IL_0026: ldloc.1
IL_0027: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_002c: nop
IL_002d: endfinally
} // end handler
view raw gistfile1.cs hosted with ❤ by GitHub

VB does something a bit more like F#

finally
{
IL_001f: ldloc.1
IL_0020: ldnull
IL_0021: ceq
IL_0023: ldc.i4.0
IL_0024: ceq
IL_0026: stloc.2
IL_0027: ldloc.2
IL_0028: brfalse.s IL_0031
IL_002a: ldloc.1
IL_002b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0030: nop
IL_0031: nop
IL_0032: endfinally
} // end handler
// which ILSpy decompiled to (not recognising it as a Using block even when decompiling to VB!)
finally
{
bool flag = r != null;
if (flag)
{
((IDisposable)r).Dispose();
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

and F# just adds one of its usual bursts of gratuitous branching in the middle

finally
{
IL_0029: ldloc.0
IL_002a: isinst [mscorlib]System.IDisposable
IL_002f: stloc.s 4
IL_0031: ldloc.s 4
IL_0033: brfalse.s IL_0037
IL_0035: br.s IL_0039
IL_0037: br.s IL_0043
IL_0039: ldloc.s 4
IL_003b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0040: ldnull
IL_0041: pop
IL_0042: endfinally
IL_0043: ldnull
IL_0044: pop
IL_0045: endfinally
} // end handler
view raw gistfile1.cs hosted with ❤ by GitHub

The C++/CLI code can rely on the stack-based object not being null, so eschews any checks

fault
{
IL_0022: ldloc.0
IL_0023: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0028: endfinally
} // end handler
IL_0029: ldloc.0
IL_002a: callvirt instance void [mscorlib]System.IDisposable::Dispose()
// which ILSpy decompiled to
catch
{
((IDisposable)r).Dispose();
throw;
}
((IDisposable)r).Dispose();
view raw gistfile1.cs hosted with ❤ by GitHub

No comments :