nanoframework / Home

:house: The landing page for .NET nanoFramework repositories.
https://www.nanoframework.net
MIT License
866 stars 78 forks source link

Extension Methods and delegates problem #1326

Open MateuszKlatecki opened 1 year ago

MateuszKlatecki commented 1 year ago

Target name(s)

ALL

Firmware version

No response

Was working before? On which version?

No response

Device capabilities

No response

Description

For a long time, we have been seeing strange problems at random times, the source of which we could not identify. Weird assertions in Interpreter, or HardFaults in Garbage Collector during ComputeReachabilityGraph, data corruption causing strange exceptions. Finally managed to extract a fairly short code that causes the problem:

using System;
using System.Threading;

namespace ExtensionMethodAndDelegateProblem
{
    public class Program
    {
        public static void Main()
        {
            Thread.Sleep(5000);

            MyCollection collection = new MyCollection();
            ContainsElements IsNotEmpty = collection.HasElements;
            while (true)
            {
                if (IsNotEmpty())
                {
                    Console.WriteLine($"collection has {collection.Count} elements");
                }
                else
                {
                    Console.WriteLine("collection is empty");
                }
            }
        }
    }

    public delegate bool ContainsElements();

    public class MyCollection
    {
        public int Count => 42;
    }

    public static class MyCollectionExtensions
    {
        public static bool HasElements(this MyCollection collection)
        {
            var upperLimit = collection == null ? 0 : collection.Count;
            return upperLimit > 0;
        }
    }
}

Running this on a real device looks like the first 3 iterations will work and the next one throws an exception: image

Trying to run it in a Virtual nanoDevice throws a Stack Underflow exception (certainly here: https://github.com/nanoframework/nf-interpreter/blob/main/src/CLR/Core/CLR_RT_StackFrame.cpp#L255) image.

Adding minor modifications to this code (e.g. adding a thread that stresses GC a lot, or modifying the MyCollection class) and running on real device, causes Hardfaults in GC:
HF in GC

or e.g. asserts in the interpreter. ldarg0 assign_assert

How to reproduce

Run code provided in description

Expected behaviour

No response

Screenshots

No response

Aditional information

No response

MateuszKlatecki commented 1 year ago

I found that if we don't attach an extension method directly to the delegate but through an indirect static method, everything seems to work fine. Working code:

public class Program
    {
        static MyCollection collection;
        public static void Main()
        {
            Thread.Sleep(5000);

            collection = new MyCollection();
            ContainsElements IsNotEmpty = Wrapper;
            while (true)
            {
                if (IsNotEmpty())
                {
                    Console.WriteLine($"collection has {collection.Count} elements");
                }
                else
                {
                    Console.WriteLine("collection is empty");
                }
            }
        }

        static bool Wrapper()
        {
            return collection.HasElements();
        }
    }

This may suggest a problem already at the time of assigning the method to the delegate. At the same time, I'm afraid that the fact that this code with a workaround does not cause problems right away, but it may explode in other situations.

josesimoes commented 1 year ago

Interesting... that suggests something related with static vs non static methods... maybe...

MateuszKlatecki commented 1 year ago

but extension methods are also static methods that can be called as if they were instance methods of a different type. For example, static void M(this string s) can be called from any string, such as "abc".M(). Maybe that's where the problem is. When assigning a method to a delegate, it really shouldn't take any parameters, but the extension method takes one parameter.

For example bellow code

public class MyClass {

}

public static class MyClassExt
{
    public static bool DoSomething(this MyClass c)
    {
        return false;
    }
}

public static class Program
{
    public static void Main()
    {
        var c = new MyClass();

        c.DoSomething();
    }
}

is equivalent to

public class MyClass
{
}

public static class MyClassExt
{
    public static bool DoSomething(MyClass c)
    {
        return false;
    }
}

public static class Program
{
    public static void Main()
    {
        MyClass c = new MyClass();
        MyClassExt.DoSomething(c);
    }
}