[Released] Roslyn C# - Runtime C# compiler

Ah sorry, I misread your last post.
In that case it is probably just missing references that need to be replaced with assembly reference assets. Looks like you have netstandard already which is good, but if it is complaining about not finding System.Object then you will probably need to add mscorlib too as a minimum. Then for further references it will depend on what parts of the Unity API you are using for example.

Are players able to use this asset in-game? I’m planning to make a game where the player can access some of the code, though I’m not sure if it will work in build before I purchase it.

Hi, Yes it will be possible to setup a game where your players are able to write C# code if that is what you mean. It will require a little extra work on your part to make that happen, since Roslyn C# is just a compiler and does not really help in any way with writing code in game. You might want to check out our other asset in addition [In Game Code Editor](https://assetstore.unity.com/packages/tools/gui/ingame-code-editor-144254) which will allow you to easily add a UI input field that supports C# syntax highlighting for writing the code, and together with Roslyn C# you will be able to build a game were players can write and run C# code.
Also there is a demo game included which allows the player to write C# source code to solve a maze problem, but this demo is only using a standard input field for writing code, so no nice features like syntax highlighting and line numbers.

I hope that helps you. Let me know if you have any more questions or if there is anything else.

1 Like

Hey @scottyboy805 I’m having a bit of a hard time trying to get nested type. The script I’m loading and compiling with roslyn looks like this:

 public class ExternalClass : MonoBehaviour
    {
        [Serializable]
        public struct ObjectWithDelay
        {
            [SerializeField] public CustomData someData;
            [SerializeField] public float delay;

            public ObjectWithDelay(CustomData someData, float delay)
            {
                this.someData = someData;
                this.delay = delay;
            }
        }  
    }

The problem is that I can’t find a way to access the nested type ObjectWithDelay from the script assembly, it’s just not being returned in any of the Find methods. So my actual question - what would be the best way to get the nested type? Hopefully I’m just missing something

PS: I’m using dotnow and ScriptType scriptType = externalDomain.CompileAndLoadMainSourceInterpreted to read the source

Hi, For finding nested types you have a couple of options so I guess it will be what suits you best:

  1. You can first find the root type (Or scan all root types in the assembly). Once you have the ScriptType for the root type (ExternalClass in this case), you can then use the FindNestedType method, or alternatively you can search through the NestedTypes array.
// Find root
ScriptType rootType = assembly.FIndType("ExternalClass");

// Find nested
ScriptType nestedType = rootType.FindNestedType("ObjectWithDelay");

// Or find all nested types
foreach(ScriptType nested in rootType.NestedTypes)
    Debug.Log(nested.FullName);
  1. Secondly you mentioned that you have tried the find methods with no success. I am not sure if you were aware the most of the find methods include an optional parameter named findNestedTypes. if you pass true for that argument then your nested type should be included in the search. Note though that in this case you should search via type name only (ObjectWithDelay) and not the relative type name (Namespace.ExternalClass+ObjectWithDelay).

Hope that helps. Let me know if you are still having trouble with that, or if there is anything else.

Roslyn C# version 1.8.2 has been released on the asset store.

This update contains code security vulnerability fixes and it is highly recommended that you update where possible to ensure that code security protection cannot be exploited in your game.

-Fix code security vulnerability relating to compiler generated state machines which could go undetected by security checks.
-Further testing and investigation of potential exploits regarding code security.

Roslyn C# version 1.9.0 has been release on the asset store. In this version the code security restrictions system has been redesigned for better usability making it easier to view and setup which assemblies, namespaces, types and members can be accessed by compiled code.

  • Redesign code security restrictions system to make it easier to configure which assemblies, namespace, types and members can be accessed by compiled code.
  • Code restrictions are now primarily a whitelist only (opt-in) system.
  • Code restrictions are now edited via a tree view, which allows for quick selection of namespaces, and types as groups.
  • Improve code restrictions for default assemblies (mscorlib/netstandard)

Retrieving line number of runtime exceptions thrown from runtime-compiled code via Roslyn

This post is just an answer to a question that has been asked many times for the underlying RoslynCSharp compiler, which I wanted to provide support for, through this Unity asset.

If you want debug info from runtime exceptions that are thrown from your runtime-compiled code, such as:

  • Source file name
  • Source line number (where exception was thrown)
  • Source column number (character of the line where the method which threw the exception starts from)
  • Error message

