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

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

Open
RedworkDE opened this issue Jun 21, 2023 · 119 comments

Comments

@RedworkDE
Copy link
Member

RedworkDE commented Jun 21, 2023

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:

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:

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();
        };
    }
}

Footnotes

  1. Bugsquad edit 2

@wp2000x
Copy link

wp2000x commented Jul 6, 2023

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?

@Quinn-L
Copy link

Quinn-L commented Jul 7, 2023

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.

@RedworkDE
Copy link
Member Author

RedworkDE commented Jul 7, 2023

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.

@Quinn-L
Copy link

Quinn-L commented Jul 7, 2023

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.

@taylorhadden
Copy link

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.

@RedworkDE
Copy link
Member Author

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)

@taylorhadden
Copy link

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.

@RedworkDE
Copy link
Member Author

You also seem to be using generic Nodes which also cause issues in some cases, see #79007

@taylorhadden
Copy link

taylorhadden commented Jul 15, 2023

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
Copy link

I have created that reproduction project: #79519

@Pilvinen
Copy link

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
Copy link

Pilvinen commented Jul 23, 2023

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.

@swissdude15
Copy link

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.

@Ryan-000
Copy link
Contributor

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.

@ashelleyPurdue
Copy link

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?

@Ryan-000
Copy link
Contributor

Ryan-000 commented Jul 29, 2023

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

@swissdude15
Copy link

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
Copy link

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.

@ashelleyPurdue
Copy link

ashelleyPurdue commented Aug 12, 2023

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
Copy link

ashelleyPurdue commented Aug 12, 2023

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.

@swissdude15
Copy link

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.

@ashelleyPurdue
Copy link

ashelleyPurdue commented Aug 12, 2023

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.

@taylorhadden
Copy link

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

@swissdude15

This comment was marked as off-topic.

@hhyyrylainen
Copy link

hhyyrylainen commented Aug 26, 2024

I don't think I've used any Godot resources at all. Other than the built-in ones of course. So I think my project contains zero resources with a C# script related to them. And I still see this issue (though I haven't updated to 4.3 yet as I've been busy the past week) so I don't think that can be the whole picture.

Edit: I've now tried Godot 4.3 and I still got the same issue within about 5 minutes.

@DSOE1024
Copy link

DSOE1024 commented Sep 5, 2024

Is it possible that the resourceformatsaver and resourceformatloader that you wrote yourself could cause this problem?

@Delsin-Yu
Copy link
Contributor

Delsin-Yu commented Oct 11, 2024

@maxime4000 If possible, can you try if #98097 fixes your issue? No, I will continue to search for the reason

Update

@MDuRieu
Copy link

MDuRieu commented Oct 18, 2024

I had this error caused by using [Export] on an enum and assigning it to the MultiplayerSynchroniser. Not sure which part was the exact cause.

@hunterloftis
Copy link
Contributor

Multiple Resource classes that are "GlobalClass" and located in the same file are problematic and when you refactor all your resource into separated files, this issue disappear.

This is not the case, at least not universally. I'm consistently seeing this issue in a project in which there are no shared GlobalClass files.

@ilexl
Copy link

ilexl commented Nov 12, 2024

Most of the time when I get this error a quick restart and build works just fine. Is there anyway to get the editor to try effectively restart and build again once if it encounters this error?

@Whitecl4ws
Copy link

These two lines were both causing unload issues in the same file for me:
Image
Create your exported variables properly otherwise you might get unloading issues.

@ilexl
Copy link

ilexl commented Nov 21, 2024

These two lines were both causing unload issues in the same file for me: Image Create your exported variables properly otherwise you might get unloading issues.

@Whitecl4ws What would be the correct way to create Exported variables then?

@Whitecl4ws
Copy link

@ilexl Well for the top line, I'm exporting a non Godot.Collections.Array object and as for the bottom, I'm exporting a variable with no getters/setters. Neither of those exported variables show up in the inspector for me.

I haven't tested it but the way you would do both would be:

[Export]
public Godot.Collections.Array<SchedulerCycleModel> Cycles { get; set; } = new Godot.Collections.Array<SchedulerCycleModel> { new() };

[Export]
public bool RunActionOnStart { get; set; } = false;

Note that my personal use case was very specific wherein I was using the GoDotTest test runner to run tests ontop of coverlet and it was failing to unload in the editor every other run. But I spent way too much time hunting these two lines down not to share my findings with unfortunate people that might deal with this issue in the future. :P

@Tichau
Copy link

Tichau commented Nov 28, 2024

For information, I reproduced the issue by modifying the listeners (from System.Diagnostics.Trace) in the static constructor of a Node. If the scene is open in the editor, Godot fails to unload the assembly (probably because the listener registered keep being used).

public partial class App : Node
{
     static App()
     {
         Trace.Listeners.Clear();
         Trace.Listeners.Add(new Core.TraceListener(hideInternalCalls: true));
     }
}

@ghtyrant
Copy link

ghtyrant commented Dec 1, 2024

Can anyone give me a direction on how to debug this? I'm currently facing this issue on every project rebuild. I'm not using any [Tool], nor GlobalClasses, nor external libraries.

