Skip to content

Instantly share code, notes, and snippets.

@Atlinx
Last active June 7, 2023 23:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Atlinx/10e27ba99ec0ff224497b81ce2429fdc to your computer and use it in GitHub Desktop.
Save Atlinx/10e27ba99ec0ff224497b81ce2429fdc to your computer and use it in GitHub Desktop.
Godot C# 3.x Marshalling for Editor Plugins

Godot C# 3.x Marshalling for Editor Plugins

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.

The Issue

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

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}");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment