godot icon indicating copy to clipboard operation
godot copied to clipboard

.NET: Failed to unload assemblies. Please check <this issue> for more information.

Open RedworkDE opened this issue 1 year ago • 91 comments

Godot version

Any 4.x version

Issue description

Assembly reloading can fail for various reasons, usually because a library used in tools code is not compatible with assembly unloading.

After unloading has failed, C# scripts will be unavailable until the editor is restarted (in rare cases it may be possible to complete the unloading by re-building assemblies after some time).

If assembly unloading fails for your project check Microsoft's troubleshooting instructions and ensure that you are not using one of the libraries known to be incompatible:

  • https://github.com/JamesNK/Newtonsoft.Json/issues/2414
  • https://github.com/dotnet/runtime/issues/65323

If you know of additional libraries that cause issues, please leave a comment. If your code doesn't use any libraries, doesn't violate any guidelines and you believe unloading is blocked by godot, please open a new issue. Already reported causes are:

  • https://github.com/godotengine/godot/issues/79519
  • https://github.com/godotengine/godot/issues/80175 [^1]
  • https://github.com/godotengine/godot/issues/81903
  • https://github.com/godotengine/godot/pull/90837 [^1]

Minimal reproduction project & Cleanup example

using Godot;
using System;

[Tool]
public partial class UnloadingIssuesSample : Node
{
    public override void _Ready()
    {
        // block unloading with a strong handle
        var handle = System.Runtime.InteropServices.GCHandle.Alloc(this);

        // register cleanup code to prevent unloading issues
        System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(System.Reflection.Assembly.GetExecutingAssembly()).Unloading += alc =>
        {
            // handle.Free();
        };
    }
}

[^1]: Bugsquad edit

RedworkDE avatar Jun 21 '23 09:06 RedworkDE

Something was wrong with one of my [Tool] class in code after upgrade from 4.0 to 4.1 ("This class does not inherit from Node") and got this error. I just changed it to Node3D and back, and then the "cache" bug got fixed magicly?

wp2000x avatar Jul 06 '23 22:07 wp2000x

System.Text.Json also has this issue where serializing your classes will be held internally by the Json library.

The workaround for this using this library is also copied below:

var assembly = typeof(JsonSerializerOptions).Assembly;
var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler");
var clearCacheMethod = updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public);
clearCacheMethod?.Invoke(null, new object?[] { null }); 

As far as I'm aware Godot doesn't provide an event for signalling when your dll/plugin is about to be unloaded, so you essentially need to call the above solution after every serialization/deserialization.

Quinn-L avatar Jul 07 '23 04:07 Quinn-L

As far as I'm aware Godot doesn't provide an event for signalling when your dll/plugin is about to be unloaded, so you essentially need to call the above solution after every serialization/deserialization.

You can use the normal AssemblyLoadContext.Unloading event to trigger such cleanup code. I unfolded the code example in the initial post demonstrating its usage.

RedworkDE avatar Jul 07 '23 06:07 RedworkDE

You can use the normal AssemblyLoadContext.Unloading event to trigger such cleanup code. I unfolded the code example in the initial post demonstrating its usage.

Thanks for the info, I somehow missed that.

As an FYI, I played around with your solution trying it on a EditorPlugin derived class, but didn't find it reliable in on _Ready. As far as I could tell (or maybe I tested it wrong), rebuilding would reload the C# project but not re-invoke _Ready causing the error on subsequent rebuilds since the 'new' Unloading event is not registered (I assume because the node itself is not removed and re-added).

My solution was to place the code within a [ModuleInitializer] method, ie:

internal class AppModule
{
    [System.Runtime.CompilerServices.ModuleInitializer]
    public static void Initialize()
    {
        System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(System.Reflection.Assembly.GetExecutingAssembly()).Unloading += alc =>
        {
            var assembly = typeof(JsonSerializerOptions).Assembly;
            var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler");
            var clearCacheMethod = updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public);
            clearCacheMethod?.Invoke(null, new object?[] { null });

            // Unload any other unloadable references
        };
    }
}

This ensures it is not dependent on any node(s) and is always registered only once, and re-registered upon reloading the assembly.

Quinn-L avatar Jul 07 '23 08:07 Quinn-L

I am not using Json.NET or System.Text.Json, and I am experiencing this issue. In the worst case, it results in data loss as serialized properties from C# Node scripts are reset to default.

The only library I am referencing is one I have authored. My .csproj file looks like this:

