In Godot, C# is integrated with the engine using marshaling. Marshalling is the process of converting a C# type to a Godot type. However, not all types can be converted, leading to marshalling problems. This is primarily an issue for editor plugins, because they must be able to persist across solution rebuilds. When you build the C# solution (either through the build button in the top right of Godot or from your IDE), Godot attempts to serialize all C# tool scripts, replace those scripts with the new ones after the rebuild, and then deserialize the data back into the new scripts. However, this requires marshalling the fields and properties of the C# tool scripts.
Godot will always attempt to marshal Array
, List<>
and Dictionary<>
types, even if the elmeent type of the collection is
unmarshallable.
Unmarshallable types can also cause C# memory corruption, leading to the eventual crash of the Godot Editor after enough solution rebuilds.
The solution is to use a custom C# class that contains C# collections. Since Godot does not attempt to marshal custom C# classes, this provides a safe way to use C# collections.
I also made a quick Python script that continually rebuilds the project to attempt to trigger the crash. If the project has marshalling bugs, then the project will crash after enough rebuilds.
Python Crash Testing Script
import subprocess
import sys
import time
from colorama import init as colorama_init
from colorama import Fore
colorama_init()
class Content:
ONE = """
using Godot;
public class CrashTest
{
public void Test()
{
GD.Print("Test");
}
}
"""
TWO = """
using Godot;
public class CrashTest
{
public void TestTwo()
{
GD.Print("TestTwo");
}
}
"""
DIVIDER = f"{Fore.LIGHTBLACK_EX}---------------------------------{Fore.RESET}"
begin_test_delay = 10;
delay = 0;
if len(sys.argv) > 1 and sys.argv[1]:
delay = int(sys.argv[1])
godot_process = subprocess.Popen(['godot', '--editor', '.'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
required_success_count = 30;
success_count = 0
print(f"{Fore.LIGHTGREEN_EX}Solution Crash Tester(Delay: {delay} ms):{Fore.RESET}")
print(f"{Fore.LIGHTGREEN_EX} Press Ctrl+C to exit{Fore.RESET}")
print(DIVIDER)
time.sleep(begin_test_delay);
try:
while True:
content = ""
try:
with open("CrashTest.cs", "r+") as file:
content = file.read()
file.close()
except OSError:
print(f"{Fore.BLUE} CrashTest.cs doesn't exist, creating new file{Fore.RESET}")
with open("CrashTest.cs", "w+") as file:
if content == Content.ONE:
print(f"{Fore.BLUE} Changing Content to TWO{Fore.RESET}")
file.write(Content.TWO)
else:
print(f"{Fore.BLUE} Changing Content to ONE{Fore.RESET}")
file.write(Content.ONE)
file.close()
time.sleep(delay/1000)
print(f"{Fore.LIGHTBLUE_EX} Running Build:{Fore.RESET}")
start = time.time()
result = subprocess.run(["dotnet", "build"], stdout=subprocess.PIPE)
end = time.time()
if (result.returncode == 0):
print(f"{Fore.BLUE} Success (%.2f s){Fore.RESET}" % (end - start))
else:
print(f"{Fore.RED} Build Failed:")
print(f"{Fore.LIGHTBLACK_EX}{result.stdout.decode('utf-8')}{Fore.RESET}");
break
if godot_process.poll() != None:
print(f"{Fore.RED}FAIL: Godot crashed. A marshalling bug exists in the project{Fore.RESET}");
break
else:
success_count += 1
print(f"{Fore.BLUE} Survived ({success_count}/{required_success_count}) rebuilds.{Fore.RESET}");
if success_count >= required_success_count:
print(f"{Fore.LIGHTGREEN_EX}SUCCESS: Godot survived the rebuilds.")
break
print(DIVIDER)
except KeyboardInterrupt:
pass
if godot_process.poll() == None:
godot_process.kill()
print(f"{Fore.LIGHTGREEN_EX}Exited...{Fore.RESET}");