You need to modify the asset slightly following these steps. The original answer is from the accepted answer on the stack overflow post: Debug DLL generated from Roslyn C# at runtime


First, go into RoslynCSharp.Compiler.RoslynCSharpCompiler.cs and add this method anywhere:

/// <summary>
/// Compiles C# code into a runtime assembly with debug info attached.
/// Idk why this works. Just read this: https://stackoverflow.com/questions/50649795/how-to-debug-dll-generated-from-roslyn-compilation
/// </summary>
public Assembly CreateAssembly(string code, IMetadataReferenceProvider[] additionalAssemblyReferences = null)
{
    var encoding = Encoding.UTF8;

    var assemblyName = Path.GetRandomFileName();
    var symbolsName = Path.ChangeExtension(assemblyName, "pdb");
    var sourceCodePath = "generated.cs";

    var buffer = encoding.GetBytes(code);
    var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true);

    var syntaxTree = CSharpSyntaxTree.ParseText
    (
        sourceText,
        new CSharpParseOptions(),
        sourceCodePath
    );

    var syntaxRootNode = syntaxTree.GetRoot() as CSharpSyntaxNode;
    var encoded = CSharpSyntaxTree.Create(syntaxRootNode, null, sourceCodePath, encoding);

    var optimizationLevel = OptimizationLevel.Debug;

    var references = UpdateReferences(additionalAssemblyReferences, out var referenceException);

    // Check for referencing error
    if (referenceException != null)
        throw referenceException;

    CSharpCompilation compilation = CSharpCompilation.Create
    (
        assemblyName,
        syntaxTrees: new[] { encoded },
        references: references,
        options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
                .WithOptimizationLevel(optimizationLevel)
                .WithPlatform(Platform.AnyCpu)
    );

    using var assemblyStream = new MemoryStream();
    using var symbolsStream = new MemoryStream();

    var emitOptions = new EmitOptions
    (
        debugInformationFormat: DebugInformationFormat.PortablePdb,
        pdbFilePath: symbolsName
    );

    var embeddedTexts = new List<EmbeddedText>
    {
        EmbeddedText.FromSource(sourceCodePath, sourceText),
    };

    EmitResult result = compilation.Emit
    (
        peStream: assemblyStream,
        pdbStream: symbolsStream,
        embeddedTexts: embeddedTexts,
        options: emitOptions
    );

    if (!result.Success)
    {
        var errors = new List<string>();

        IEnumerable<Diagnostic> failures = result.Diagnostics.Where
        (
            diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error
        );

        foreach (Diagnostic diagnostic in failures)
        {
            errors.Add($"{diagnostic.Id}: {diagnostic.GetMessage()}");
        }

        throw new Exception(string.Join("\n", errors));
    }

    assemblyStream.Seek(0, SeekOrigin.Begin);
    symbolsStream?.Seek(0, SeekOrigin.Begin);

    Assembly assembly = Assembly.Load(assemblyStream.ToArray(), symbolsStream.ToArray());
    return assembly;
}

That’s it. The next step is using this method to compile, execute and debug your code:


My runtime script specifically has a Main() method, which I want to call, but you can adjust the code below for your needs:

In any Unity script, just add this function to get the Main method from the runtime-compiled code:

/// <summary>
/// Compiles the C# runtime code into a static class with a Main() method.
/// </summary>
/// <param name="programCode">C# runtime generated block code</param>
/// <returns></returns>
MethodInfo GetMainMethod(string programCode, out RoslynCSharp.Compiler.CompilationError[] errors)
{
    errors = null;

    if (Domain == null)
    {
        // Create the domain
        Domain = ScriptDomain.CreateDomain("MainProgram", true);

        // Add assembly references
        foreach (AssemblyReferenceAsset reference in assemblyReferences)
        {
            Domain.RoslynCompilerService.ReferenceAssemblies.Add(reference);
        }
    }

    try
    {
        // Compile using RoslynCSharp asset because the wrappers provided in the 'Domain.CompileResult' already
        // give source line/column numbers for compilation errors only (not runtime errors)
        // Exception is thrown at this line if the code doesn't compile correctly
        var type = Domain.CompileAndLoadMainSource(programCode, ScriptSecurityMode.UseSettings, assemblyReferences);

        // Compile using custom function because there are no provided wrappers for catching runtime errors
        var assembly = Domain.RoslynCompilerService.CreateAssembly(programCode, assemblyReferences);

        // My runtime script always has a public static Main method (which I want to call) within the assembly
        var method = assembly.GetTypes().First().GetMethod("Main");

        return method;
    }
    catch (Exception e)
    {
        Debug.LogError($"Failed to compile code: {e.Message}");

        // Output compilation errors
        if (Domain.CompileResult != null && Domain.CompileResult.ErrorCount > 0)
        {
            errors = Domain.CompileResult.Errors;
        }
    }

    return null;
}