<Project Sdk="Godot.NET.Sdk/4.1.0">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Lib\Hawthorn\Hawthorn\Hawthorn.csproj" />
  </ItemGroup>
</Project>

That project's csproj is dead simple, with no other project references.

@RedworkDE, you mentioned "does not violate any guidelines"; what are these guidelines? Is there a documentation page? Something else? Thanks.

taylorhadden avatar Jul 14 '23 17:07 taylorhadden

It refers to Microsoft's docs page that was linked a bit further up: https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability#troubleshoot-unloadability-issues

Also all these really also have to apply to the library, but for most libraries there isn't too much you can do about it (except not use the library)

RedworkDE avatar Jul 14 '23 19:07 RedworkDE

I am hitting this issue and I'm fairly confident it is not my (direct) fault.

I can reliably cause this error to appear in the console by running a C# rebuild while an offending Scene is loaded in the editor:

modules/mono/glue/runtime_interop.cpp:1324 - System.ArgumentException: An item with the same key has already been added. Key: BehaviorActorView`1[LeshonkiView]
     at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
     at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
     at Godot.Bridge.ScriptManagerBridge.ScriptTypeBiMap.Add(IntPtr scriptPtr, Type scriptType) in /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs:line 23
     at Godot.Bridge.ScriptManagerBridge.TryReloadRegisteredScriptWithClass(IntPtr scriptPtr) in /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 579

If I build again, I will get the "Failed to unload assemblies" error. If I close the offending scene and run the rebuild again, I rebuild without any issues and everything is fine.

I've been trying to figure out precisely what's needed to get this problem to repro. It seems connected to the new [GlobalClass] attribute and resources, but not necessarily on a one-to-one basis.

taylorhadden avatar Jul 15 '23 01:07 taylorhadden

You also seem to be using generic Nodes which also cause issues in some cases, see https://github.com/godotengine/godot/pull/79007

RedworkDE avatar Jul 15 '23 06:07 RedworkDE

Yeah, it seems that's the root cause. That would be very unfortunate if true. I have a micro reproduction project and I'll write up another issue. It runs fine but for the occasional compilation issue.

taylorhadden avatar Jul 15 '23 12:07 taylorhadden

I have created that reproduction project: #79519

taylorhadden avatar Jul 15 '23 21:07 taylorhadden

I don't know what is going on or why but I started getting this error today in Godot 4.1.1 .NET version. It's completely blocking me from doing any development. I've spent all day trying to hunt down weird behaviors. It seems that when this issue happens it's corrupting properties in the editor - or possibly inherited scenes are breaking. Restarting the editor is good for one run. But I keep constantly having to restart. This is 100% blocking all game development for me.

Pilvinen avatar Jul 23 '23 14:07 Pilvinen

Slight update. I'm just guessing here, but I suspect the issue might be somehow related to the new GlobalClass. That's all I can think of. I got rid of the errors by deleting a node which had either been added via GlobalClass node or it was a regular node with the same script attached to it. Either way, I deleted it and the error was gone. I added it back in as GlobalClass node - the error stayed gone. Maybe this is helpful or maybe it is not, but I thought I'd mention it. It was a nightmare to track down.

Pilvinen avatar Jul 23 '23 18:07 Pilvinen

I can absolutely understand that. The problems since 4.1 are unfortunately unexpected, varied and sometimes difficult to analyse. The new GlobalClass attribute does not work correctly for me so far, so I will not use this great feature for the time being. But I still got the error message above.

As described above, some problems are related to tool code whose assemblies cannot be unloaded during build. This certainly has side effects on GlobalClass classes. In principle, this is a problem of .net and poorly implemented libs (which cannot be unloaded) and not an engine bug. But it is important that a solution is found, because it makes the use of .net, especially as tool-code in the editor, much more difficult.

The simplest workaround is probably to close the problematic scene tabs (mostly [tool] code) before each build. Of course, this is annoying and tedious.

Otherwise, try the code listed at the top. Surprisingly, the following worked for me:

`[Tool]
...
            AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()).Unloading += alc =>
            {

               // trigger unload 
                AssemblyLoadContext.GetLoadContext(typeof(JsonSerializerOptions).Assembly).Unload();
                AssemblyLoadContext.GetLoadContext(typeof(DataContractJsonSerializer).Assembly).Unload();

                // Unload any other unloadable references					

            };   

With this code, the unload works after an incorrect attempt.

swissdude15 avatar Jul 23 '23 20:07 swissdude15

Yeah, it seems that's the root cause. That would be very unfortunate if true. I have a micro reproduction project and I'll write up another issue. It runs fine but for the occasional compilation issue.