I'm quite stumped what could cause this, so I read through Microsoft's documentation on how to debug this. As far as I understood, you create a helper program that loads the assemblies, unloads them and then checks for errors. So far so good, but I don't know how to apply this to Godot now, since I'm not in control of loading my game's assembly. Also, just loading the assemblies from an external program does not cause any unloading issues, but I guess this is just because nothing's happening when I simply load the assemblies but don't call any functions.

edit: Just to make sure, I've stopped using Rider, made a clean build of the project using Godot but was able to reproduce the issue.

@Delsin-Yu
Copy link
Contributor

@ghtyrant You may check if you are using any unload-incompatible featrues (System.Text.Json, or the Example above) in editor reachable code ([ModuleInitializer], static constructor, instance constructer, [Export]ed properties).

@ghtyrant
Copy link

ghtyrant commented Dec 2, 2024

@Delsin-Yu Thanks a lot. I went through all of my code again and noticed I missed two exported variables that were not properties. Now I can rebuild my project without restarting Godot.

Any plans on adding a warning/error messages for such cases?

@Delsin-Yu
Copy link
Contributor

@ghtyrant That sounds like a relatively heavy task that requires a dedicated Roslyn analyzer to go through all code paths that the editor may touch and perform syntax tree analysis; you are welcome to open up a proposal in the proposal repo and see if any community contributor is interested.

@godotengine godotengine deleted a comment from Anasbaltar Dec 2, 2024
@godotengine godotengine deleted a comment from Anasbaltar Dec 2, 2024
@godotengine godotengine deleted a comment from Anasbaltar Dec 2, 2024
@4everalone
Copy link

Folks,
In my case the issue was with my code and not knowing how Godot works. I thought pre-allocating objects via packagedScene.Instantiate in separate thread and shoving them into a list is a way to go. But now I realized that C#'s ThreadPool and the list were holding references that prevented it from being unloaded. In my case threadpool was holding threads. I guess i will have to go with more complex object pooling

@RPGgrenade
Copy link

Has any solution to this been found (or proposed to be fixed in the next update)? I'm running into this issue myself and I'm unsure what exactly is causing it. If it's a tool script ok. But I'd like to know what KIND of thing can cause this. Cuz at the moment it's a little confusing.

@ntngamedev
Copy link

Nothing?

@jsbeckr
Copy link

jsbeckr commented Jan 31, 2025

I was playing around with a [Tool] script and the new [ExportToolButton] attribute. Sadly it seems to happen every time I build the solution now.

[Tool]
public partial class BodyPartEditor : Node2D
{
    [Export(PropertyHint.File, "*.tres")]
    public string ResourcePath { get; set; }

    [ExportToolButton("Save Resource")]
    public Callable SaveResourceButton { get; set; }

    [ExportToolButton("Load Resource")]
    public Callable LoadResourceButton { get; set; }

    public override void _Ready()
    {
        SaveResourceButton = Callable.From(SaveResource);
        LoadResourceButton = Callable.From(LoadResource);

        // ...
    }
}

I am not sure if I use the [ExportToolButton] attribute correctly, but it's working (after restarting the editor). This feels like a major blocker in my opinion, if this is how it's supposed to work.

@MatthiasBae
Copy link

@jsbeckr I am having the same problem with the [ExportToolButton]. As soon as I comment it out it works building the application.

[Tool]
[GlobalClass]
public partial class DatasourceSqlDb : DatasourceBase<SqlConnection, DatasourceConfigSqlDb> {
    
    [ExportGroup("Connection")]
    [Export]
    public override DatasourceConfigSqlDb Config { get; set; }
    [ExportToolButton("Test Connection")]
    public Callable TestConnectionButton => Callable.From(this.TestConnection);
...
}

@BrahRah
Copy link

BrahRah commented Feb 7, 2025

I spend 2 month on an addon. I actually planned to release it next week but as I use System.Text.Json, other dll libs, threads, async await + some more unidentified things that cause ".NET: Failed to unload assemblies." my addon now causes that error with every press of build. I can't release an addon that causes this issue whenever build is pressed!!!! From what I've read there is no usable solution yet...

Please!!!! At least make a build signal or smth so I can use that to deactivate the addon/addons scripts temporarily... otherwise this addon I spent so much time on will not be usable.... I actually have worked on 2 more that just needed some polishing. I planned to release those within the year but with this error around those would also become useless......

----- edit:

I've worked on the mentioned workaround for the .net disconnect issue with chatgpt, but I don't know any c++ and have no clue if this would actually work. It would be nice if someone familiar with godots source could take a look at it:
https://paste.ofcode.org/XRUPe4NbzyjnhxXD4izZMJ

The idea is to make the build send a signal. Addons could then unload .net stuff that causes the .net disconnect. When the addons are finished unloading they would send a "I'm done signal" and godot would then start the build process. When finished there would be a "finished building signal" and the addons would load back what was unloaded before.

To refine this further addons could subscribe to the build pause thing, then godot could check for all subscribed addons to finish unloading. So that addons that don't need to unload anything won't make godot pause the build process.

@Delsin-Yu
Copy link
Contributor

@BrahRah
I believe the ISerializationListener interface is precisely what you need.

Image

As for unloading static caches produced by System.Text.Json, an existing solution was mentioned above, you can call the following helper method (JsonCacheControl.ClearCache) before unloading the assembly (in ISerializationListener.OnBeforeSerialize).

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests