How to use CodeAnalysis.CSharp to compile C# code in game dynamically?

Godot Version

4.3 stable mono

Question

Hi there, I’m using godot to develop a ship controlling platform. What I want to do is to add a feature that allows users to control the ship by uploading their own C# code. The program will compile the code and run it in the game, the code is designed to invoke some functions in the game and fetch some datas related to their ship, which aim to achieve some specific goal.

To make it possible, I use Microsoft.CodeAnalysis.CSharp to compile the code send by users, which seems like below:

using Godot;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.IO;
using System.Reflection;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;


public partial class ScriptsUIManager : Node
{
	[Export] OptionButton languageOptionButton;
	[Export] Button openScriptButton;
	[Export] Button compileScriptButton;
	[Export] Button runScriptButton;
	[Export] FileDialog openScriptFileDialog;

	[Export] CodeEdit scriptCodeEdit;

	string scriptPath;

	private MethodInfo scriptMethod;
	private Task scriptTask;

	public override void _Ready()
	{
		openScriptButton.Pressed += OpenScriptButton_Pressed;
		openScriptFileDialog.FileSelected += OpenScriptFileDialog_FileSelected;
		compileScriptButton.Pressed += CompileScriptButton_Pressed;
		runScriptButton.Pressed += RunScriptButton_Pressed;
	}

	private MetadataReference[] InitReferences()
	{
		var currentAssembly = Assembly.GetExecutingAssembly();
		var referencedAssemblies = currentAssembly.GetReferencedAssemblies();
		var references = new List<MetadataReference>();
		foreach (var assemblyName in referencedAssemblies)
		{
			try {
				var assembly = Assembly.Load(assemblyName);
				references.Add(MetadataReference.CreateFromFile(assembly.Location));
				GD.Print("INFO: Loaded assembly: ", assembly.FullName);
			}
			catch {
				GD.PrintErr("ERROR: Failed to load assembly: ", assemblyName.FullName);
			}
		}
		try {
			references.Add(MetadataReference.CreateFromFile(currentAssembly.Location));
		}
		catch {
			GD.PrintErr("ERROR: Failed to load current assembly: ", currentAssembly.FullName);
		}
		return references.ToArray();
	}

	public override void _Process(double delta)
	{
	}

	void CompileScriptButton_Pressed()
	{
		scriptMethod = CompileScript(scriptCodeEdit.Text);
		if (scriptMethod != null)
		{
			GD.Print("Script compiled successfully.");
		}
		else
		{
			GD.PrintErr("FATAL: Script compilation failed.");
		}
	}

	void RunScriptButton_Pressed()
	{
		if (scriptMethod != null && scriptTask == null)
		{
			scriptTask = new Task(() => ExecuteTask(scriptMethod));
			scriptTask.Start();
		}
		else if (scriptTask != null)
		{
			GD.Print("INFO: Task is already running.");
		}
		else if (scriptMethod == null)
		{
			GD.PrintErr("ERROR: Script is not compiled.");
		}
	}

	void OpenScriptFileDialog_FileSelected(string path)
	{
		scriptPath = path;

		// 读取脚本文件内容并显示到CodeEdit控件中
		scriptCodeEdit.Text = File.ReadAllText(scriptPath);
	}

	private MethodInfo CompileScript(string codeContent)
	{
		GD.Print("Compiling script file at ", scriptPath);
		if (languageOptionButton.Selected == 0)
		{
			// Python
			// 之后再完善
			return null;
		}
		else
		{
			var code = codeContent;
			var syntaxTree = CSharpSyntaxTree.ParseText(code);
			var assemblyName = Path.GetRandomFileName();
			var references = InitReferences();
			var compilation = CSharpCompilation.Create(assemblyName, new[] { syntaxTree }, null,
				new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
			var ms = new MemoryStream();
			var result = compilation.Emit(ms);
			GD.Print("Compilation result: ", result.Success);
			if (!result.Success)
			{
				var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
				foreach (var diagnostic in failures)
				{
					GD.PrintErr(diagnostic.Id, ": ", diagnostic.GetMessage());
				}
				return null;
			}
			else
			{
				ms.Seek(0, SeekOrigin.Begin);
				var assembly = Assembly.Load(ms.ToArray());
				var type = assembly.GetType("ShipOrder");
				var method = type.GetMethod("Execute");
				return method;
			}
		}
	}

	void OpenScriptButton_Pressed()
	{
		if (languageOptionButton.Selected == 0)
		{
			openScriptFileDialog.Filters = new string[] { "*.py;Python Script" };
		}
		else if (languageOptionButton.Selected == 1)
		{
			openScriptFileDialog.Filters = new string[] { "*.cs;C# Script" };
		}

		openScriptFileDialog.PopupCentered();
	}

	private void ExecuteTask(MethodInfo method)
	{
		if (method != null)
		{
			var instance = Activator.CreateInstance(method.DeclaringType);
			method.Invoke(instance, null);
		}
		else
		{
			GD.PrintErr("FATAL: Compile failed.");
		}
	}

}

However, when it comes to run, some MetadataReference was not loaded to the references, the errors comes like these:

 ERROR: Failed to load assembly: Microsoft.CodeAnalysis, Version=4.13.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
  ERROR: Failed to load assembly: Microsoft.CodeAnalysis.CSharp, Version=4.13.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
  ERROR: Failed to load assembly: System.Collections.Immutable, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  ERROR: Failed to load assembly: RJCP.SerialPortStream, Version=2.4.2.0, Culture=neutral, PublicKeyToken=5f5e7b70c6a74deb

Also this is my test code:

using System;
using Godot;
using System.Threading;

public class ShipOrder
{
    public void Execute()
    {
        // Debug AUVData Class
        GD.Print("ShipOrder.Execute() called");
        Thread.Sleep(500);
        // Fetch AUVData
        GD.Print("ShipOrder.Execute() fetching AUVData");
        GD.Print("Current Temperature: " + AUVData.AHT10Temp);
        Thread.Sleep(500);
        GD.Print("ShipOrder.Execute() finished");
    }
}

These assemblies failed to be loaded due to unknown reasons. However, when I did the same on MonoGame, the MetaDataReferences were fine loaded and it runs successfully. I’m not sure it’s related to Godot’s speacial features?

Anyway, Could anyone help me on this problem? Thanks a lot.

I was able to compile your example after editing the C# project file (.csproj) by autocompleting the error in Visual Studio 2022 (I think it should be also doable with Visual Studio Code and C# extension).

Here’s the full .csproj file for reference:

<Project Sdk="Godot.NET.Sdk/4.4.1">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
  </ItemGroup>
</Project>

Stepping further, I was able to compile C# code in C# program (using your code as base).

Here’s working example of the code that I got in the end and that I built with Visual Studio:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;

public partial class Node3d
{
    string scriptCodeEditText = @"
using System;

public class Test
{
	public int Execute()
	{
		return 413;
	}
}
	";

    string scriptPath;
    private MethodInfo scriptMethod;
    private Task<object> scriptTask;

	private void Print(params object[] param)
	{
		foreach (object obj in param) Console.Write(obj);
		Console.WriteLine();
	}

	public void CompileScriptButton_Pressed()
	{
		scriptMethod = CompileScript(scriptCodeEditText);
		if (scriptMethod != null)
		{
			Print("Script compiled successfully.");
		}
		else
		{
			Print("FATAL: Script compilation failed.");
		}
	}

	public void RunScriptButton_Pressed()
	{
		if (scriptMethod != null && scriptTask == null)
		{
			scriptTask = new Task<object>(() => ExecuteTask(scriptMethod));
			scriptTask.RunSynchronously();
			Print(scriptTask.Result);
		}
		else if (scriptTask != null)
		{
			Print("INFO: Task is already running.");
		}
		else if (scriptMethod == null)
		{
			Print("ERROR: Script is not compiled.");
		}
	}

