16

I have the following code that exhibits a strange problem:

var all = new FeatureService().FindAll();
System.Diagnostics.Debug.Assert(all != null, "FindAll must not return null");
System.Diagnostics.Debug.WriteLine(all.ToString()); // throws NullReferenceException

The signature of the FindAll method is:

public List<FeatureModel> FindAll()

Stepping through the code I have confirmed that the return value from FindAll is not null, and as you can see from the Assert, the "all" variable is not null, yet in the following line it appears to be null.

The issue is not specific to failing when the ToString() method is called. I simplified it down to this reproducible example while trying to trace the root cause.

This may be a clue: in the debugger, the variable "all" appears in the Locals window with a value of "Cannot obtain value of local or argument 'all' as it is not available at this instruction pointer, possibly because it has been optimized away."

I considered trying one of the approaches documented elsewhere for disabling code optimization but this wouldn't really solve the problem since the release version of the code will still be optimized.

I am using Visual Studio 2010 with .NET 4.0.

Any thoughts?

UPDATE: per request, here is the entire method:

protected override List<FeatureModel> GetModels() {
    var all = new FeatureService().FindAll();
    var wr = new WeakReference(all);
    System.Diagnostics.Debug.Assert(all != null, "FindAll must not return null");
    System.Diagnostics.Debug.WriteLine(wr.IsAlive);
    System.Diagnostics.Debug.WriteLine(all.ToString()); // throws NullReferenceException
    return all;
}

As an FYI, the original implementation was simply:

protected override List<FeatureModel> GetModels() {
    return new FeatureService().FindAll();
}

I originally encountered the null exception in the calling method. The code I posted was after tracing the issue for a while.

UPDATE #2: As requested, here is the stack trace from the exception:

 at FeatureCrowd.DomainModel.FeatureSearch.GetModels() in C:\Users\Gary\Documents\Visual Studio 2010\Projects\FeatureCrowd\FeatureCrowd.DomainModel\FeatureSearch.cs:line 32
 at FeatureCrowd.DomainModel.FeatureSearch.CreateIndex() in C:\Users\Gary\Documents\Visual Studio 2010\Projects\FeatureCrowd\FeatureCrowd.DomainModel\FeatureSearch.cs:line 42
 at FeatureCrowd.DomainModel.FeatureService.CreateSearchIndex() in C:\Users\Gary\Documents\Visual Studio 2010\Projects\FeatureCrowd\FeatureCrowd.DomainModel\FeatureService.cs:line 100
 at Website.MvcApplication.BuildLuceneIndexThread(Object sender) in C:\Users\Gary\Documents\Visual Studio 2010\Projects\FeatureCrowd\FeatureCrowd.Website\Global.asax.cs:line 50
 at Website.MvcApplication.Application_Start() in C:\Users\Gary\Documents\Visual Studio 2010\Projects\FeatureCrowd\FeatureCrowd.Website\Global.asax.cs:line 61
John Saunders
  • 160,644
  • 26
  • 247
  • 397
