Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: System.Runtime.CompilerServices.Unsafe 6.1.0 breaking changes: Entry Point was Not Found Exception #184

Closed
moltco opened this issue Nov 21, 2024 · 12 comments

Comments

@moltco
Copy link

moltco commented Nov 21, 2024

I use System.Runtime.CompilerServices.Unsafe 6.1.0 in a .NET481 solution which uses Usafe.As to cast interface to a concrete type; currently I use VS 2022 17.10 and 17.12 on Win11 Pro.

The code works perfectly/as-exected with System.Runtime.CompilerServices.Unsafe 6.0.0 package.

When I upgrade to 6.1.0 I start getting "Entry Point was Not Found" exception when methods try accessing some of the cast lists (in this particular case it is a Unsafe.As<BindingList> cast).

The issue does not happen when in Debug mode, only when compiled as Release which makes it weirder. It also happens in between an if check and accessing the list. The code roughly goes as:

// myObject implements IMyObject<IMyList>, something like
//  {
//      bool On;
//      BindingList<SomeDataObject> Records; // SomeDataObject implements IMyList
//   }
// the CheckObject method below is then called with concrete objects, eg CheckObject(SuperObject<NewData> that implements the interface
// because of the limitations of c# version I am using I need to do Unsafe.As<> conversion at some point between SuperObject<NewData>
// and the interface
   
private void CheckObject(IMyObject<IMyList> myObject){
   if (myObject?.On) { 
       Debug.WriteLine($"{myObject?.Records is null}, {myObject.Records?.Count}"); // WORKS OK and llogs "false, 5" 
       
       if (myObject?.Records?.Count > 0)  // FAILS HERE on accessing Count with "Entry point not found"
            Debug.WriteLine("We have more than 0 records");
   }
}

The error log shows:

Exception: System.EntryPointNotFoundException: Entry point was not found.
   at System.Collections.Generic.ICollection`1.get_Count()

Sadly my codebase is too large to share or to be able to easily extract code to reproduce issue and share here. I hope since this is a minor version update on a maintenance package that the changes are fairly small and the description above helps in triaging and identifying the root cause of the issue.

@moltco
Copy link
Author

moltco commented Nov 22, 2024

I have found a workaround which works with v6.1 of nuget: if I switch the "Code Optimization" off in Visual Studio/MSBuild for 2 projects then it starts working again.

It feels that dotnet nuget tool also plays part in it as it does not reliably/repeatably update packages, I found some inconsitencies as to when it installs v6.0 vs v6.1. Finally, I am wondering if there were changes in the compiler itself as I also had an unusual intermittent issue in one of the xaml/wpf forms where xaml compiler (all of the sudden) would not 'bind by text' to an event handler (as specified in xaml), but when I assigned the event in the code-behind constructor everything was fine. Needless to say, this code worked/compiled perfectly for years without a glitch, ie this wasn't some new code with bugs.

It feels, after 25 years in production, C#/VS and the .Net stack still have a long way to go to become a reliable/repetitive platform.

@ericstj
Copy link
Member

ericstj commented Nov 25, 2024

Hello @moltco. Thank you for your report of this issue, and I'm sorry you are facing this problem in updating dependencies.

I've seen two causes for this issue before with System.Runtime.CompilerServices.Unsafe.

  1. Mismatched generic type arguments used in conjunction with Unsafe.As EntryPointNotFoundException throwns in action runtime#85990 I'm less inclined to suspect that here if you mentioned it's been working for some time, however in that other case the filer mentioned the problem was persistent.
  2. Missing bindingRedirect. This can happen when the object that is the target of the Unsafe.As cast is loading at a different version than the caller expects. I can imagine it could also be anything in the type closure of the object. Imagine the object's instance somewhere has loaded a type from an assembly version 1, but then the caller does an Unsafe.As cast to a type with assembly version 2. This is allowed, due to unsafe but will throw at the point the runtime tries to access the member using the wrong type -- different assembly versions are effectively different types. I only know about this version of the problem from past observations and suggested solutions which fixed it. @jkotas might be better able to explain why this causes the runtime to throw an EntryPointNotFoundException. Since you just updated assemblies, my hunch is that you are missing a bindingRedirect and it's causing this. You might see different behavior in release or without optimizations due to load order of assemblies and a resolve handler.

For reference, here's the diff in IL between the 6.0 and 6.1 version of this assembly for .NETFramework. I don't see anything on its own that would explain a regression here.

@jkotas
Copy link
Member

jkotas commented Nov 26, 2024

CheckObject method below is then called with concrete objects, eg CheckObject(SuperObject that implements the interface
because of the limitations of c# version I am using I need to do Unsafe.As<> conversion at some point between SuperObject
and the interface

Unsafe.As is just an unsafe performance optimization. Valid uses of Unsafe.As do not allow you to do anything more than a regular safe cast.

Does your app work fine when you replace Unsafe.As<T>(obj) with a regular cast (T)obj?

@jkotas
Copy link
Member

jkotas commented Nov 26, 2024

@jkotas might be better able to explain why this causes the runtime to throw an EntryPointNotFoundException.

The runtime assumes matching types for things like calling interface methods. If you introduce a type mismatch using invalid use of Unsafe.As, the types won't match anymore that will manifest as EntryPointNotFoundException or some other hard to diagnose crash.

@moltco
Copy link
Author

moltco commented Nov 26, 2024

Hi both, thank you so much for your insights and suggestions.

My code is fairly simple - if memory serves well, the only reason I use Usafe.As is because c# 7 does not allow default interface implementation and it was struggling to cast in a normal (safe) way when there is a generic type involved. I will have to check the code again but from memory it has a base class that provides default interface implementation and then other classes inherit that and override functionality when required.

The normal casts worked in a simplistic example of this but when I introduced generic that is also based on interface with default implementation it could not figure out that these can be cast as well. So when I had something like class Clazz: IFirstType<ISecondType> where... it would struggle to cast this back to interfaces. But the objects are 100% valid and can be cast. I guess what I am trying to say is that in reality I should not require Unsafe.As and that all objects are pretty much known from the start so there should be nothing 'usafe' about these casts. But the standard cast will not work (ie would not cast). I can try to spend more time on this to see if I can somehow avoid Unsafe.As but I recall reading quite a bit on how to do default interface implementation in c#7 and I recal this was the only approach that would work.

bindingRedirects
With regards to bindingRedirects- I don't use them intentionally, only what compiler/VS decides to use as 'automatic binding'. I checked app.config - the only bindingRedirect that is active there appears to be for Tasks

         <assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/> 
         <bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1"/> 
       </dependentAssembly>

I have manually removed the bindingRedirect for Unsafe in the past as I was trying to troubleshoot and wasn't sure if bindingRedirect was actually needed so I was hoping VS will be clever enough to put a 'fresh' redirect if it was needed (but it didn't)

      <!-- <dependentAssembly> -->
        <!-- <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/> -->
        <!-- <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0"/> -->
      <!-- </dependentAssembly> -->

If you suspect bindingRedirects as a possible cause, then it could be part of the toolchain that does not update these 'automatically' as it shoud? Unsure if this is done by VS/MSBuild/Nuget.

EntryPoint issue 85990 [https://github.com/dotnet/runtime/issues/85990]

This issue looks similar to what I am doing in a way but the suggestion of object becoming null half-way through the operation and therefore not being able to cast doesn't sound right to me at least in this case for three reasons: a) everything works if the code runs in 'debug' mode, b) everything runs if code is compiled to Release without 'code optimizations', c) everything works in unit tests as well.

If it was a timing/race issue in my code then it would presentitself at least once in Debug or with 'unoptimized' code. To put it in context, the line where my code breaks is before any real 'work' starts, it is a simple async validation routine that checks that object (thas is Usafe.As cast in the background) has required parameters so it's not as if the object would be disposed or a task completed midway and therefore releasing the object or making it null - only if the compiler optimization decided it could release the object for some reason.

@jkotas
Copy link
Member

jkotas commented Nov 26, 2024

But the standard cast will not work (ie would not cast).

This suggests that you are doing invalid unsafe cast. Could you please share a small working repro for what you are trying to do?

@moltco
Copy link
Author

moltco commented Nov 28, 2024

@jkotas @ericstj

I have tried to create a simplified version of the simplified code here https://github.com/moltco/vs-unsafe I have added some tests and code that illustrate usage and where Entry Point Not Found is thrown. The only difference with the 'real' code is that the code from UseCase1 would be async and called with await from the form before some other work is done, and before the async context is completed.

This code has been created quite a while back and I don't recall everything in detail but, as I mentioned before, without Unsafe.As the compiler was complaining. I recall doing quite a few tests using different c# inheritance approaches which didn't work but I don't recall detail.

In short, I was trying to achieve a flexible design so I can have default or derived implementations of IListManager and IListEntry and be able to reference concrete implementation by the interface (e.g. DoWork(IListManager object)) without having to cast or check types.

Personally, I am not a fan of that code and would be very happy to ditch it if there was a better solution within the constraints of c# v7.

Please let me know if you have any questions or suggestions.

@jkotas
Copy link
Member

jkotas commented Nov 28, 2024

Thank you for sharing a repro.

without Unsafe.As the compiler was complaining

The C# compiler was trying to tell you that there is no situation where the cast can succeed. You can suppress the C# compiler by casting to object first:

            get => (BindingList<IListEntry>)(object)(Records);
            set => Records = (BindingList<TRec>)(object)(value);

This will make the source compile, but then you get an InvalidCastException at runtime:

Unable to cast object of type 'System.ComponentModel.BindingList`1[ExampleProject.ExampleListEntry]' to type 'System.ComponentModel.BindingList`1[ExampleProject.IListEntry]'

Your use of Unsafe.As is invalid. Here is the relevant section from our documentation:

The behavior of Unsafe.As(o) is only well-defined if the typical "safe" casting operation (T)o would have succeeded. Use of this API to circumvent casts that would otherwise have failed is unsupported and could result in runtime instability.

The weird behaviors that you are seeing is a typical manifestation of runtime instability. You may also see weird crashes, hangs or data corruptions in unrelated code.

@jkotas jkotas closed this as completed Nov 28, 2024
@moltco
Copy link
Author

moltco commented Nov 28, 2024

Thanks... Would you know why is the cast considered not possible if the implementation explicitly implements the interface (or inherits implementation)? Or is there another design/pattern that could be used to achieve the same?

@jkotas
Copy link
Member

jkotas commented Nov 28, 2024

In .NET, all implemented interfaces have to be explicitly declared on a type itself or on a parent type.

interface IMyInterface
{
    void M();
}

// This type does not implement IMyInterface. It cannot be made to implement the interface via unsafe cast.
class A1
{
    public void M() { }
}

class Base : IMyInterface
{
    public void M() { }
}

// This type implements IMyInterface via inheritance.
class A2 : Base
{
}

In .NET 5+, we have introduced a new feature to implement interfaces dynamically https://learn.microsoft.com/en-us/samples/dotnet/samples/idynamicinterfacecastable/ . This feature is not available in .NET Framework. It is the only way that I can think of how a type can implement an interface that is not explicitly declared on it.

@moltco
Copy link
Author

moltco commented Nov 29, 2024

I believe all objects in my repro follow the correct usage example above - all classes implement interface either via inheritance or directly, e.g.

   // implements via inheritance
   public class ExampleListManager :
       ListManagerWFO<ExampleListEntry>,        // base class that implements IListManager
       IUndo<ExampleListEntry>,
       IFilterProvider<ExampleListEntry>
    // implements via inheritance and interface directly
    public class ExampleListEntry :
        BaseListEntry,           // inheritance
        IListEntry,                // direct interface implementation
        IFilterable, ISelectable, 
        ISomethingElse, ISomethingElse2

I have spent some more time on the example code and have produced a new brach in the repo - can you please check the regular-cast branch? In this branch I don't use Unsafe.As, I just use a regular cast and this compiles and works to an extent.

The key issue with that code is that .NET is not picking the correct implementation of code in some scenarios, i.e. not picking implementation in the most recent object/ancestor when accessing objects via the interface. Perhaps' that is intended (?) but I can't see why would anyone prefer to access the parent 'by default' instead of the current object.

Note: with Usafe.As cast - this works as expected, so at least we can say that the behaviour between cast and Usafe.As is not consistent?

If you check the regular-cast branch, what I have done (see updated repo): I remove Unsafe.As and introduce casts in public abstract class ListManagerWFO<TRec>

   BindingList<IListEntry> IListManager<IListEntry>.Records
   {
       get => Records as BindingList<IListEntry>;
       set => Records = value as BindingList<TRec>;
   }
   
   // was:
   // BindingList<IListEntry> IListManager<IListEntry>.Records
   // {
   //     get => Unsafe.As<BindingList<IListEntry>>(Records);
   //     set => Records = Unsafe.As<BindingList<TRec>>(value);
   //  }

After this change, I can get some calls to work correctly, e.g. in UseCase1Test.cs, in method DoSomething_v1_ReturnsCorrectStringBuilder I can do this:

var listManager1 = new ExampleListManager();
Assert.AreEqual(3, listManager1.Records?.Count, "Original object's Records count failed");

This works OK - so far, so good.

However, when I pass this same listManager1 object to useCase.DoSomething_v1(IListManager<IListEntry> one, IListManager<IListEntry> two) (note the mehod signature uses interfaces) - this fails as accessing the object via the interface does not pull the implementation from the concrete object passed but from one of the ancestors where there isn't an implementation.

If one puts a breakpoint on that call and inspect the lists, the various implementations of Records are visible, including the correct one from the concrete object passed with 3 records.

I guess, in useCase.DoSomething_v1 I tried to check type and cast conditionally, but whilst the type is correct, after the cast I still don't get the correct Records.Count. But, in DoSomething_v2 I tried an explicit, hard-coded, cast for each call - this works. So If for every call I do a cast such as (List1 as ExampleListManager).Records.Count this will return correct count. But this defies point of having interfaces imho. Check examples in DoSomething_v1 and DoSomething_v2.

The apparent need to explicitly cast objects on every call feels counter to the concept of interfaces; ie what is the benefit of having interfaces if I need to hard-code cast on every line of code? If I had several concrete implementations, I would need to duplicate same code many times to be able to hard-code cast for each new concrete implementation.

Finally, re Dynamic Interfaces: as all objects implement interfaces explicitly or via inheritance, this does not sound to me like use case for Dynamic Interfaces - everything is 'known' from the start and I should not need to explicitly direct the compiler to use the passed object to find implementation of interface, that sounds like something that should be the default behaviour (?)

@jkotas
Copy link
Member

jkotas commented Nov 30, 2024

        // List1 & List2.Records will be null which is not correct

This line override public BindingList<ExampleListEntry> Records { get; set; } = new BindingList<ExampleListEntry>() is only overriding the abstract Record methods. It is not overriding the IListManager<IListEntry>.Records interface implementation. It is why you are seeing null here. It is the expected behavior as far as I can tell.

The interface implementations and virtual method implementations are independent in .NET. In typical C# code, the C# compiler uses public method to implement interfaces for convenience. This behavior can be suppressed, and the virtual method implementations can be made independent on the interface method implementations like you have done in your example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants