Search code examples
c++-clivisual-studio-2015cil

NullReferenceException in VS2015 C++/CLI Release Build


I'm getting "System.NullReferenceException: Object reference not set to an instance of an object." on my release build. I have created a sample application that imitates what's there in my production code.

void Abc::LogService::Log(String^ message)
{
    try
    {    
        int ret = DoProcessing(message);
        Exception^ ex;
        if (ret == 0)
        {
            ex = gcnew ArgumentException("Processing done.");
        }
        else
        {
            ex = gcnew ArgumentNullException("message", "Null args");
        }
        throw ex;
    }
    finally
    {
        //do someother thing.
    }
}

With the above code, it reports the exception line to be: at Abc.LogService.Log(String message) in logservice.cpp:line 19 which corresponds to the throw ex; statement in the code.

The MSIL in the release build for this function looks as:

.method public hidebysig instance void  Log(string message) cil managed
{
  // Code size       46 (0x2e)
  .maxstack  4
  .locals ([0] class [mscorlib]System.Exception V_0,
           [1] class [mscorlib]System.Exception ex)
  .try
  {
    IL_0000:  ldarg.0
    IL_0001:  ldarg.1
    IL_0002:  call       instance int32 Abc.LogService::DoProcessing(string)
    IL_0007:  ldnull
    IL_0008:  stloc.1
    IL_0009:  brtrue.s   IL_0018
    IL_000b:  ldstr      "Processing done."
    IL_0010:  newobj     instance void [mscorlib]System.ArgumentException::.ctor(string)
    IL_0015:  stloc.0
    IL_0016:  br.s       IL_0028
    IL_0018:  ldstr      "message"
    IL_001d:  ldstr      "Null args"
    IL_0022:  newobj     instance void [mscorlib]System.ArgumentNullException::.ctor(string,
                                                                                     string)
    IL_0027:  stloc.0
    IL_0028:  ldloc.1
    IL_0029:  throw
    IL_002a:  leave.s    IL_002d
  }  // end .try
  finally
  {
    IL_002c:  endfinally
  }  // end handler
  IL_002d:  ret
} // end of method LogService::Log

From the MSIL code, it shows that at statement IL_0028, it loads up a null value and calls the throw in the subsequent statement. The strange part is this happens only if I have the try-finally block. Debug build of the above code works fine.

Does this sound as a bug in VS2015 v140 toolkit?


Solution

  • Yes, this is an optimizer bug. Pretty unusual, first one I've seen for C++/CLI, a language where the jitter is supposed to do the heavy lifting. It appears to be tripped by declaring the ex variable inside the try-block, getting it to choke on the initialization guarantee. Looks like a flow analysis bug.

    Short from compiling with /Od, one workaround is to move the variable out of the try block

    void Log(String^ message) {
        Exception^ ex;
        try {
           // etc...
    }
    

    Also producing much better MSIL, completely eliminating the variable:

    .method public hidebysig instance void  Log(string message) cil managed
    {
      // Code size       41 (0x29)
      .maxstack  4
      .try
      {
        IL_0000:  ldarg.0
        IL_0001:  ldarg.1
        IL_0002:  call       instance int32 Test::DoProcessing(string)
        IL_0007:  brtrue.s   IL_0015
        IL_0009:  ldstr      "Processing done."
        IL_000e:  newobj     instance void [mscorlib]System.ArgumentException::.ctor(string)
        IL_0013:  br.s       IL_0024
        IL_0015:  ldstr      "message"
        IL_001a:  ldstr      "Null args"
        IL_001f:  newobj     instance void [mscorlib]System.ArgumentNullException::.ctor(string,
                                                                                         string)
        IL_0024:  throw
        IL_0025:  leave.s    IL_0028
      }  // end .try
      finally
      {
        IL_0027:  endfinally
      }  // end handler
      IL_0028:  ret
    } // end of method Test::Log
    

    Optimizer bugs suck, you can report it at connect.microsoft.com