.NET decompiler distinction between "using" and "try...finally"
Asked Answered
C

2

14

Given the following C# code in which the Dispose method is called in two different ways:

class Disposable : IDisposable
{
    public void Dispose()
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var disposable1 = new Disposable())
        {
            Console.WriteLine("using");
        }

        var disposable2 = new Disposable();
        try
        {
            Console.WriteLine("try");
        }
        finally
        {
            if (disposable2 != null)
                ((IDisposable)disposable2).Dispose();
        }
    }
}

Once compiled using release configuration then disassembled with ildasm, the MSIL looks like this:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       57 (0x39)
  .maxstack  1
  .locals init ([0] class ConsoleApplication9.Disposable disposable2,
           [1] class ConsoleApplication9.Disposable disposable1)
  IL_0000:  newobj     instance void ConsoleApplication9.Disposable::.ctor()
  IL_0005:  stloc.1
  .try
  {
    IL_0006:  ldstr      "using"
    IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0010:  leave.s    IL_001c
  }  // end .try
  finally
  {
    IL_0012:  ldloc.1
    IL_0013:  brfalse.s  IL_001b
    IL_0015:  ldloc.1
    IL_0016:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_001b:  endfinally
  }  // end handler
  IL_001c:  newobj     instance void ConsoleApplication9.Disposable::.ctor()
  IL_0021:  stloc.0
  .try
  {
    IL_0022:  ldstr      "try"
    IL_0027:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_002c:  leave.s    IL_0038
  }  // end .try
  finally
  {
    IL_002e:  ldloc.0
    IL_002f:  brfalse.s  IL_0037
    IL_0031:  ldloc.0
    IL_0032:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0037:  endfinally
  }  // end handler
  IL_0038:  ret
} // end of method Program::Main

How does a .NET decompiler such as DotPeek or JustDecompile make the difference between using and try...finally?

Churchwell answered 13/4, 2018 at 11:11 Comment(6)
It guesses based on the pattern, frankly :) If you manually write something that happens to compile exactly to what the compiler-generated version would compile to, it will guess wrong. Often there isn't a risk of that, as many compiler expansions involve unpronounceable names (names that aren't legal in regular C#) or non-existent keywords (the one that decompilers often show as memberof, for example - which can be expressed in IL but not C#, so it it sees that: it knows you didn't write it by hand)Somatoplasm
That's what I thought but I tested it and as you can see, while both look identical in MSIL, DotPeek, for exemple, still manages to make the difference...Churchwell
are you sure it doesn't have access to the PDB?Somatoplasm
You are right, it uses more than the assembly itself.Churchwell
fair enough, but in that case it isn't really actually decompiling, in most cases :)Somatoplasm
That is not how the C# compiler generates the code for using. It uses an unnamed variable to store the return value of the new expression, then assigns it to disposable2. Which ensures that your program cannot accidentally re-assign disposable2 and make the Dispose() call fail. That coding pattern is enough to give the decompiler sufficient hints.Piemonte
R
8

It doesn't make a difference actually. As Mark says in comments - if you write the same code as compiler would generate for using - decompiler won't be able to make a difference.

However, many decompilers, including DotPeek, can actually use debugging symbols (.pdb) files to locate the actual source code, and then use actual source code, so that no decompilation happens at all. Also, compiling in Debug mode might affect the pattern too (that is - your attempt to mimic using statement might have different resulting IL in debug vs release compilations).

To prevent DotPeek from using your real source code files, go to Tools > Options > Decompiler and uncheck "Use sources from symbol files when available". Then compile your code in Release and observe that DotPeek will decompile both statements as using.

Ruvalcaba answered 13/4, 2018 at 11:21 Comment(0)
W
4

How does a .NET decompiler such as DotPeek or JustDecompile make the difference between using and try...finally?

Decompilers largely work on pattern matching. Generally, the IL is translated into the simplest equivalent representation possible in the target language (in this case, C#). That code model is then passed through a series of transformations that attempt to match up code sequences against well-known patterns. With a debug build of ILSpy, you can actually view the output at different stages of this pipeline.

A decompiler's pipeline may include transforms like a loop rewriter. A loop rewriter might reconstitute for loops by looking for while loops that are preceded by a variable initializers and which also contain common iteration statements prior to each back edge. When such a loop is detected, it gets rewritten as a more concise for loop. It doesn't know that the original code actually contained a for loop; it's simply trying to find the most concise way to represent the code while maintaining correctness.

In a similar way, a using rewriter would look for try/finally blocks where the finally contains a simple null check and Dispose() call, then rewrite those as using blocks, which are more concise while still being correct according to the language spec. The decompiler doesn't know that the code contained a using block, but since almost no one uses the explicit try/finally form, the results tend to be consistent with the original source.

Wrathful answered 13/4, 2018 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.