gxclarke
  • 1,953
  • 3
  • 21
  • 42
  • Is that code copied and pasted from your project? If not, can you copy and paste the actual code? – Lasse V. Karlsen Jul 31 '10 at 21:52
  • Yes, that is the actual code I am running and reproducing the problem with. – gxclarke Jul 31 '10 at 21:52
  • Can you post the stack trace from the exception? – Lasse V. Karlsen Jul 31 '10 at 21:53
  • Is this a release-build or a debug-build? – Lasse V. Karlsen Jul 31 '10 at 21:54
  • Have you verified that it indeed crashes on that third line (the second debug line above) and not the next line (which you haven't posted) or somewhere else in that method? – Lasse V. Karlsen Jul 31 '10 at 21:59
  • `FeatureModel` doesn't happen to be a COM object (RCW) or anything else out of the ordinary, by any chance? -- And yes, Lasse's above comment reminded me... Did you consider that the debugger is malfunctioning, and not your code? It happens from time to time. – stakx - no longer contributing Jul 31 '10 at 22:00
  • 1
    If the signature for that method is exactly as posted, the type of objects in play has nothing to do with it. `List` does not implement `ToString` which means it falls back to `Object.ToString` that should just return the full type name of the object. Since this fails with a `NullReferenceException`, I believe the variable contains `null` after all. – Lasse V. Karlsen Jul 31 '10 at 22:03
  • Good point @Lasse- a quick look using reflector confirms this. – RichardOD Jul 31 '10 at 22:05
  • 1
    Of course, `List` isn't sealed, so even though the method signature says `List`, it might in fact return a custom descendant class in the project, which does implement `ToString`. I doubt it though, that would probably have been relevant information that Gary would've posted. – Lasse V. Karlsen Jul 31 '10 at 22:07
  • 1
    @Lasse, well `List` *can* still be subclassed - it isn't `sealed`. Seems unlikely, *but* I can imagine some frameworks doing this. – Marc Gravell Jul 31 '10 at 22:08
  • I've updated the question with the full code and the stack trace. It is a debug build. FeatureModel is a POCO. It has no methods, just properties. I don't think the debugger is malfunctioning. I noticed this problem originally at run-time before stepping through with the debugger. I am using System.Collections.Generic.List directly (not a descendant) and I have not added any extension methods. – gxclarke Jul 31 '10 at 22:36
  • The lines you've posted in the question, can you annotate them with the name of the file they're in and their line numbers? – Lasse V. Karlsen Jul 31 '10 at 22:36
  • Can you do a test for me? Can you see what `bool wtf = all.GetType() == typeof(List)` ? I'm trying to see if there is some obscure subclass here with a borked `ToString`... – Marc Gravell Jul 31 '10 at 22:50
  • @marc: I can't right now because Lasse if remoted into my box (what a great guy!). But the issue didn't originally manifest itself on the ToString. I added that as a simple case for reproducing the issue. – gxclarke Jul 31 '10 at 23:12
  • @marc, take a look at my updated answer, check the screencast at the bottom as well, this is really really odd. – Lasse V. Karlsen Aug 01 '10 at 00:16
  • I added a new answer instead, rolled back the original ones, comments got taken out of context. We can see what happens and optionally just deleted the old one if necessary. – Lasse V. Karlsen Aug 01 '10 at 00:20
  • Anyone knows how to nudge Eric Lippert? I'd bet he could give us some useful information. – Lasse V. Karlsen Aug 01 '10 at 00:30
  • @Lasse: Eric is on holiday at the moment, but I can nudge Mads. Can you extract this into a short but complete program? I'd like to investigate it a bit more myself before passing it on. – Jon Skeet Aug 01 '10 at 06:59
  • I'll attempt to reduce it to a simple example. – Lasse V. Karlsen Aug 01 '10 at 10:26
  • 1
    I've managed to reduce it to a simple solution file containing 3 projects (it doesn't work with just 1 project), I'll post the reproduction and ask people to verify it in a new question, linking back to this one. – Lasse V. Karlsen Aug 01 '10 at 13:07
  • I've posted a question with request for verification, after managing to reduce the problem to a few simple projects and files. It is available here: http://stackoverflow.com/questions/3382231/possible-c-4-0-compiler-error-can-others-verify – Lasse V. Karlsen Aug 01 '10 at 13:43

3 Answers3

23

After looking over the code over TeamViewer, and finally downloading, compiling and running the code on my own machine, I believe this is a case of compiler error in C# 4.0.


I've posted a question with request for verification, after managing to reduce the problem to a few simple projects and files. It is available here: Possible C# 4.0 compiler error, can others verify?


The likely culprit is not this method:

protected override List<FeatureModel> GetModels() {
    var fs = new FeatureService();
    var all = fs.FindAll();
    var wr = new WeakReference(all);
    System.Diagnostics.Debug.Assert(all != null, "FindAll must not return null");
    System.Diagnostics.Debug.WriteLine(wr.IsAlive);
    System.Diagnostics.Debug.WriteLine(all.ToString()); // throws NullReferenceException
    return all;
}

But the method it calls, FeatureService.FindAll:

public List<FeatureModel> FindAll() {
    string key = Cache.GetQueryKey("FindAll");
    var value = Cache.Load<List<FeatureModel>>(key);
    if (value == null) {
        var query = Context.Features;
        value = query.ToList().Select(x => Map(x)).ToList();
        var policy = Cache.GetDefaultCacheItemPolicy(value.Select(x => Cache.GetObjectKey(x.Id.ToString())), true);
        Cache.Store(key, value, policy);
    }
    value = new List<FeatureModel>();
    return value;
}

