using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Unity.Pipeline.Models; using UnityEngine; namespace Unity.Pipeline.Compilation { /// /// Shared Roslyn compilation service for both code evaluation and hot reload systems. /// Eliminates code duplication by providing common compilation infrastructure. /// Thread-safe and can be called from any thread. /// public static class RoslynCompilationService { #if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG) /// /// Compile source code to assembly with customizable options. /// Thread-safe compilation that can run on any thread. /// public static CompilationResult Compile(CompilationRequest request) { try { // Parse syntax tree. When emitting debug info, the tree needs a document path and // UTF-8 encoding so the portable PDB can reference a source document. var syntaxTree = request.EmitDebugInformation ? CSharpSyntaxTree.ParseText( SourceText.From(request.SourceCode, Encoding.UTF8), path: request.DocumentPath ?? request.AssemblyName + ".cs") : CSharpSyntaxTree.ParseText(request.SourceCode); // Get metadata references with optional additional prefixes var references = GetMetadataReferences(request.AdditionalAssemblyPrefixes); // Create compilation. Disable optimizations when emitting debug info so the JIT // keeps full sequence points (otherwise breakpoints can't bind). var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); if (request.EmitDebugInformation) options = options.WithOptimizationLevel(OptimizationLevel.Debug); var compilation = CSharpCompilation.Create( request.AssemblyName, new[] { syntaxTree }, references, options); // Get diagnostics var diagnostics = compilation.GetDiagnostics(); var diagnosticInfos = ConvertDiagnostics(diagnostics, request.LineNumberOffset); // Check for compilation errors var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); if (errors.Any()) { return new CompilationResult { Success = false, Diagnostics = diagnosticInfos }; } // Compile to memory using (var peStream = new MemoryStream()) { byte[] pdbBytes = null; EmitResult emitResult; if (request.EmitDebugInformation) { using (var pdbStream = new MemoryStream()) { emitResult = compilation.Emit( peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb)); if (emitResult.Success) pdbBytes = pdbStream.ToArray(); } } else { emitResult = compilation.Emit(peStream); } if (!emitResult.Success) { var emitDiagnostics = ConvertDiagnostics(emitResult.Diagnostics, request.LineNumberOffset); return new CompilationResult { Success = false, Diagnostics = diagnosticInfos.Concat(emitDiagnostics).ToList() }; } // Load assembly from memory. When symbols are present, load them alongside the // assembly so an attached debugger can bind breakpoints into the emitted code. var assemblyBytes = peStream.ToArray(); var assembly = pdbBytes != null ? PipelineUtils.LoadFromBytes(assemblyBytes, pdbBytes) : PipelineUtils.LoadFromBytes(assemblyBytes); return new CompilationResult { Success = true, Assembly = assembly, AssemblyBytes = assemblyBytes, PdbBytes = pdbBytes, Diagnostics = diagnosticInfos }; } } catch (Exception ex) { return new CompilationResult { Success = false, Diagnostics = new List { new DiagnosticInfo { Severity = "error", Message = $"Compilation exception: {ex.Message}", Line = 0, Column = 0, Id = "ROSLYN001" } } }; } } /// /// Async wrapper for compilation that runs on background thread. /// Use when you need to avoid blocking the calling thread. /// public static Task CompileAsync(CompilationRequest request) { return Task.Run(() => Compile(request)); } /// /// Get metadata references for compilation with optional additional assembly prefixes. /// Handles both Editor and Runtime scenarios with appropriate filtering. /// public static List GetMetadataReferences(string[] additionalPrefixes = null) { var references = new List(); var assemblies = PipelineUtils.GetLoadedAssemblies() .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(PipelineUtils.GetLoadedAssemblyPath(a))); if (Application.isEditor) { // Editor: Include all loaded assemblies for maximum compatibility foreach (var assembly in assemblies) { try { references.Add(MetadataReference.CreateFromFile(PipelineUtils.GetLoadedAssemblyPath(assembly))); } catch { // Skip problematic assemblies } } } else { // Runtime: Use curated filtering for Unity + user assemblies var allowedPrefixes = new List { "UnityEngine", "Assembly-CSharp", "netstandard", "mscorlib", "System.", "Unity.Pipeline" }; // Add any additional prefixes (e.g., for hot reload: "Microsoft.CodeAnalysis") if (additionalPrefixes != null) { allowedPrefixes.AddRange(additionalPrefixes); } foreach (var assembly in assemblies) { try { var assemblyName = assembly.GetName().Name; if (allowedPrefixes.Any(prefix => assemblyName.StartsWith(prefix))) { references.Add(MetadataReference.CreateFromFile(PipelineUtils.GetLoadedAssemblyPath(assembly))); } } catch { // Skip problematic assemblies } } } return references; } /// /// Convert Roslyn diagnostics to DiagnosticInfo list with optional line number adjustment. /// Handles line number offset for wrapped code scenarios. /// private static List ConvertDiagnostics(IEnumerable diagnostics, int lineOffset = 0) { var result = new List(); foreach (var diagnostic in diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning)) { var location = diagnostic.Location.GetLineSpan(); var line = Math.Max(0, location.StartLinePosition.Line - lineOffset); var column = location.StartLinePosition.Character; result.Add(new DiagnosticInfo { Severity = diagnostic.Severity.ToString().ToLower(), Message = diagnostic.GetMessage(), Line = line, Column = column, Id = diagnostic.Id }); } return result; } #else /// /// Runtime compilation not supported on this platform. /// Desktop development builds only (Windows/Mac/Linux). /// public static CompilationResult Compile(CompilationRequest request) { return new CompilationResult { Success = false, Diagnostics = new List { new DiagnosticInfo { Severity = "error", Message = "Runtime code compilation only supported on Desktop development builds", Line = 0, Column = 0, Id = "PLATFORM001" } } }; } /// /// Runtime compilation not supported on this platform. /// public static Task CompileAsync(CompilationRequest request) { return Task.FromResult(Compile(request)); } /// /// Runtime compilation not supported on this platform. /// public static List GetMetadataReferences(string[] additionalPrefixes = null) { return new List(); } #endif } /// /// Request for Roslyn compilation with customizable options. /// public class CompilationRequest { /// /// Source code to compile. /// public string SourceCode { get; set; } /// /// Name for the generated assembly (should be unique). /// public string AssemblyName { get; set; } /// /// Additional assembly prefixes to include in metadata references. /// Useful for hot reload scenarios that need "Microsoft.CodeAnalysis" etc. /// public string[] AdditionalAssemblyPrefixes { get; set; } /// /// Line number offset to subtract from diagnostic line numbers. /// Used when source code has wrapper code that should be hidden from diagnostics. /// public int LineNumberOffset { get; set; } /// /// When true, emit a portable PDB and load the assembly with its symbols so an attached /// managed debugger can bind breakpoints. Compiles unoptimized. Default false (no symbols). /// public bool EmitDebugInformation { get; set; } /// /// Document path recorded in the syntax tree when is set. /// Source that is not covered by explicit #line directives maps to this path. /// public string DocumentPath { get; set; } } /// /// Result from Roslyn compilation. /// public class CompilationResult { /// /// Whether compilation was successful. /// public bool Success { get; set; } /// /// Compiled assembly (only available if Success = true). /// public Assembly Assembly { get; set; } /// /// Raw assembly bytes (for potential disk saving or caching). /// public byte[] AssemblyBytes { get; set; } /// /// Portable PDB bytes when was set; /// null otherwise. /// public byte[] PdbBytes { get; set; } /// /// Compilation diagnostics (errors, warnings, info). /// public List Diagnostics { get; set; } = new List(); } }