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 6: Hot reload throws exceptions #67719

Open
wihrl opened this issue Oct 21, 2022 · 6 comments
Open

.NET 6: Hot reload throws exceptions #67719

wihrl opened this issue Oct 21, 2022 · 6 comments

Comments

@wihrl
Copy link

wihrl commented Oct 21, 2022

Godot version

4.0 beta 3

System information

Win 11

Issue description

Applying method changes while debugging a C# project results in errors. The method still gets updated, but the resulting error can be seen in the hot reload output in the Output panel of Visual Studio.
[Error] Applying updates failed: Microsoft.VisualStudio.HotReload.Components.DeltaApplier.ManagedDeltaApplierFailedToConnectException: Exception of type 'Microsoft.VisualStudio.HotReload.Components.DeltaApplier.ManagedDeltaApplierFailedToConnectException' was thrown.

In Rider, this manifests itself as only being able to apply the changes once.
image

Steps to reproduce

Create a new C# project and setup debugging.

In the .csproj:

<PropertyGroup>
    <StartAction>Program</StartAction>
    <StartProgram>path to godot</StartProgram>
  </PropertyGroup>

launchSettings.json:

{
  "profiles": {
    "HotReloadTest": {
      "commandName": "Project",
      "commandLineArgs": "--path C:\\Projects\\HotReloadTest"
    }
  }
}

Then start debugging, change a method and apply the change.

Minimal reproduction project

No response

@Calinou Calinou changed the title .NET 6 Hot reload throws exceptions .NET 6: Hot reload throws exceptions Oct 22, 2022
@definitelyokay
Copy link

Just out of curiosity, does anyone know if Godot 4 is supposed to work with dotnet's hot reloading capabilities out of the box? If so, is that documented anywhere I can read about it?

@wihrl
Copy link
Author

wihrl commented Oct 30, 2022

Probably not documented anywhere, but I'd assume that since Godot 4 is now just using the regular .NET runtime, there's nothing preventing this from working. I mean, it already kind of works in VS.

@wihrl
Copy link
Author

wihrl commented Feb 2, 2023

I found a workaround by using reflection to poll MethodBody.LocalSignatureMetadataToken.

/// <summary>
/// Using the standard .NET Hot Reload support does not work with Godot.
/// This class serves as a workaround to detect changes in code using reflection.
/// Replace with [assembly: System.Reflection.Metadata.MetadataUpdateHandler)] once it works.
/// </summary>
public static class HotReloadChecker
{
    static readonly Dictionary<(string, string), int> _tokens = new();

    public static bool Changed<T>(T instance, string methodName)
    {
        var type = instance.GetType();
        var method = type.GetMethod(methodName)?.GetMethodBody() ??
                     throw new Exception($"Method {methodName} not found");
        var token = method.LocalSignatureMetadataToken;

        var key = (type.Name, methodName);

        bool changed = false;
        if (_tokens.TryGetValue(key, out var lastToken))
            changed = lastToken != token;

        _tokens[key] = token;
        return changed;
    }
} 

@nathanpovo
Copy link

I found a workaround by using reflection to poll MethodBody.LocalSignatureMetadataToken.

/// <summary>
/// Using the standard .NET Hot Reload support does not work with Godot.
/// This class serves as a workaround to detect changes in code using reflection.
/// Replace with [assembly: System.Reflection.Metadata.MetadataUpdateHandler)] once it works.
/// </summary>
public static class HotReloadChecker
{
    static readonly Dictionary<(string, string), int> _tokens = new();

    public static bool Changed<T>(T instance, string methodName)
    {
        var type = instance.GetType();
        var method = type.GetMethod(methodName)?.GetMethodBody() ??
                     throw new Exception($"Method {methodName} not found");
        var token = method.LocalSignatureMetadataToken;

        var key = (type.Name, methodName);

        bool changed = false;
        if (_tokens.TryGetValue(key, out var lastToken))
            changed = lastToken != token;

        _tokens[key] = token;
        return changed;
    }
} 

@wihrl how is this supposed to be used? Where should this code be placed?

I cannot find any documentation that mentions creating a class like this to aid in hot reloading. What I did find is the MetadataUpdateHandlerAttribute attribute (see info on this here, and here) but I cannot see how your code can be converted to fit this attribute class.

@qwe321qwe321qwe321
Copy link

qwe321qwe321qwe321 commented Apr 27, 2024

@wihrl how is this supposed to be used? Where should this code be placed?

I cannot find any documentation that mentions creating a class like this to aid in hot reloading. What I did find is the MetadataUpdateHandlerAttribute attribute (see info on this here, and here) but I cannot see how your code can be converted to fit this attribute class.