I added generic nodes today, and I started getting this issue.

Ryan-000 avatar Jul 24 '23 01:07 Ryan-000

I'm getting this issue in 4.1, and I'm not using any third-party libraries, nor am I using generic nodes, nor am I using the [GlobalClass] or [Tool] attributes. It just seems to be happening at random whenever I recompile.

Well, OK. I used to have a C# script that had both of those attributes, but I've since rewritten it in GDScript, so it shouldn't be relevant anymore...right? Is there any chance that the ghost of the C# version of that script still lives on? Maybe in some cache somewhere?

ashelleyPurdue avatar Jul 27 '23 21:07 ashelleyPurdue

Also happens whenever I enable a C# editor plugin, and only goes away once I disable the plugin and restart the editor.

Ryan-000 avatar Jul 29 '23 22:07 Ryan-000

The new [GlobalClass] attribute also causes this problem because these classes make it act like [Tool] code. The basic problem probably lies in the System.Collection.Generic class. :

  modules/mono/glue/runtime_interop.cpp:1324 - System.ArgumentException: An item with the same key has already been added. Key: Game.WeaponAbilityData
     at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
     at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
     at Godot.Bridge.ScriptManagerBridge.ScriptTypeBiMap.Add(IntPtr scriptPtr, Type scriptType) in /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs:line 23
     at Godot.Bridge.ScriptManagerBridge.AddScriptBridge(IntPtr scriptPtr, godot_string* scriptPath) in /root/godot/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs:line 419

I will try today the method mentioned above by Quinn-L and put the unload handler in an EditorPlugin. But I have rather little hope that it will work cleanly.

swissdude15 avatar Aug 09 '23 17:08 swissdude15

I will try today the method mentioned above by Quinn-L and put the unload handler in an EditorPlugin. But I have rather little hope that it will work cleanly.

Nope. We decided not to waste so much time with this problem and rewrite certain parts of the code in GDScript to work around the trouble spots with C#. We are not happy with this. This will especially affect [GlobalClass] and [Tool] code, probably also System.Collection.Generic will cause trouble.

swissdude15 avatar Aug 10 '23 08:08 swissdude15

I think I figured out what was causing it for me. It wasn't editor plugins, global classes, or tool scripts. No, it's because I had more than one Node class in a single script file.

namespace FlightSpeedway
{
    public partial class Player : CharacterBody3D
    {
        private PlayerState _currentState;
        private Vector3 _spawnPoint;
        private Vector3 _spawnRotation;

        public void ChangeState<TState>() where TState : PlayerState
        {
            GD.Print($"Changing state to {typeof(TState).Name}");
            _currentState?.OnStateExited();

            foreach (var state in States())
            {
                state.ProcessMode = ProcessModeEnum.Disabled;
            }

            _currentState = States().First(s => s is TState);
            _currentState.ProcessMode = ProcessModeEnum.Inherit;
            _currentState.OnStateEntered();
        }

        private IEnumerable<PlayerState> States()
        {
            for (int i = 0; i < GetChildCount(); i++)
            {
                var child = GetChild<Node>(i);

                if (child is PlayerState state)
                    yield return state;
            }
        }
    }

    public partial class PlayerState : Node
    {
        protected Player _player => GetParent<Player>();
        protected Node3D _model => GetNode<Node3D>("%Model");

        public virtual void OnStateEntered() {}
        public virtual void OnStateExited() {}
    }
}

Once I moved PlayerState to a separate file, the issue stopped happening.

In case this is relevant, there are other node classes(such as PlayerFlyState) that inherit PlayerState. Those nodes exist as children of the player node, and they get enabled/disabled as the player changes states.

ashelleyPurdue avatar Aug 12 '23 01:08 ashelleyPurdue

I will try today the method mentioned above by Quinn-L and put the unload handler in an EditorPlugin. But I have rather little hope that it will work cleanly.

Nope. We decided not to waste so much time with this problem and rewrite certain parts of the code in GDScript to work around the trouble spots with C#. We are not happy with this. This will especially affect [GlobalClass] and [Tool] code, probably also System.Collection.Generic will cause trouble.

System.Collections.Generic definitely does not cause trouble. I've been using it extensively in a different Godot 4.1 project, and have never seen this problem in that project.

ashelleyPurdue avatar Aug 12 '23 01:08 ashelleyPurdue

No, it's because I had more than one Node class in a single script file.

I can't confirm, we have all classes in separate files and as soon as [Tool] or [GlobalClass] is used, there are the problems described above. With [Tool] code you can work around the problem by closing these scene tabs before compiling. With [GlobalClass] you can't, because the editor keeps instances of these classes persistently in the background and the binding of some libs prevents unloading. And as I see it, System.Collection.Generic is used by the Godot code generator and this might cause trouble, because maybe one of the libraries in this namespace can't be unloaded correctly. I.e. even if you don't use a Generic in your [GlobalClass] script, it causes problems in the editor. At least this is always the case with us.

swissdude15 avatar Aug 12 '23 08:08 swissdude15

No, it's because I had more than one Node class in a single script file.

I can't confirm, we have all classes in separate files and as soon as [Tool] or [GlobalClass] is used, there are the problems described above.

I'm not saying those scripts don't trigger the problem; I'm saying that multiple nodes in one file also triggers this problem.

ashelleyPurdue avatar Aug 12 '23 20:08 ashelleyPurdue

There are multiple causes of this problem. From the comments, there are at least three ways you can run into the issue..

taylorhadden avatar Aug 13 '23 16:08 taylorhadden

Without fixing this bug, the use of version 4.1.x is significantly limited and currently not recommended. We are using 4.0 until this is fixed, but are mourning the new features of 4.1 that we would have loved to use. Hopefully a fix in 4.2?

swissdude15 avatar Aug 20 '23 10:08 swissdude15

Just ran into this issue but in a seemingly different way than described above.

Failing code:

[GlobalClass]
public partial class WeaponDefinition : Resource
{
    [Export]
    public InventoryItemDefinition InventoryItem { get; private set; } = new();
}

InventoryItemDefinition is another GlobalClass resource. Figured I'd initialize the property with new() but that is a really bad idea. Think this causes some issues in the resource editor too, in addition to this issue.

Fixed code:

[GlobalClass]
public partial class WeaponDefinition : Resource
{
    [Export]
    public InventoryItemDefinition InventoryItem { get; private set; } = null!;
}

Rylius avatar Sep 07 '23 20:09 Rylius

Just ran into this issue but in a seemingly different way than described above.

Failing code:

[GlobalClass]
public partial class WeaponDefinition : Resource
{
    [Export]
    public InventoryItemDefinition InventoryItem { get; private set; } = new();
}

InventoryItemDefinition is another GlobalClass resource. Figured I'd initialize the property with new() but that is a really bad idea. Think this causes some issues in the resource editor too, in addition to this issue.

Fixed code:

[GlobalClass]
public partial class WeaponDefinition : Resource
{
    [Export]
    public InventoryItemDefinition InventoryItem { get; private set; } = null!;
}

Are you sure it was new() that was the problem, and not the fact that it was being initialized in the first place? If you actually assign it a value in the inspector, does the problem come back?

ashelleyPurdue avatar Sep 07 '23 22:09 ashelleyPurdue

Are you sure it was new() that was the problem, and not the fact that it was being initialized in the first place? If you actually assign it a value in the inspector, does the problem come back?

Yes. I've gone through my commit history one by one to figure out what caused this issue (somehow didn't happen immediately/might not have edited any code for a few commits) and everything was fine just before I added the new() (the GlobalClass resources existed for several weeks before that). Removing it immediately fixed the issue. With null! the inspector just shows "<empty>" (as expected), and assigning a new resource or loading an existing saved one work perfectly fine.

Rylius avatar Sep 07 '23 22:09 Rylius

Interesting! Have you tried what happens when you change the GlobalClass code and run the build again? This leads to the known problems with us, because the GlobalClass assemblies (like [Tool] also) run in the editor and have to be unloaded for the new version, but this often fails.

swissdude15 avatar Sep 08 '23 08:09 swissdude15

My project currently has 21 classes with [GlobalClass] (mostly resources, some nodes) and so far I haven't had any issues when it comes to unloading. Adding new [Export] properties and modifying methods both work fine. (I don't have any [Tool] classes, addons or editor plugins, if that helps?)

Rylius avatar Sep 08 '23 08:09 Rylius

Huge thanks @Rylius! I was in the exact same case where I used to initialize my exported properties like this:

[Export]
public MyCustomResource MyProperty { get; set; } = new() { ResourceLocalToScene = true };

Removing the initialization from the code and doing it in the editor with the inspector instead worked like a charm! The issue in my project is gone now. Custom resources I have made so far were annotated with both [Tool] and [GlobalClass] if it helps.

steb-vs avatar Sep 10 '23 19:09 steb-vs