If I changed the call in GetModels from this:

var all = fs.FindAll();

to this:

var all = fs.FindAll().ToList(); // remember, it already returned a list

then the program crashes with a ExecutionEngineException.


After doing a clean, a build, and then looking at the compiled code through Reflector, here's how the output looks (scroll to the bottom of the code for the important part):

public List<FeatureModel> FindAll()
{
    List<FeatureModel> value;
    Func<FeatureModel, string> CS$<>9__CachedAnonymousMethodDelegate6 = null;
    List<FeatureModel> CS$<>9__CachedAnonymousMethodDelegate7 = null;
    string key = base.Cache.GetQueryKey("FindAll");
    if (base.Cache.Load<List<FeatureModel>>(key) == null)
    {
        if (CS$<>9__CachedAnonymousMethodDelegate6 == null)
        {
            CS$<>9__CachedAnonymousMethodDelegate6 = (Func<FeatureModel, string>) delegate (Feature x) {
                return this.Map(x);
            };
        }
        value = base.Context.Features.ToList<Feature>().Select<Feature, FeatureModel>(((Func<Feature, FeatureModel>) CS$<>9__CachedAnonymousMethodDelegate6)).ToList<FeatureModel>();
        if (CS$<>9__CachedAnonymousMethodDelegate7 == null)
        {
            CS$<>9__CachedAnonymousMethodDelegate7 = (List<FeatureModel>) delegate (FeatureModel x) {
                return base.Cache.GetObjectKey(x.Id.ToString());
            };
        }
        Func<Feature, FeatureModel> policy = (Func<Feature, FeatureModel>) base.Cache.GetDefaultCacheItemPolicy(value.Select<FeatureModel, string>((Func<FeatureModel, string>) CS$<>9__CachedAnonymousMethodDelegate7), true);
        base.Cache.Store<List<FeatureModel>>(key, value, (CacheItemPolicy) policy);
    }
    value = new List<FeatureModel>();
    bool CS$1$0000 = (bool) value;
    return (List<FeatureModel>) CS$1$0000;
}

Notice the 3 last lines of the method, here's what they look like in the code:

value = new List<FeatureModel>();
return value;

here's what Reflector says:

value = new List<FeatureModel>();
bool CS$1$0000 = (bool) value;
return (List<FeatureModel>) CS$1$0000;

It creates the list, then casts it to a boolean, then casts it back to a list and returns it. Most likely this causes a stack problem.

Here's the same method, in IL (still through Reflector), I've stripped away most of the code:

.method public hidebysig instance class [mscorlib]System.Collections.Generic.List`1<class FeatureCrowd.DomainModel.FeatureModel> FindAll() cil managed
{
    .maxstack 5
    .locals init (
        [0] string key,
        [1] class [mscorlib]System.Collections.Generic.List`1<class FeatureCrowd.DomainModel.FeatureModel> 'value',
        [2] class [System.Data.Entity]System.Data.Objects.ObjectSet`1<class FeatureCrowd.DomainModel.Feature> query,
        [3] class [mscorlib]System.Func`2<class FeatureCrowd.DomainModel.Feature, class FeatureCrowd.DomainModel.FeatureModel> policy,
        [4] class [mscorlib]System.Func`2<class FeatureCrowd.DomainModel.FeatureModel, string> CS$<>9__CachedAnonymousMethodDelegate6,
        [5] class [mscorlib]System.Collections.Generic.List`1<class FeatureCrowd.DomainModel.FeatureModel> CS$<>9__CachedAnonymousMethodDelegate7,
        [6] bool CS$1$0000,
        [7] char CS$4$0001)
    ...
    L_009f: newobj instance void [mscorlib]System.Collections.Generic.List`1<class FeatureCrowd.DomainModel.FeatureModel>::.ctor()
    L_00a4: stloc.1 
    L_00a5: ldloc.1 
    L_00a6: stloc.s CS$1$0000
    L_00a8: br.s L_00aa
    L_00aa: ldloc.s CS$1$0000
    L_00ac: ret 
}

Here's a screencast showing the debug session, if you just want the Reflector output, skip to about 2:50.

Community
  • 1
  • 1
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • 3
    You went *far* beyond reasonable effort here - excellent job. – Marc Gravell Aug 01 '10 at 08:34
  • 3
    I've posted a question with request for verification, after managing to reduce the problem to a few simple projects and files. It is available here: http://stackoverflow.com/questions/3382231/possible-c-4-0-compiler-error-can-others-verify – Lasse V. Karlsen Aug 01 '10 at 13:41
  • I _think_ Im seeing the same thing now, suddenly, in a VS2017 / .NET Framework 4.7 / C# 7.0 project. Not sure, but I am getting a NullRef on a line with `HashSet assList = new HashSet();` ... – Ted Feb 05 '19 at 18:01
9

After Lasse discovered that the FindAll method was generating the wrong IL, I then came across another method that was also generating the wrong IL -- I also found the root cause and resolution.

The relevant line in the second method is:

var policy = Cache.GetDefaultCacheItemPolicy(dependentKeys, true);

Cache is my own object. The GetDefaultCacheItemPolicy method returns a System.Runtime.Caching.CacheItemPolicy object. The generated IL, however, looked like this:

Func<Feature, FeatureModel> policy = (Func<Feature, FeatureModel>) base.Cache.GetDefaultCacheItemPolicy(dependentKeys, true);

There are two projects in play here. The methods that are generating the wrong IL are in one project called DomainModel, and the Cache object is in a Utilities project, which is referenced by the first. The second project contains a reference to System.Runtime.Caching but the first does not.

The fix was to add a reference to System.Runtime.Caching to the first project. Now the generated IL looks correct:

CacheItemPolicy policy = base.Cache.GetDefaultCacheItemPolicy(dependentKeys, true);

The first method (that Lasse posted about in his answer) now also generates proper IL.

Hooray!

gxclarke
  • 1,953
  • 3
  • 21
  • 42
2

Left for posterity, this is not the problem.

See my new answer.


Here's what I believe.

Contrary to what you're saying, I believe the program is not in fact crashing in any of the lines posted, but instead crashes on one of the lines following them, which you haven't posted.

The reason I believe this is that I also believe you're doing a Release-build, in which case both Debug lines will be removed, since they're tagged with a [Conditional("DEBUG")] attribute.

The clue here is that the all variable has been optimized away, and this should only happen during a Release-build, not a Debug-build.

In other words, I believe the all variable is actuall null after all, and the Debug lines aren't executed, because they're not compiled into the assembly. The debugger is dutifully reporting that the all variable no longer exists.

Note that all of this should be easy to test. Just place a breakpoint on the first of the two Debug-lines that you've posted. If the breakpoint is hit, my hypothesis is most likely wrong. If it doesn't (and I'm going to guess that the breakpoint symbol shows up as a hollow circle at runtime), then those lines aren't compiled into the assembly.

Community
  • 1
  • 1
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • It's a good theory, but I don't think it is the case. I have been stepping through the code while attempting to diagnose this and I see the WriteLine output being produced. I have my configuration set to a debug build. I also reset my entire IDE to default settings while diagnosing just in case I had customized any of the debug values in the past. I also verified that the pdb file is produced, and has the same time stamp as the assembly (and the time stamp matches my last build time). – gxclarke Jul 31 '10 at 22:21
  • Is the code proprietary? If not, would you be willing to upload the code, or a compiled version of it, somewhere? – Lasse V. Karlsen Jul 31 '10 at 22:24
  • @Gary, you can make sure by using Trace.Assert() – H H Jul 31 '10 at 22:24
  • 1
    If not, a TeamViewer session could also be set up, so I can see your screen. – Lasse V. Karlsen Jul 31 '10 at 22:34
  • @Lasse: we can do TeamViewer (and thanks for offering to help!). Do you use google talk or another IM client, so I can give you the login info? My email address (and IM) is gxclarke at gmail dot com. – gxclarke Jul 31 '10 at 22:41
  • Live Messenger: lasse@vkarlsen.no or google talk: lassevk@gmail.com – Lasse V. Karlsen Jul 31 '10 at 22:43
  • Added new answer after debugging session here: http://stackoverflow.com/questions/3379894/c-code-seems-to-get-optimized-in-an-invalid-way-such-that-an-object-value-become/3380268#3380268 – Lasse V. Karlsen Aug 01 '10 at 00:39