	public MethodInfo CompileScript(string codeContent)
	{
		Print("Compiling script file at ", scriptPath);

		var code = codeContent;

		var syntaxTree = CSharpSyntaxTree.ParseText(code);
		var assemblyName = Path.GetRandomFileName();
		var compilation = CSharpCompilation.Create(assemblyName, [syntaxTree],
			[
				MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location),
				MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
			],
			new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
			);
		var ms = new MemoryStream();
		var result = compilation.Emit(ms);
		Print("Compilation result: ", result.Success);
		if (!result.Success)
		{
			var failures = result.Diagnostics;
			foreach (var diagnostic in failures)
			{
				Print(diagnostic.Id, ": ", diagnostic.GetMessage());
				diagnostic.AdditionalLocations.ToList().ForEach(x => Print(x));

			}
			return null;
		}
		else
		{
			ms.Seek(0, SeekOrigin.Begin);
			var assembly = Assembly.Load(ms.ToArray());
			var type = assembly.GetType("Test");
			var method = type.GetMethod("Execute");
			return method;
		}
	}

	private object ExecuteTask(MethodInfo method)
	{
		if (method != null)
		{
			var instance = Activator.CreateInstance(method.DeclaringType);
			return method.Invoke(instance, null);
		}
		else
		{
			Print("FATAL: Compile failed.");
			return null;
		}
	}
}

public class Program()
{
	public static void Main(string[] args)
	{
		var a = new Node3d();
		a.CompileScriptButton_Pressed();
		a.RunScriptButton_Pressed();
    }
}

And here’s the code adapted to Godot:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
using System;
using System.Threading.Tasks;
using System.IO;
using System.Linq;
using Godot;

public partial class Node3d : Node
{
	string scriptCodeEditText = @"
using System;

public class Test
{
	public int Execute()
	{
		return 413;
	}
}
	";

	string scriptPath;
	private MethodInfo scriptMethod;
	private Task<object> scriptTask;

	public void CompileScriptButton_Pressed()
	{
		scriptMethod = CompileScript(scriptCodeEditText);
		if (scriptMethod != null)
		{
			GD.Print("Script compiled successfully.");
		}
		else
		{
			GD.Print("FATAL: Script compilation failed.");
		}
	}

	public void RunScriptButton_Pressed()
	{
		if (scriptMethod != null && scriptTask == null)
		{
			scriptTask = new Task<object>(() => ExecuteTask(scriptMethod));
			scriptTask.RunSynchronously();
			GD.Print(scriptTask.Result);
		}
		else if (scriptTask != null)
		{
			GD.Print("INFO: Task is already running.");
		}
		else if (scriptMethod == null)
		{
			GD.Print("ERROR: Script is not compiled.");
		}
	}

	public MethodInfo CompileScript(string codeContent)
	{
		GD.Print("Compiling script file at ", scriptPath);

		var code = codeContent;

		var syntaxTree = CSharpSyntaxTree.ParseText(code);
		var assemblyName = Path.GetRandomFileName();
		var compilation = CSharpCompilation.Create(assemblyName, [syntaxTree],
			[
				MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location),
				MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
			],
			new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
			);
		var ms = new MemoryStream();
		var result = compilation.Emit(ms);
		GD.Print("Compilation result: ", result.Success);
		if (!result.Success)
		{
			var failures = result.Diagnostics;
			foreach (var diagnostic in failures)
			{
				GD.Print(diagnostic.Id, ": ", diagnostic.GetMessage());
				diagnostic.AdditionalLocations.ToList().ForEach(x => GD.Print(x));

			}
			return null;
		}
		else
		{
			ms.Seek(0, SeekOrigin.Begin);
			var assembly = Assembly.Load(ms.ToArray());
			var type = assembly.GetType("Test");
			var method = type.GetMethod("Execute");
			return method;
		}
	}

	private object ExecuteTask(MethodInfo method)
	{
		if (method != null)
		{
			var instance = Activator.CreateInstance(method.DeclaringType);
			return method.Invoke(instance, null);
		}
		else
		{
			GD.Print("FATAL: Compile failed.");
			return null;
		}
	}
	
	private void _on_button_pressed() {
		CompileScriptButton_Pressed();
		RunScriptButton_Pressed();
	}
}

And a screenshot showing that it’s actually working:

1 Like

A bit off-topic, but I feel like this is kind of a bad idea from a security standpoint. I can totally see a forum post somewhere saying: “If you want to have a cool ship that is invincible and yadda yadda - just copy-paste this code…”

But said code really downloads and installs a virus. Or deletes critical system files. Or savegames. Or does whatever else mischief.

Maybe consider using some other language with a more restrictive environment, where you have full control over the available standard library, etc?

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.