[MetadataUpdateHandlerAttribute] is the official way to catch the event from hot-reloading. But it still doesn't work for Godot. (.NET hot reload itself has worked on Godot already since .NET 6.0)

I guess the @wihrl 's workaround was that you can call this method to check if the specific method has been hot-reloaded. To kinda catch the event of hot reloading, you'll need to call this method all the time.

@qwe321qwe321qwe321
Copy link

qwe321qwe321qwe321 commented Apr 28, 2024

I got a better workaround to me that using the custom attribute to register the notification and its callback.

HotReloadManager.cs

using Godot;
using System;
using System.Collections.Generic;
using System.Reflection;
using WatchMethod = (System.Reflection.MethodInfo watched, HotReloadCallback callback);

public delegate void HotReloadCallback(string methodName);

/// <summary>
/// Using the standard .NET Hot Reload support does not work with Godot.
/// This class serves as a workaround to detect changes in code using MethodInfo.GetMethodBody().LocalSignatureMetadataToken.
/// Put [NotifyHotReload] attribute to the method you want to catch the notification when it is hot reloaded.
/// And remember to put a script to call HotReloadManager.CheckHotReloaded() to update the notification system.
/// 
/// Replace with [assembly: System.Reflection.Metadata.MetadataUpdateHandler)] once it works.
/// </summary>
public static class HotReloadManager {
	static readonly Dictionary<MethodInfo, int> _methodTokens = new();
	static List<WatchMethod> _watchMethods = new List<WatchMethod>();
	static HotReloadManager() {
		Initialize();
	}

	static void Initialize() {
		GD.Print($"Initialize {nameof(HotReloadManager)}");

		_watchMethods.Clear();
		foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
			foreach (var type in assembly.GetTypes()) {
				foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) {
					var attribute = method.GetCustomAttribute<NotifyHotReloadAttribute>();
					if (attribute == null) {
						continue;
					}
					string callbackName = attribute.CallbackMethodName;
					if (string.IsNullOrEmpty(callbackName)) {
						continue;
					}
					// Callback must be static.
					MethodInfo callback = type.GetMethod(
						callbackName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static,
						types: [typeof(string)] // The first parameter is the name of the reloaded method.
						);
					if (callback == null) {
						continue;
					}
					_watchMethods.Add((method, callback.CreateDelegate<HotReloadCallback>()));
				}
			}
		}
		foreach (var method in _watchMethods) {
			int token = method.watched.GetMethodBody()?.LocalSignatureMetadataToken ??
				throw new Exception($"Method {method.watched}'s MethodBody not found");
			_methodTokens.Add(method.watched, token);
			GD.Print($"{method.watched.DeclaringType}.{method.watched.Name} registered hot-reloading.");
		}
	}

	public static bool CheckHotReloaded() {
		bool changed = false;
		foreach (var method in _watchMethods) {
			int token = method.watched.GetMethodBody()?.LocalSignatureMetadataToken ??
				throw new Exception($"Method {method}'s MethodBody not found");
			if (token != _methodTokens[method.watched]) {
				GD.Print($"{method.watched.DeclaringType}.{method.watched.Name} changed!");
				// Update the token.
				_methodTokens[method.watched] = token;
				changed = true;
				// Callback.
				method.callback.Invoke(method.watched.Name);
			}
		}
		return changed;
	}
}


[AttributeUsage(AttributeTargets.Method)]
public class NotifyHotReloadAttribute : Attribute {
	public string CallbackMethodName;
	public NotifyHotReloadAttribute(string callbackMethodName) {
		CallbackMethodName = callbackMethodName;
	}
}

Usage

using Godot;

public partial class HotReloadTest : Label {
	private static int _hotReloadTimes = 0;
	private static string _hotReloadMessage = "";

	public override void _Process(double delta) {
		// You should put HotReloadChecker.CheckHotReloaded() call in its own class to update it as often as you like.
		HotReloadManager.CheckHotReloaded();

		// Update text on the game ui.
		Text = $"Hot Reload Times: {_hotReloadTimes}\n" +
			$"MyMethod: {MyMethod()}\n" +
			$"MyMethod2: {MyMethod2()}\n" +
			_hotReloadMessage;
	}

	[NotifyHotReload(nameof(OnHotReloadCallback))]
	public string MyMethod() {
		return "Method 1";
	}

	[NotifyHotReload(nameof(OnHotReloadCallback))]
	public string MyMethod2() {
		return "Method 2";
	}

	// The callback method must be static and with 1 string parameter.
	private static void OnHotReloadCallback(string methodName) {
		_hotReloadTimes++;
		_hotReloadMessage = $"{methodName} has been changed!";
	}
}
vscode_hotload_maanger.mp4

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

No branches or pull requests

5 participants