godot
godot copied to clipboard
.NET: Failed to unload assemblies. Please check <this issue> for more information.
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
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?
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.
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.
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.
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.
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)
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.
You also seem to be using generic Node
s which also cause issues in some cases, see https://github.com/godotengine/godot/pull/79007
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 have created that reproduction project: #79519
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.
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.
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.
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.
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?
Also happens whenever I enable a C# editor plugin, and only goes away once I disable the plugin and restart the editor.
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.
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.
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.
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 alsoSystem.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.
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.
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.
There are multiple causes of this problem. From the comments, there are at least three ways you can run into the issue..
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?
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!;
}
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 withnew()
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?
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.
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.
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?)
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.