327 lines
11 KiB
C#
327 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Unity.Pipeline.Models;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Pipeline.Compilation
|
|
{
|
|
/// <summary>
|
|
/// C# code evaluation service using shared Roslyn compilation infrastructure.
|
|
/// Wraps user code in Execute() method, compiles, and executes with result serialization.
|
|
/// Supports both Editor and Runtime compilation for desktop development builds.
|
|
/// </summary>
|
|
public static class EvalCodeCompiler
|
|
{
|
|
#if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG)
|
|
|
|
// Fixed lines in the generated source before user code begins
|
|
private const int BaseCodeLineOffset = 11;
|
|
|
|
/// <summary>
|
|
/// Compile and execute code on Unity's main thread using Roslyn.
|
|
/// Eval is a MainThreadRequired command, so the server marshals the caller to the main
|
|
/// thread before invoking this; compilation + execution run synchronously here.
|
|
/// </summary>
|
|
public static EvalResponse CompileAndExecuteOnMainThread(string code, int timeoutMs, System.Diagnostics.Stopwatch stopwatch)
|
|
{
|
|
if (stopwatch == null)
|
|
{
|
|
stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
}
|
|
|
|
try
|
|
{
|
|
// Compile on current thread
|
|
var compilationResult = CompileCodeToAssembly(code, timeoutMs, stopwatch);
|
|
|
|
if (!compilationResult.Success)
|
|
{
|
|
stopwatch.Stop();
|
|
return EvalResponse.EvalFailure(
|
|
"Compilation Failed",
|
|
"Code compilation failed",
|
|
stopwatch.ElapsedMilliseconds,
|
|
compilationResult.Diagnostics);
|
|
}
|
|
|
|
// Execute on current thread
|
|
var executionResult = ExecuteCompiledAssembly(compilationResult.Assembly);
|
|
|
|
stopwatch.Stop();
|
|
|
|
if (executionResult.Success)
|
|
{
|
|
return EvalResponse.EvalSuccess(
|
|
executionResult.ReturnValue,
|
|
executionResult.Output,
|
|
stopwatch.ElapsedMilliseconds);
|
|
}
|
|
else
|
|
{
|
|
return EvalResponse.EvalFailure(
|
|
executionResult.Error,
|
|
executionResult.ErrorDetails,
|
|
stopwatch.ElapsedMilliseconds);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
stopwatch.Stop();
|
|
return EvalResponse.EvalFailure(
|
|
"Execution Failed",
|
|
ex.ToString(),
|
|
stopwatch.ElapsedMilliseconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compile C# code to assembly using shared RoslynCompilationService.
|
|
/// Thread-safe and can run on background thread.
|
|
/// </summary>
|
|
private static CompilationResult CompileCodeToAssembly(string userCode, int timeoutMs, System.Diagnostics.Stopwatch stopwatch)
|
|
{
|
|
var id = Guid.NewGuid().ToString("N").Substring(0, 8);
|
|
|
|
// Generate wrapper code
|
|
var sourceCode = BuildSourceCode(id, userCode, Application.isEditor);
|
|
|
|
// Use shared compilation service
|
|
var request = new CompilationRequest
|
|
{
|
|
SourceCode = sourceCode,
|
|
AssemblyName = $"PipelineEval_{id}",
|
|
LineNumberOffset = BaseCodeLineOffset // Adjust diagnostics for wrapper code
|
|
};
|
|
|
|
var result = RoslynCompilationService.Compile(request);
|
|
|
|
if (result.Success)
|
|
{
|
|
// Add execution context for eval-specific needs
|
|
return new CompilationResult
|
|
{
|
|
Success = true,
|
|
Assembly = result.Assembly,
|
|
ExecutionContext = new ExecutionContext
|
|
{
|
|
AssemblyId = id,
|
|
TypeName = $"PipelineEvaluation.PipelineEval_{id}",
|
|
MethodName = "Execute"
|
|
},
|
|
Diagnostics = result.Diagnostics
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new CompilationResult
|
|
{
|
|
Success = false,
|
|
Diagnostics = result.Diagnostics
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate C# source code wrapper for user code with conditional editor support.
|
|
/// </summary>
|
|
private static string BuildSourceCode(string id, string userCode, bool isEditorContext)
|
|
{
|
|
var editorUsing = isEditorContext ? "using UnityEditor;" : "";
|
|
|
|
return $@"using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
{editorUsing}
|
|
|
|
namespace PipelineEvaluation
|
|
{{
|
|
public static class PipelineEval_{id}
|
|
{{
|
|
public static object Execute()
|
|
{{
|
|
{userCode}
|
|
return null;
|
|
}}
|
|
}}
|
|
}}";
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Execute compiled assembly and return result.
|
|
/// Must run on Unity's main thread for safe Unity API access.
|
|
/// </summary>
|
|
private static ExecutionResult ExecuteCompiledAssembly(Assembly assembly)
|
|
{
|
|
try
|
|
{
|
|
var executionContext = GetExecutionContextFromAssembly(assembly);
|
|
|
|
var type = assembly.GetType(executionContext.TypeName);
|
|
var method = type?.GetMethod(executionContext.MethodName, BindingFlags.Public | BindingFlags.Static);
|
|
|
|
if (type == null)
|
|
{
|
|
return new ExecutionResult
|
|
{
|
|
Success = false,
|
|
Error = "Assembly Load Error",
|
|
ErrorDetails = $"Could not find type {executionContext.TypeName} in compiled assembly"
|
|
};
|
|
}
|
|
|
|
if (method == null)
|
|
{
|
|
return new ExecutionResult
|
|
{
|
|
Success = false,
|
|
Error = "Method Not Found",
|
|
ErrorDetails = $"Could not find method {executionContext.MethodName} in compiled type"
|
|
};
|
|
}
|
|
|
|
var result = method.Invoke(null, null);
|
|
|
|
return new ExecutionResult
|
|
{
|
|
Success = true,
|
|
ReturnValue = SerializeResult(result)
|
|
};
|
|
}
|
|
catch (TargetInvocationException tie) when (tie.InnerException != null)
|
|
{
|
|
return new ExecutionResult
|
|
{
|
|
Success = false,
|
|
Error = "Runtime Error",
|
|
ErrorDetails = tie.InnerException.Message,
|
|
StackTrace = tie.InnerException.StackTrace
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ExecutionResult
|
|
{
|
|
Success = false,
|
|
Error = "Execution Error",
|
|
ErrorDetails = ex.Message,
|
|
StackTrace = ex.StackTrace
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get execution context from compiled assembly.
|
|
/// </summary>
|
|
private static ExecutionContext GetExecutionContextFromAssembly(Assembly assembly)
|
|
{
|
|
// Find PipelineEvaluation types
|
|
var evalTypes = assembly.GetTypes()
|
|
.Where(t => t.Namespace == "PipelineEvaluation" && t.Name.StartsWith("PipelineEval_"))
|
|
.ToList();
|
|
|
|
if (evalTypes.Any())
|
|
{
|
|
var evalType = evalTypes.First();
|
|
return new ExecutionContext
|
|
{
|
|
TypeName = evalType.FullName,
|
|
MethodName = "Execute"
|
|
};
|
|
}
|
|
|
|
// Fallback
|
|
return new ExecutionContext
|
|
{
|
|
TypeName = "PipelineEvaluation.PipelineEval_Unknown",
|
|
MethodName = "Execute"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serialize result for JSON response.
|
|
/// </summary>
|
|
private static object SerializeResult(object value)
|
|
{
|
|
if (value == null) return null;
|
|
|
|
// Handle primitive types directly
|
|
if (value is string || value is bool || value is int || value is long || value is float || value is double)
|
|
return value;
|
|
|
|
// Handle Unity objects
|
|
if (value is UnityEngine.Object unityObj)
|
|
{
|
|
try
|
|
{
|
|
return Newtonsoft.Json.JsonConvert.DeserializeObject(UnityEngine.JsonUtility.ToJson(unityObj));
|
|
}
|
|
catch
|
|
{
|
|
return value.ToString();
|
|
}
|
|
}
|
|
|
|
// Handle other objects via JSON serialization
|
|
try
|
|
{
|
|
return Newtonsoft.Json.JsonConvert.DeserializeObject(
|
|
Newtonsoft.Json.JsonConvert.SerializeObject(value));
|
|
}
|
|
catch
|
|
{
|
|
return value.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result from compilation process.
|
|
/// </summary>
|
|
private class CompilationResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public Assembly Assembly { get; set; }
|
|
public ExecutionContext ExecutionContext { get; set; }
|
|
public List<DiagnosticInfo> Diagnostics { get; set; } = new List<DiagnosticInfo>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result from code execution.
|
|
/// </summary>
|
|
private class ExecutionResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public object ReturnValue { get; set; }
|
|
public string Output { get; set; }
|
|
public string Error { get; set; }
|
|
public string ErrorDetails { get; set; }
|
|
public string StackTrace { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execution context for compiled assembly.
|
|
/// </summary>
|
|
private class ExecutionContext
|
|
{
|
|
public string AssemblyId { get; set; }
|
|
public string TypeName { get; set; }
|
|
public string MethodName { get; set; }
|
|
}
|
|
|
|
#else
|
|
/// <summary>
|
|
/// Runtime compilation not supported on this platform.
|
|
/// Desktop development builds only (Windows/Mac/Linux).
|
|
/// </summary>
|
|
public static EvalResponse CompileAndExecuteOnMainThread(string code, int timeoutMs, System.Diagnostics.Stopwatch stopwatch)
|
|
{
|
|
return EvalResponse.EvalFailure(
|
|
"Platform Not Supported",
|
|
"Runtime code compilation only supported on Desktop development builds");
|
|
}
|
|
#endif
|
|
}
|
|
} |