I’ve added the compile step using the original RoslynCSharp unity asset if you want to still debug compilation errors, but my custom method debugs runtime errors.


Lastly, paste this code in any method to execute and debug:

// Get 'Program.cs' class
var sourceCode = "[ENTER YOUR C# SOURCE CODE HERE]";
var mainMethod = GetMainMethod(sourceCode, out var errors);
        
// Output compile-time errors
if (errors != null)
{
    foreach (var error in errors)
    {
        Debug.LogError
        (
            $"Error on line: {error.SourceLine + 1}\n" +
            $"character: {error.SourceColumn + 1}\n" +
            $"message: {error.Message}"
        );
    }
}
// Run the program
else
{
    // The Main() method of my program is the entry point, and is static, with no parameters.
    // It returns an object of type 'RuntimeErrorOutput' (which I custom-made)
    // which is 'null' (if there are no errors), otherwise contains the exception debug info.
    var runtimeErrorOutput = mainMethod.Invoke(null, null);

    // Runtime error found
    if (runtimeErrorOutput != null)
    {
        var output = (RuntimeErrorOutput)runtimeErrorOutput;

        var errorMessage = $"Error occured in file: {output.SourceFile}\n" +
                            $"line: {output.SourceLine}\n" +
                            $"character: {output.SourceColumn}\n" +
                            $"message: {output.Message}";

        Debug.LogError(errorMessage);
    }
}

There is a special object which I use to collect debug info from the exception thrown which is called RuntimeErrorOutput. Make sure this can be accessed from your runtime code by including the Unity assembly:

public class RuntimeErrorOutput
{
    public readonly string SourceFile;
    public readonly int SourceLine;
    public readonly int SourceColumn;
    public readonly string Message;

    public RuntimeErrorOutput(Exception e)
    {
        // Some exceptions are hidden within 'InnerException'
        // e.g. TargetException (due to calling methods by reflection)
        // e.g. StackOverflowException (idk why)
        // Necessary to trace down any hidden internal exceptions
        while (e.InnerException != null)
        {
            e = e.InnerException;
        }

        // Use stack trace to find the last stack frame where an error was found on.
        // Sometimes, the file line and column number don't exactly match where the error
        // was found and may instead point to the function which was last called, where the error
        // occured. This may be due to the compiler optimizing by doing 'method inlining', which
        // you can prevent by adding the attribute:
        //      [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        // onto all of your methods.
        // See more info here: https://stackoverflow.com/questions/7971118/exceptions-stacktrace-doesnt-show-where-the-exception-was-thrown
        var st = new System.Diagnostics.StackTrace(e, true);

        // Get the top-most frame (most previous stack frame where error was found)
        var frame = st.GetFrame(0);

        SourceFile = frame.GetFileName();
        SourceLine = frame.GetFileLineNumber();
        SourceColumn = frame.GetFileColumnNumber();
        Message = e.Message;
    }
}

Finally, your source needs to be set up in a way to return the exception debug info from within the code. Here’s some example source that I compiled, and the output:

Source

using System;
using UnityEngine;

namespace MyProject
{
	class Program
	{
		public static int Variable1 = 0;
		
		public static RuntimeErrorOutput Main()
		{
			try
			{
				if (true)
				{
					Debug.Log(myInt());
				}
				return null;
			}
			catch (Exception e) { return new RuntimeErrorOutput(e); }
		}		
		
		static int myInt()
		{
			return (5 / Program.Variable1);
		}
		
	}
}

Error Output

Error occured in file: generated.cs
line: 25
character: 4
message: Attempted to divide by zero.


Hope this helps. Send me any questions if it doesn’t.

1 Like