Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Unity.Pipeline.CodeReload;
|
||||
using Unity.Pipeline.Models;
|
||||
using Unity.Pipeline.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiles transformed code reload source using the shared RoslynCompilationService.
|
||||
/// Creates versioned DLLs and manages code reload assembly lifecycle.
|
||||
/// </summary>
|
||||
static class CodeReloadCompiler
|
||||
{
|
||||
/// <summary>
|
||||
/// Interpreter-targeted overrides (in-editor backend and push-to-player) carry an embedded
|
||||
/// PDB and #line mapping, so runtime errors report the user's source file and line instead
|
||||
/// of an IL offset. Costs a few KB per pushed override and compiles it unoptimized
|
||||
/// (irrelevant to interpreted execution). Always on; this knob exists only so tests can
|
||||
/// exercise the no-line-info error path — nothing in production writes it, and a domain
|
||||
/// reload resets it to true.
|
||||
/// </summary>
|
||||
internal static bool EmitSourceLineInfo = true;
|
||||
|
||||
#if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG)
|
||||
|
||||
private static readonly Dictionary<string, int> _versionTracker = new();
|
||||
private const string CodeReloadTempDir = "Temp/CodeReload";
|
||||
|
||||
/// <summary>
|
||||
/// Compile source code on main thread synchronously to avoid deadlocks.
|
||||
/// </summary>
|
||||
/// <param name="sourceCode">Code reload source code to compile.</param>
|
||||
/// <param name="baseFileName">Base filename for versioned assembly naming.</param>
|
||||
/// <param name="assemblyDir">Optional directory to save compiled assembly to disk.</param>
|
||||
/// <param name="emitPdb">Emit a portable PDB and load symbols so breakpoints can bind.</param>
|
||||
/// <param name="documentPath">Source document path recorded in the PDB (the original .cs file).</param>
|
||||
/// <param name="interpreterMethods">When set, the compiled bytes register with the interpreter backend instead of Assembly.Load.</param>
|
||||
/// <returns>The compilation result.</returns>
|
||||
internal static CodeReloadCompileResult CompileSourceCodeOnMainThread(string sourceCode, string baseFileName, string assemblyDir = null, bool emitPdb = false, string documentPath = null, InterpreterOverrideSet interpreterMethods = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var useInterpreter = interpreterMethods != null;
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"CodeReload: Starting synchronous source code compilation for {baseFileName}");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceCode))
|
||||
{
|
||||
return CodeReloadCompileResult.Failure(
|
||||
"Empty Source Code",
|
||||
"Source code cannot be empty",
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
// Generate versioned assembly name
|
||||
var nextVersion = GetNextVersion(baseFileName);
|
||||
var assemblyName = $"{baseFileName}_{nextVersion:D3}";
|
||||
|
||||
// Ensure temp directory exists
|
||||
Directory.CreateDirectory(CodeReloadTempDir);
|
||||
|
||||
// The interpreter path needs only the bytes (no Assembly.Load), with the PDB embedded
|
||||
// in them: the interpreter reads sequence points only from an embedded PDB and uses
|
||||
// them for source lines in runtime error messages.
|
||||
var compilationResult = CompileCodeReloadAssembly(sourceCode, assemblyName,
|
||||
emitPdb: emitPdb && !useInterpreter, documentPath, loadAssembly: !useInterpreter,
|
||||
embedPdb: useInterpreter && EmitSourceLineInfo);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
var dumpPath = DumpFailedSource(assemblyName, sourceCode);
|
||||
return CodeReloadCompileResult.Failure(
|
||||
"Compilation Failed",
|
||||
"Code reload source code compilation failed" +
|
||||
(dumpPath != null ? $" (generated source: {dumpPath})" : ""),
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
compilationResult.Diagnostics.Select(FormatDiagnostic).ToList());
|
||||
}
|
||||
|
||||
List<string> registeredMethods;
|
||||
List<string> skippedOverrides;
|
||||
if (useInterpreter)
|
||||
{
|
||||
registeredMethods = InterpreterCodeReloadExecutor.Register(
|
||||
compilationResult.AssemblyBytes, interpreterMethods.TypeName, interpreterMethods.MethodNames,
|
||||
out skippedOverrides, out var bindingWarnings);
|
||||
// The reload response carries one flat Diagnostics list; append binding warnings
|
||||
// (unbound host members that throw only if reached) so the client sees the gap
|
||||
// in the same reply that reports the reload as applied.
|
||||
skippedOverrides.AddRange(bindingWarnings);
|
||||
}
|
||||
else
|
||||
{
|
||||
registeredMethods = RegisterCodeReloadMethods(compilationResult.Assembly, assemblyName, out skippedOverrides);
|
||||
}
|
||||
|
||||
// Save assembly to disk if assemblyDir is specified
|
||||
string actualAssemblyPath = null;
|
||||
if (!string.IsNullOrWhiteSpace(assemblyDir))
|
||||
{
|
||||
Directory.CreateDirectory(assemblyDir);
|
||||
actualAssemblyPath = Path.Combine(assemblyDir, $"{assemblyName}.dll");
|
||||
File.WriteAllBytes(actualAssemblyPath, compilationResult.AssemblyBytes);
|
||||
Debug.Log($"CodeReload: Assembly saved to disk: {actualAssemblyPath}");
|
||||
|
||||
// Symbols are loaded in-memory; also write the .pdb next to the .dll for convenience.
|
||||
if (compilationResult.PdbBytes != null)
|
||||
{
|
||||
var pdbPath = Path.Combine(assemblyDir, $"{assemblyName}.pdb");
|
||||
File.WriteAllBytes(pdbPath, compilationResult.PdbBytes);
|
||||
Debug.Log($"CodeReload: Symbols saved to disk: {pdbPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAssemblyPath = Path.Combine(CodeReloadTempDir, $"{assemblyName}.dll"); // For compatibility (in-memory)
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"CodeReload: Successfully compiled source code -> {assemblyName}.dll with {registeredMethods.Count} methods in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
var sourceCompileResult = CodeReloadCompileResult.Success(
|
||||
assemblyName,
|
||||
actualAssemblyPath,
|
||||
registeredMethods,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
sourceCompileResult.Diagnostics = skippedOverrides;
|
||||
return sourceCompileResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"CodeReload: Synchronous source code compilation failed for {baseFileName}: {ex.Message}");
|
||||
return CodeReloadCompileResult.Failure(
|
||||
"Exception",
|
||||
ex.ToString(),
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up old code reload DLL versions, keeping only the latest for each base filename.
|
||||
/// </summary>
|
||||
/// <param name="assemblyDir">Directory containing code-reload assemblies, or null for the default temp directory.</param>
|
||||
/// <returns>Which files were deleted, and whether cleanup succeeded.</returns>
|
||||
public static CodeReloadCleanupResult CleanupCodeReloadDlls(string assemblyDir = null)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var deletedFiles = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
// Use specified assemblyDir or default to CodeReloadTempDir
|
||||
var targetDir = !string.IsNullOrWhiteSpace(assemblyDir) ? assemblyDir : CodeReloadTempDir;
|
||||
|
||||
if (!Directory.Exists(targetDir))
|
||||
{
|
||||
return new CodeReloadCleanupResult
|
||||
{
|
||||
Success = true,
|
||||
DeletedFiles = deletedFiles,
|
||||
Message = $"No code reload directory found: {targetDir}",
|
||||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
|
||||
var dllFiles = Directory.GetFiles(targetDir, "*.dll");
|
||||
var groupedFiles = dllFiles
|
||||
.Select(f => new
|
||||
{
|
||||
FilePath = f,
|
||||
FileName = Path.GetFileNameWithoutExtension(f),
|
||||
BaseFilename = ExtractBaseFilename(Path.GetFileNameWithoutExtension(f)),
|
||||
Version = ExtractVersion(Path.GetFileNameWithoutExtension(f))
|
||||
})
|
||||
.Where(f => f.Version > 0) // Only versioned code reload DLLs
|
||||
.GroupBy(f => f.BaseFilename)
|
||||
.ToList();
|
||||
|
||||
foreach (var group in groupedFiles)
|
||||
{
|
||||
var sortedFiles = group.OrderByDescending(f => f.Version).ToList();
|
||||
|
||||
// Keep the latest version, delete older ones
|
||||
for (int i = 1; i < sortedFiles.Count; i++)
|
||||
{
|
||||
var fileToDelete = sortedFiles[i];
|
||||
File.Delete(fileToDelete.FilePath);
|
||||
deletedFiles.Add(fileToDelete.FileName + ".dll");
|
||||
Debug.Log($"CodeReload: Deleted old version {fileToDelete.FileName}.dll");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear registry and reset version tracker
|
||||
CodeReloadRegistry.ClearAllOverrides();
|
||||
_versionTracker.Clear();
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
Debug.Log($"CodeReload: Cleanup completed - deleted {deletedFiles.Count} files in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
return new CodeReloadCleanupResult
|
||||
{
|
||||
Success = true,
|
||||
DeletedFiles = deletedFiles,
|
||||
Message = $"Deleted {deletedFiles.Count} old DLL versions",
|
||||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
Debug.LogError($"CodeReload: Cleanup failed: {ex.Message}");
|
||||
|
||||
return new CodeReloadCleanupResult
|
||||
{
|
||||
Success = false,
|
||||
DeletedFiles = deletedFiles,
|
||||
Message = $"Cleanup failed: {ex.Message}",
|
||||
ExecutionTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next version number for a base filename.
|
||||
/// </summary>
|
||||
private static int GetNextVersion(string baseFilename)
|
||||
{
|
||||
if (!_versionTracker.ContainsKey(baseFilename))
|
||||
{
|
||||
_versionTracker[baseFilename] = 0;
|
||||
}
|
||||
|
||||
return ++_versionTracker[baseFilename];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile transformed override source to raw assembly bytes without loading or registering
|
||||
/// anything, for the push-to-player path (the device runs the bytes through the interpreter).
|
||||
/// The PDB is embedded so the interpreter can report source lines instead of IL offsets;
|
||||
/// <paramref name="documentPath"/> is recorded for source outside the transform's #line
|
||||
/// directives.
|
||||
/// </summary>
|
||||
internal static bool TryCompileOverrideBytes(string transformedCode, string baseFileName, out byte[] ilBytes, out List<string> diagnostics, string documentPath = null, string[] preprocessorSymbols = null)
|
||||
{
|
||||
ilBytes = null;
|
||||
diagnostics = new List<string>();
|
||||
|
||||
var assemblyName = $"{baseFileName}_{GetNextVersion(baseFileName):D3}";
|
||||
var result = CompileCodeReloadAssembly(transformedCode, assemblyName, emitPdb: false, documentPath: documentPath, loadAssembly: false, embedPdb: EmitSourceLineInfo, preprocessorSymbols: preprocessorSymbols);
|
||||
if (!result.Success)
|
||||
{
|
||||
diagnostics = result.Diagnostics.Select(FormatDiagnostic).ToList();
|
||||
var dumpPath = DumpFailedSource(assemblyName, transformedCode);
|
||||
if (dumpPath != null)
|
||||
diagnostics.Add($"Generated source written to {dumpPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
ilBytes = result.AssemblyBytes;
|
||||
return ilBytes != null && ilBytes.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One diagnostic as the user should read it: the line of THEIR file it maps to (via the
|
||||
/// transform's #line directives), the compiler message, and the generated line that
|
||||
/// actually failed. The generated source differs from what the user wrote (bodies are
|
||||
/// lifted into a static class, members re-qualified), so a bare compiler message such as
|
||||
/// "the type name 'var' does not exist in the type 'Foo'" is not debuggable on its own.
|
||||
/// </summary>
|
||||
internal static string FormatDiagnostic(DiagnosticInfo d)
|
||||
{
|
||||
var where = d.File != null
|
||||
? $"{Path.GetFileName(d.File)}({d.FileLine})"
|
||||
: $"generated line {d.Line + 1}";
|
||||
var text = $"{where}: {d.Severity} {d.Id}: {d.Message}";
|
||||
if (!string.IsNullOrEmpty(d.Source))
|
||||
text += $"\n generated line {d.Line + 1}: {d.Source}";
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the generated override source next to the versioned DLLs so a failed compile can
|
||||
/// be inspected whole. Returns the path, or null when the write failed (never fatal).
|
||||
/// </summary>
|
||||
private static string DumpFailedSource(string assemblyName, string sourceCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(CodeReloadTempDir);
|
||||
var path = Path.Combine(CodeReloadTempDir, $"{assemblyName}.failed.cs");
|
||||
File.WriteAllText(path, sourceCode);
|
||||
return path;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile code reload source code using shared RoslynCompilationService.
|
||||
/// Synchronous compilation for main thread use.
|
||||
/// </summary>
|
||||
private static CompilationResult CompileCodeReloadAssembly(string sourceCode, string assemblyName, bool emitPdb = false, string documentPath = null, bool loadAssembly = true, bool embedPdb = false, string[] preprocessorSymbols = null)
|
||||
{
|
||||
var request = new CompilationRequest
|
||||
{
|
||||
// Null = the project's define set; the push path passes the player's defines.
|
||||
PreprocessorSymbols = preprocessorSymbols,
|
||||
SourceCode = sourceCode,
|
||||
AssemblyName = assemblyName,
|
||||
// Ensure Unity.Pipeline assemblies are included (contains CodeReloadOverrideMethodAttribute)
|
||||
// Also include test assemblies for testing scenarios
|
||||
AdditionalAssemblyPrefixes = new[] { "Unity.Pipeline", "Unity.Pipeline.Tests" },
|
||||
EmitDebugInformation = emitPdb,
|
||||
EmbedDebugInformation = embedPdb,
|
||||
DocumentPath = documentPath,
|
||||
// The interpreter path runs the bytes directly; skip Assembly.Load (IL2CPP-unsafe).
|
||||
SkipLoad = !loadAssembly,
|
||||
// Interpreter-bound bytes only: the interpreter reaches non-public members via
|
||||
// reflection. Loaded assemblies keep standard access checks — Mono JIT-enforces
|
||||
// accessibility on Assembly.Load'd IL, so relaxing them would only trade a compile
|
||||
// error for a throw at first dispatch (see AccessibilityValidator).
|
||||
AllowNonPublicMemberAccess = !loadAssembly
|
||||
};
|
||||
|
||||
return RoslynCompilationService.Compile(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the [CodeReloadOverrideMethod] methods of a compiled assembly with the registry.
|
||||
/// Only overrides that actually bind are returned; overrides that were skipped (e.g. the
|
||||
/// target is not a registered [CodeReload] method, or a signature mismatch) are reported via
|
||||
/// <paramref name="skipped"/> with a user-facing reason.
|
||||
/// </summary>
|
||||
private static List<string> RegisterCodeReloadMethods(Assembly assembly, string assemblyId, out List<string> skipped)
|
||||
{
|
||||
var registeredMethods = new List<string>();
|
||||
skipped = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
// Register assembly types for discovery
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
CodeReloadRegistry.RegisterCodeReloadType(type, assemblyId);
|
||||
|
||||
// Find methods with [CodeReloadOverrideMethod] attribute for overrides
|
||||
var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
foreach (var method in staticMethods)
|
||||
{
|
||||
// Try multiple ways to find the attribute
|
||||
var codeReloadAttr = method.GetCustomAttribute<CodeReloadOverrideMethodAttribute>();
|
||||
if (codeReloadAttr == null)
|
||||
{
|
||||
// Try by name in case of type loading issues
|
||||
var attrByName = method.GetCustomAttributes(true)
|
||||
.FirstOrDefault(a => a.GetType().Name == "CodeReloadOverrideMethodAttribute");
|
||||
if (attrByName != null)
|
||||
{
|
||||
// Cast to the attribute type
|
||||
codeReloadAttr = attrByName as CodeReloadOverrideMethodAttribute;
|
||||
}
|
||||
}
|
||||
|
||||
if (codeReloadAttr != null)
|
||||
{
|
||||
if (CodeReloadRegistry.RegisterMethodOverride(method, codeReloadAttr, type, out var skipReason))
|
||||
{
|
||||
registeredMethods.Add(codeReloadAttr.TargetMethodId);
|
||||
}
|
||||
else
|
||||
{
|
||||
skipped.Add($"{method.Name} -> {codeReloadAttr.TargetMethodId}: {skipReason}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"CodeReload: Registered {registeredMethods.Count} override methods: {string.Join(", ", registeredMethods)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"CodeReload: Error registering methods from assembly {assemblyId}: {ex.Message}");
|
||||
Debug.LogError($"CodeReload: Stack trace: {ex.StackTrace}");
|
||||
}
|
||||
|
||||
return registeredMethods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract base filename from versioned filename (e.g., "PlayerMovement_001" -> "PlayerMovement").
|
||||
/// </summary>
|
||||
private static string ExtractBaseFilename(string versionedFilename)
|
||||
{
|
||||
var lastUnderscoreIndex = versionedFilename.LastIndexOf('_');
|
||||
if (lastUnderscoreIndex > 0)
|
||||
{
|
||||
var potentialVersion = versionedFilename.Substring(lastUnderscoreIndex + 1);
|
||||
if (int.TryParse(potentialVersion, out _))
|
||||
{
|
||||
return versionedFilename.Substring(0, lastUnderscoreIndex);
|
||||
}
|
||||
}
|
||||
return versionedFilename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract version number from versioned filename (e.g., "PlayerMovement_001" -> 1).
|
||||
/// </summary>
|
||||
private static int ExtractVersion(string versionedFilename)
|
||||
{
|
||||
var lastUnderscoreIndex = versionedFilename.LastIndexOf('_');
|
||||
if (lastUnderscoreIndex > 0)
|
||||
{
|
||||
var potentialVersion = versionedFilename.Substring(lastUnderscoreIndex + 1);
|
||||
if (int.TryParse(potentialVersion, out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else
|
||||
// Code reload requires Roslyn compilation, which is only available in the Editor and in
|
||||
// Desktop development builds. In all other builds the methods below are compiled instead,
|
||||
// matching the surface used by callers (CodeReloadCommands, InPlaceReloadProcessor)
|
||||
// so the project still builds, and returning a clear "not supported" failure at runtime.
|
||||
const string NotSupportedMessage =
|
||||
"Code reload compilation is only supported on Desktop development builds (Windows/Mac/Linux).";
|
||||
|
||||
/// <summary>
|
||||
/// Code reload source compilation (in-place editing) not supported on this build.
|
||||
/// </summary>
|
||||
/// <param name="sourceCode">Unused on this build.</param>
|
||||
/// <param name="baseFileName">Unused on this build.</param>
|
||||
/// <param name="assemblyDir">Unused on this build.</param>
|
||||
/// <param name="emitPdb">Unused on this build.</param>
|
||||
/// <param name="documentPath">Unused on this build.</param>
|
||||
/// <param name="interpreterMethods">Unused on this build.</param>
|
||||
/// <returns>A "Platform Not Supported" failure.</returns>
|
||||
internal static CodeReloadCompileResult CompileSourceCodeOnMainThread(string sourceCode, string baseFileName, string assemblyDir = null, bool emitPdb = false, string documentPath = null, InterpreterOverrideSet interpreterMethods = null)
|
||||
{
|
||||
return CodeReloadCompileResult.Failure("Platform Not Supported", NotSupportedMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override compilation to bytes (push path) not supported on this build (no Roslyn).
|
||||
/// </summary>
|
||||
/// <param name="transformedCode">Unused on this build.</param>
|
||||
/// <param name="baseFileName">Unused on this build.</param>
|
||||
/// <param name="ilBytes">Always null on this build.</param>
|
||||
/// <param name="diagnostics">Always contains the "not supported" message on this build.</param>
|
||||
/// <param name="documentPath">Unused on this build.</param>
|
||||
/// <param name="preprocessorSymbols">Unused on this build.</param>
|
||||
/// <returns>Always false on this build.</returns>
|
||||
internal static bool TryCompileOverrideBytes(string transformedCode, string baseFileName, out byte[] ilBytes, out List<string> diagnostics, string documentPath = null, string[] preprocessorSymbols = null)
|
||||
{
|
||||
ilBytes = null;
|
||||
diagnostics = new List<string> { NotSupportedMessage };
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code reload cleanup not supported on this build.
|
||||
/// </summary>
|
||||
/// <param name="assemblyDir">Unused on this build.</param>
|
||||
/// <returns>A failure result explaining code reload isn't supported on this build.</returns>
|
||||
public static CodeReloadCleanupResult CleanupCodeReloadDlls(string assemblyDir = null)
|
||||
{
|
||||
return new CodeReloadCleanupResult
|
||||
{
|
||||
Success = false,
|
||||
Message = NotSupportedMessage,
|
||||
ExecutionTimeMs = 0
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The override set an interpreter-backed compile registers instead of Assembly.Load: the
|
||||
/// reloaded type plus its surviving method names. Passing one to
|
||||
/// <see cref="CodeReloadCompiler.CompileSourceCodeOnMainThread"/> selects the interpreter path.
|
||||
/// </summary>
|
||||
internal class InterpreterOverrideSet
|
||||
{
|
||||
public string TypeName { get; set; }
|
||||
public List<string> MethodNames { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from code reload compilation operation.
|
||||
/// </summary>
|
||||
class CodeReloadCompileResult
|
||||
{
|
||||
/// <summary>Whether compilation succeeded.</summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
/// <summary>Name of the compiled code-reload assembly.</summary>
|
||||
public string AssemblyName { get; set; }
|
||||
/// <summary>Path the assembly was written to, if saved to disk.</summary>
|
||||
public string OutputPath { get; set; }
|
||||
/// <summary>Method ids registered as overrides.</summary>
|
||||
public List<string> RegisteredMethods { get; set; } = new List<string>();
|
||||
/// <summary>How long compilation took, in milliseconds.</summary>
|
||||
public long ExecutionTimeMs { get; set; }
|
||||
/// <summary>Error message, if compilation failed.</summary>
|
||||
public string Error { get; set; }
|
||||
/// <summary>Additional error details for debugging.</summary>
|
||||
public string ErrorDetails { get; set; }
|
||||
/// <summary>Compiler diagnostics (errors/warnings), if any.</summary>
|
||||
public List<string> Diagnostics { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>Create a successful compile result.</summary>
|
||||
/// <param name="assemblyName">Name of the compiled assembly.</param>
|
||||
/// <param name="outputPath">Path the assembly was written to, if saved to disk.</param>
|
||||
/// <param name="registeredMethods">Method ids registered as overrides.</param>
|
||||
/// <param name="executionTimeMs">How long compilation took, in milliseconds.</param>
|
||||
/// <returns>A successful result.</returns>
|
||||
public static CodeReloadCompileResult Success(string assemblyName, string outputPath, List<string> registeredMethods, long executionTimeMs)
|
||||
{
|
||||
return new CodeReloadCompileResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
AssemblyName = assemblyName,
|
||||
OutputPath = outputPath,
|
||||
RegisteredMethods = registeredMethods,
|
||||
ExecutionTimeMs = executionTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Create a failed compile result.</summary>
|
||||
/// <param name="error">Error message.</param>
|
||||
/// <param name="errorDetails">Additional error details for debugging.</param>
|
||||
/// <param name="executionTimeMs">How long compilation took, in milliseconds.</param>
|
||||
/// <param name="diagnostics">Compiler diagnostics (errors/warnings), if any.</param>
|
||||
/// <returns>A failed result.</returns>
|
||||
public static CodeReloadCompileResult Failure(string error, string errorDetails, long executionTimeMs = 0, List<string> diagnostics = null)
|
||||
{
|
||||
return new CodeReloadCompileResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = error,
|
||||
ErrorDetails = errorDetails,
|
||||
ExecutionTimeMs = executionTimeMs,
|
||||
Diagnostics = diagnostics ?? new List<string>()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from code reload cleanup operation.
|
||||
/// </summary>
|
||||
class CodeReloadCleanupResult
|
||||
{
|
||||
/// <summary>Whether cleanup succeeded.</summary>
|
||||
public bool Success { get; set; }
|
||||
/// <summary>Paths of the deleted files.</summary>
|
||||
public List<string> DeletedFiles { get; set; } = new List<string>();
|
||||
/// <summary>Human-readable summary or error message.</summary>
|
||||
public string Message { get; set; }
|
||||
/// <summary>How long cleanup took, in milliseconds.</summary>
|
||||
public long ExecutionTimeMs { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29ede4b57fcc6c64b84e5f6ee75df373
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,335 @@
|
||||
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>
|
||||
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>
|
||||
/// <param name="code">The C# expression/statement(s) to evaluate.</param>
|
||||
/// <param name="timeoutMs">Timeout for compilation, in milliseconds.</param>
|
||||
/// <param name="stopwatch">Running stopwatch used to report execution time in the response.</param>
|
||||
/// <returns>The evaluation result.</returns>
|
||||
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>
|
||||
/// <param name="code">Unused on this build.</param>
|
||||
/// <param name="timeoutMs">Unused on this build.</param>
|
||||
/// <param name="stopwatch">Unused on this build.</param>
|
||||
/// <returns>A failure result explaining runtime compilation isn't supported on this build.</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ea6a44edd711194a968a40f1eb16f59
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared IlInterpreter <see cref="IlInterpreter.Interpreter.HostBinding"/> surface used by the
|
||||
/// interpreter-backed code-reload path. Mirrors IlInterpreter's
|
||||
/// <c>UnityHostBinding.Prototyping()</c> minus the Input System block, Audio, and physics —
|
||||
/// anything outside UnityEngine's core module binds on demand via <see cref="ResolveAutoBindType"/>,
|
||||
/// so the package forces no engine-module dependency. Reference types use <c>AllowType<T>()</c>
|
||||
/// and blittable structs <c>AllowTypeStruct<T>()</c> for AOT/IL2CPP safety; static classes
|
||||
/// and enums use <c>AllowType(typeof(...))</c>.
|
||||
/// </summary>
|
||||
static class IlInterpreterHostBindings
|
||||
{
|
||||
// Used by the interpreter-backed code-reload sink; available in dev players (incl. IL2CPP).
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD || ENABLE_RUNTIME_PIPELINE
|
||||
/// <summary>
|
||||
/// Single source of truth for the interpreter's host surface: BCL + the standard UnityEngine
|
||||
/// types + <c>Debug</c>. Every consumer — eval binding, code-reload binding, and the build-time
|
||||
/// <c>link.xml</c> preservation set — is a projection of this one builder, so they cannot
|
||||
/// drift. Callers overlay only what is site-specific: eval overlays capturing <c>Debug.Log</c>
|
||||
/// variants; code reload overlays <c>AllowType(targetType)</c>; link.xml reads
|
||||
/// <see cref="IlInterpreter.Interpreter.HostBinding.RegisteredTypes"/> off the result.
|
||||
/// </summary>
|
||||
public static IlInterpreter.Interpreter.HostBinding CreateStandard()
|
||||
{
|
||||
// Route IlInterpreter registration warnings to the Unity console (the default sink is
|
||||
// stderr, which Unity players never surface).
|
||||
IlInterpreter.Interpreter.HostBinding.Warn = msg => Debug.LogWarning(msg);
|
||||
|
||||
var binding = AddStandardUnity(new IlInterpreter.Interpreter.HostBinding().AllowBcl())
|
||||
.AllowType(typeof(Debug)); // logging from eval/reloaded bodies (eval overlays capture on top)
|
||||
// Demand-time auto-bind: the interpreter resolves every TypeRef a loaded script carries
|
||||
// through this policy, so engine/game types outside the curated surface bind on first use
|
||||
// instead of throwing "not registered". The curated list above stays the fast-path core
|
||||
// (typed delegates, flat structs) and the link.xml baseline — a floor, not a ceiling.
|
||||
// Types the policy declines (see ResolveAutoBindType) are not bindable — the
|
||||
// project-extension seam that could promote them was deferred to a future release.
|
||||
binding.AutoBindResolver = ResolveAutoBindType;
|
||||
return binding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-bind policy for <see cref="CreateStandard"/>: resolve a CLR full name from the loaded
|
||||
/// assemblies, skipping (a) the BCL — the hand-rolled <c>AllowBcl</c> shims collapse doubles
|
||||
/// into the script's float number space and must stay authoritative — and (b) this package's
|
||||
/// own assemblies. On IL2CPP dev players this self-limits to types the stripper kept (see the
|
||||
/// dev-build link.xml widening in <c>CodeReloadLinkXmlGenerator</c>).
|
||||
/// </summary>
|
||||
static Type ResolveAutoBindType(string fullName)
|
||||
{
|
||||
foreach (var asm in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
if (IsAutoBindSkippedAssembly(asm.GetName().Name ?? string.Empty)) continue;
|
||||
Type t = null;
|
||||
try { t = asm.GetType(fullName, throwOnError: false); } catch { }
|
||||
if (t != null) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Internal for tests: the classification itself is the contract (a misclassified game
|
||||
// assembly silently loses auto-bind), and no test can load a real assembly by these names.
|
||||
internal static bool IsAutoBindSkippedAssembly(string name) =>
|
||||
name == "System" ||
|
||||
name.StartsWith("System.", StringComparison.Ordinal) ||
|
||||
name.StartsWith("Microsoft.", StringComparison.Ordinal) ||
|
||||
name.StartsWith("Mono.", StringComparison.Ordinal) ||
|
||||
name == "mscorlib" ||
|
||||
name == "netstandard" ||
|
||||
name.StartsWith("nunit.", StringComparison.Ordinal) ||
|
||||
name.StartsWith("Unity.Pipeline", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Register the standard UnityEngine type surface onto <paramref name="binding"/> (fluent).
|
||||
/// Private: <see cref="CreateStandard"/> is the only supported way to build the surface,
|
||||
/// closing the compose-it-yourself seam where per-site drift used to enter.
|
||||
/// </summary>
|
||||
static IlInterpreter.Interpreter.HostBinding AddStandardUnity(IlInterpreter.Interpreter.HostBinding binding)
|
||||
{
|
||||
return binding
|
||||
.AllowType(typeof(Resources))
|
||||
.AllowType(typeof(PrimitiveType))
|
||||
// The base Object surface carries the fake-null operators (op_Equality,
|
||||
// op_Implicit -> bool) every `target == null` / `if (component)` guard compiles
|
||||
// against, plus Destroy/Instantiate/name. Explicit, not auto-bound: the guards
|
||||
// must work on IL2CPP without relying on the on-demand resolver.
|
||||
.AllowType<UnityEngine.Object>()
|
||||
.AllowType<GameObject>()
|
||||
.AllowType<Transform>()
|
||||
.AllowType<Camera>()
|
||||
.AllowType<Renderer>()
|
||||
.AllowType<MeshRenderer>()
|
||||
.AllowType<SpriteRenderer>()
|
||||
.AllowType<LineRenderer>()
|
||||
.AllowType<TrailRenderer>()
|
||||
.AllowType<Material>()
|
||||
.AllowType<TextMesh>()
|
||||
.AllowType<Sprite>()
|
||||
.AllowType(typeof(Shader)) // Shader.Find for material creation
|
||||
.AllowType(typeof(CameraClearFlags)) // cam.clearFlags
|
||||
.AllowType(typeof(TextAnchor)) // TextMesh.anchor
|
||||
.AllowType(typeof(TextAlignment)) // TextMesh.alignment
|
||||
.AllowType<Vector2>()
|
||||
.AllowTypeStruct<Vector3>()
|
||||
.AllowType<Quaternion>()
|
||||
.AllowType<Color>()
|
||||
.AllowType(typeof(Mathf))
|
||||
.AllowType(typeof(Time))
|
||||
// 3D physics (Rigidbody, Collider, Physics, ForceMode, RaycastHit, ...) is NOT
|
||||
// registered here: those types live in UnityEngine.PhysicsModule, and a compile-time
|
||||
// reference would force a physics module dependency on every consumer. They bind on
|
||||
// demand through AutoBindResolver instead — reflection-tier, and present in a player
|
||||
// only when the game's own code kept the module alive.
|
||||
.AllowType(typeof(Space))
|
||||
.AllowType<Ray>()
|
||||
.AllowType(typeof(LayerMask))
|
||||
.AllowType(typeof(Cursor))
|
||||
.AllowType(typeof(CursorLockMode))
|
||||
.AllowType(typeof(Screen))
|
||||
.AllowType(typeof(Rect))
|
||||
.AllowType(typeof(ScreenOrientation))
|
||||
.AllowType(typeof(Application))
|
||||
.AllowType(typeof(PlayerPrefs))
|
||||
.AllowType(typeof(Color32))
|
||||
.AllowType(typeof(System.Collections.IEnumerator))
|
||||
.AllowType(typeof(UnityEngine.Random))
|
||||
// UITK value-changed callbacks read evt.newValue/previousValue on ChangeEvent<T>.
|
||||
// Those are members on an OPEN generic type, which auto-bind can't resolve at Load
|
||||
// (there's no closed instantiation to invoke against) — so reflect on the receiver's
|
||||
// concrete type at call time. Covers Slider/Toggle/TextField/DropdownField/... the
|
||||
// whole RegisterValueChangedCallback surface, without a compile-time UITK dependency.
|
||||
.Allow("ChangeEvent`1", "get_newValue",
|
||||
(recv, _) => recv?.GetType().GetProperty("newValue")?.GetValue(recv), 0)
|
||||
.Allow("ChangeEvent`1", "get_previousValue",
|
||||
(recv, _) => recv?.GetType().GetProperty("previousValue")?.GetValue(recv), 0)
|
||||
// AllowType registers a type's methods/fields/props but not its constructors, so the
|
||||
// common ctors are wired explicitly (Vector3's comes from AllowTypeStruct above).
|
||||
.AllowConstructor("Vector2",
|
||||
(_, args) => (object)new Vector2(Convert.ToSingle(args[0]), Convert.ToSingle(args[1])), 2)
|
||||
.AllowConstructor("Color",
|
||||
(_, args) => (object)new Color(Convert.ToSingle(args[0]), Convert.ToSingle(args[1]), Convert.ToSingle(args[2]), Convert.ToSingle(args[3])), 4)
|
||||
.AllowConstructor("GameObject", (_, args) => (object)new GameObject(), 0)
|
||||
.AllowConstructor("GameObject", (_, args) => (object)new GameObject((string)args[0]), 1)
|
||||
.AllowConstructor("Material", (_, args) => (object)new Material((Shader)args[0]), 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb251aef26c42704ca3d81d67fd13fc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
// The package bundles Roslyn renamed with a "UnityPipeline." prefix (Runtime/Plugins/CodeAnalysis);
|
||||
// the dotnet test projects reference the same DLLs (IlInterpreter/BundledRoslyn.props).
|
||||
using UnityPipeline.Microsoft.CodeAnalysis;
|
||||
using UnityPipeline.Microsoft.CodeAnalysis.CSharp;
|
||||
using UnityPipeline.Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Rewrites C# string interpolation (<c>$"..."</c>) into <c>string.Format(fmt, args…)</c> at the
|
||||
/// source level: the interpreter can't run the compiler's normal interpolation lowering
|
||||
/// (<c>DefaultInterpolatedStringHandler</c>, a ref struct), but it can call <c>String.Format</c>
|
||||
/// (bound in <see cref="IlInterpreter.Interpreter.HostBinding"/>). Applied by
|
||||
/// <see cref="SourceCodeTransformer"/> before an in-place reload body is compiled.
|
||||
///
|
||||
/// Purely syntactic — no semantic model. Format specifiers, alignment, escaped braces, verbatim
|
||||
/// strings and nested interpolation are handled. The one unserved case is an interpolation whose
|
||||
/// target type is <c>FormattableString</c>/<c>IFormattable</c>: rewriting yields a <c>string</c>,
|
||||
/// so that rare usage becomes a compile error rather than a silent wrong result.
|
||||
/// </summary>
|
||||
sealed class InterpolationToFormatRewriter : CSharpSyntaxRewriter
|
||||
{
|
||||
public override SyntaxNode VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node)
|
||||
{
|
||||
var fmt = new StringBuilder();
|
||||
var args = new List<ArgumentSyntax>();
|
||||
int index = 0;
|
||||
|
||||
foreach (var content in node.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case InterpolatedStringTextSyntax text:
|
||||
// ValueText decodes escapes (\t etc.) but leaves literal braces doubled
|
||||
// ({{ / }}) — exactly the escaping a Format string wants, so append as-is.
|
||||
fmt.Append(text.TextToken.ValueText);
|
||||
break;
|
||||
|
||||
case InterpolationSyntax interp:
|
||||
fmt.Append('{').Append(index++);
|
||||
if (interp.AlignmentClause != null)
|
||||
fmt.Append(',').Append(interp.AlignmentClause.Value.ToString().Trim());
|
||||
if (interp.FormatClause != null)
|
||||
fmt.Append(':').Append(interp.FormatClause.FormatStringToken.ValueText);
|
||||
fmt.Append('}');
|
||||
// Visit the hole so nested interpolations rewrite bottom-up and later
|
||||
// member-qualification passes see it.
|
||||
args.Add(SyntaxFactory.Argument((ExpressionSyntax)Visit(interp.Expression)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No holes: emit a plain string literal (no Format, no brace doubling), matching how
|
||||
// Roslyn treats a hole-less interpolated string.
|
||||
if (index == 0)
|
||||
{
|
||||
var raw = new StringBuilder();
|
||||
foreach (var content in node.Contents)
|
||||
if (content is InterpolatedStringTextSyntax t)
|
||||
// A plain literal wants single braces, so undo the {{ / }} escaping ValueText keeps.
|
||||
raw.Append(t.TextToken.ValueText.Replace("{{", "{").Replace("}}", "}"));
|
||||
return SyntaxFactory.LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(raw.ToString()))
|
||||
.WithTriviaFrom(node);
|
||||
}
|
||||
|
||||
var allArgs = new List<ArgumentSyntax>(args.Count + 1)
|
||||
{
|
||||
SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(fmt.ToString()))),
|
||||
};
|
||||
allArgs.AddRange(args);
|
||||
|
||||
// Passing holes as loose args lets Roslyn pick the Format overload: fixed arity for ≤3,
|
||||
// params object[] for ≥4 — both are bound on the interpreter side.
|
||||
return SyntaxFactory.InvocationExpression(
|
||||
SyntaxFactory.ParseExpression("string.Format"),
|
||||
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(allArgs)))
|
||||
.WithTriviaFrom(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d10aa4b9388057a4f96c43b0c41c35d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.CodeReload;
|
||||
using IlInterpreter.Interpreter;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// IL2CPP-safe in-place code reload backend: instead of <c>Assembly.Load</c> + reflection, the
|
||||
/// transformed override assembly bytes run in the IlInterpreter interpreter, and each override is
|
||||
/// registered as an interpreter dispatch in <see cref="CodeReloadRegistry"/>.
|
||||
///
|
||||
/// Override methods are <c>static {ret} {Method}({TargetType} instance, ...)</c>, dispatched as
|
||||
/// <c>interpreter.Invoke("{Method}", instance, ...args)</c>. The host binding is the shared
|
||||
/// standard surface plus the target type — public and non-public members (the override is
|
||||
/// compiled with access checks disabled). Other referenced types bind on demand during Load via
|
||||
/// the binding's <c>AutoBindResolver</c>.
|
||||
///
|
||||
/// Inherits the interpreter's limits: constructs IlInterpreter doesn't lower yet throw at dispatch
|
||||
/// time, and <see cref="CodeReloadRegistry.TryInvokeCodeReload"/> falls back to the original
|
||||
/// compiled body for that call. Member refs unbound at Load are reported through the
|
||||
/// <c>warnings</c> out param so the reload response shows the gap before play-mode execution hits it.
|
||||
/// </summary>
|
||||
static class InterpreterCodeReloadExecutor
|
||||
{
|
||||
// Editor and any development player (incl. IL2CPP on device): the interpreter runs pushed IL
|
||||
// bytes without Assembly.Load or Roslyn.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD || ENABLE_RUNTIME_PIPELINE
|
||||
|
||||
/// <summary>
|
||||
/// Load <paramref name="assemblyBytes"/> into an interpreter and register an interpreter-backed
|
||||
/// override for each <paramref name="methodNames"/> entry on <paramref name="typeName"/>.
|
||||
/// A name with no woven dispatch target (the method — or its whole type — was added since
|
||||
/// the running build was compiled) cannot be dispatched from compiled code and is reported
|
||||
/// through <paramref name="skipped"/>; it still loads into the interpreter, so other
|
||||
/// reloaded bodies in the same file can call it.
|
||||
/// Returns the method ids that bound; <paramref name="skipped"/> carries
|
||||
/// reasons for any that didn't; <paramref name="warnings"/> carries non-fatal load-time
|
||||
/// diagnostics (host members that resolved to throwing stubs — the reload still applies, and
|
||||
/// a stub only throws if its call site is reached).
|
||||
/// </summary>
|
||||
public static List<string> Register(
|
||||
byte[] assemblyBytes, string typeName, IEnumerable<string> methodNames,
|
||||
out List<string> skipped, out List<string> warnings)
|
||||
{
|
||||
var registered = new List<string>();
|
||||
skipped = new List<string>();
|
||||
warnings = new List<string>();
|
||||
|
||||
var names = new List<string>(methodNames ?? Array.Empty<string>());
|
||||
if (assemblyBytes == null || names.Count == 0)
|
||||
return registered;
|
||||
|
||||
// All methods in one in-place file share the target type, but not every NAME resolves:
|
||||
// a [CodeReload] method added since the last compile has no woven prologue to dispatch
|
||||
// from, so its id is not in the registry. Resolve the type from the first name that is;
|
||||
// the rest load but only intra-file calls reach them (reported below). A file whose type
|
||||
// was never compiled resolves nothing — targetType stays null and the binding runs
|
||||
// without the target-type overlay.
|
||||
Type targetType = null;
|
||||
var resolvable = new List<string>();
|
||||
var unwovenNames = new List<string>();
|
||||
foreach (var m in names)
|
||||
{
|
||||
if (CodeReloadRegistry.TryGetReloadableDeclaringType($"{typeName}.{m}", out var t))
|
||||
{
|
||||
targetType = targetType ?? t;
|
||||
resolvable.Add(m);
|
||||
}
|
||||
else
|
||||
{
|
||||
unwovenNames.Add(m);
|
||||
}
|
||||
}
|
||||
|
||||
IlInterpreter.Interpreter.ScriptInterpreter interpreter;
|
||||
try
|
||||
{
|
||||
// Standard shared surface plus the target type so the rewritten body can touch its
|
||||
// own members, public and non-public. Every other type the override references is
|
||||
// bound on demand by the Load pass via the binding's AutoBindResolver.
|
||||
var binding = IlInterpreterHostBindings.CreateStandard();
|
||||
if (targetType != null)
|
||||
binding = binding.AllowType(targetType)
|
||||
.AllowNonPublicInstanceMembers(targetType);
|
||||
|
||||
interpreter = new IlInterpreter.Interpreter.ScriptInterpreter(
|
||||
binding, msg => Debug.Log(msg));
|
||||
// Lenient: a method that can't be lowered becomes a skipped stub (reported per method
|
||||
// in the registration loop below) instead of failing the whole file's reload — so one
|
||||
// bad method (e.g. SetupUI referencing a member missing on the running build) doesn't
|
||||
// drop the other reloaded methods (UIUpdate) in the same file.
|
||||
interpreter.Load(new OverrideScript(assemblyBytes), lenient: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Lenient load no longer throws for per-method lowering failures, so this is a
|
||||
// whole-assembly failure (parse error, cctor, a genuine crash). Anything but a
|
||||
// ScriptValidationException carries an unactionable message ("Index was outside the
|
||||
// bounds of the array") — name the type and put the full stack in the log.
|
||||
var detail = ex is IlInterpreter.ScriptValidationException
|
||||
? ex.Message
|
||||
: $"{ex.GetType().Name}: {ex.Message} (full stack in the editor log)";
|
||||
Debug.LogError($"CodeReload: interpreter load failed for '{typeName}': {ex}");
|
||||
foreach (var m in names)
|
||||
skipped.Add($"{m}: interpreter load failed: {detail}");
|
||||
return registered;
|
||||
}
|
||||
|
||||
// These member refs resolved to throwing stubs. The reload still applies (a stub only
|
||||
// throws if reached), but report the gap in the reload response rather than as a mid-play
|
||||
// ScriptRuntimeException on whichever frame first hits it.
|
||||
if (interpreter.UnboundHostMembers.Count > 0)
|
||||
warnings.Add(
|
||||
$"warning: {interpreter.UnboundHostMembers.Count} host member(s) referenced by the " +
|
||||
$"reloaded code are not in the binding and will throw if reached: " +
|
||||
$"{string.Join(", ", interpreter.UnboundHostMembers)}. Types outside the standard " +
|
||||
"surface and demand-time auto-bind are not callable from interpreted code; " +
|
||||
"BCL types beyond the shims are intentionally not auto-bound.");
|
||||
|
||||
foreach (var methodName in resolvable)
|
||||
{
|
||||
var targetMethodId = $"{typeName}.{methodName}";
|
||||
var name = methodName; // capture per iteration
|
||||
|
||||
// Skipped under lenient load — its body couldn't be lowered. Report the reason and
|
||||
// move on rather than register a dispatcher that throws on first call. The most
|
||||
// common cause across the editor→player boundary is a reference the running build
|
||||
// lacks, so make that actionable.
|
||||
if (interpreter.TryGetLoweringSkip(methodName, out var lowerSkip))
|
||||
{
|
||||
if (lowerSkip.Contains("did not resolve to a host or script method"))
|
||||
lowerSkip += " — the referenced member is neither an interpreted " +
|
||||
"(reloaded/[CodeReload]) method nor a bound host member. If it was added " +
|
||||
"since the running build was made, rebuild/redeploy the player to sync, " +
|
||||
"or mark it [CodeReload].";
|
||||
skipped.Add($"{methodName}: interpreter load skipped this method: {lowerSkip}");
|
||||
continue;
|
||||
}
|
||||
|
||||
object Dispatch(object instance, object[] parameters)
|
||||
{
|
||||
// Override signature: static M(TargetType instance, ...originalArgs). Use typed
|
||||
// Invoke overloads for the common arities to avoid a per-call object[] (args are
|
||||
// already boxed by the woven prologue). Returns the interpreter's (boxed) result,
|
||||
// which the woven prologue of a value-returning method unboxes; void methods
|
||||
// return null, which that prologue ignores.
|
||||
int n = parameters?.Length ?? 0;
|
||||
switch (n)
|
||||
{
|
||||
case 0: return interpreter.Invoke<object>(name, instance);
|
||||
case 1: return interpreter.Invoke<object, object>(name, instance, parameters[0]);
|
||||
case 2: return interpreter.Invoke<object, object, object>(name, instance, parameters[0], parameters[1]);
|
||||
default:
|
||||
var args = new object[1 + n];
|
||||
args[0] = instance;
|
||||
Array.Copy(parameters, 0, args, 1, n);
|
||||
return interpreter.Invoke(name, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (CodeReloadRegistry.RegisterInterpreterMethodOverride(targetMethodId, Dispatch, out var skipReason))
|
||||
registered.Add(targetMethodId);
|
||||
else
|
||||
skipped.Add($"{methodName} -> {targetMethodId}: {skipReason}");
|
||||
}
|
||||
|
||||
// A method with no woven target has no compiled caller to dispatch from. It is still
|
||||
// part of the loaded assembly, so other reloaded bodies in this file call it fine —
|
||||
// report it so the reload response says why it isn't independently dispatchable.
|
||||
foreach (var methodName in unwovenNames)
|
||||
{
|
||||
if (interpreter.TryGetLoweringSkip(methodName, out var lowerSkip))
|
||||
{
|
||||
skipped.Add($"{methodName}: interpreter load skipped this method: {lowerSkip}");
|
||||
continue;
|
||||
}
|
||||
// An empty registry means discovery never ran (nothing entered Play Mode / no
|
||||
// driver booted), not that this one method is new — say so instead of blaming the
|
||||
// build.
|
||||
if (!CodeReloadRegistry.HasReloadableMethods)
|
||||
{
|
||||
skipped.Add($"{methodName}: no [CodeReload] targets are registered in this session, so " +
|
||||
"there is nothing to dispatch to. Targets are discovered when Play Mode starts " +
|
||||
"(or a Player boots); enter Play Mode and save again.");
|
||||
continue;
|
||||
}
|
||||
skipped.Add($"{methodName}: no woven dispatch target — the method (or its type) was " +
|
||||
"added after the running build was compiled. Other reloaded [CodeReload] bodies in " +
|
||||
"this file can call it; dispatching it on its own requires a rebuild.");
|
||||
}
|
||||
|
||||
return registered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push-time host-surface check: load the compiled override bytes into a throwaway
|
||||
/// interpreter against the same binding a player builds on receipt, and return the member
|
||||
/// refs that resolved to throwing stubs. Running the device's load pass in the editor turns
|
||||
/// a mid-play ScriptRuntimeException into a synchronous diagnostic in the
|
||||
/// reload_file_player_interpreter response (the device ack is async and carries only a warning
|
||||
/// count).
|
||||
///
|
||||
/// The editor's surface is a superset of a device player's (no IL2CPP stripping, and
|
||||
/// editor-only assemblies resolve), so a clean result here is no guarantee — the player's
|
||||
/// own load pass stays the backstop. The converse holds strictly: every member reported
|
||||
/// here is a throwing stub on the player too.
|
||||
/// </summary>
|
||||
/// <param name="note">Non-fatal context when the check was degraded: the target type
|
||||
/// could not be resolved (its non-public members may be reported as unbound), or the
|
||||
/// interpreter load itself threw (the player's load will likely fail the same way).</param>
|
||||
public static List<string> ValidateBindingSurface(
|
||||
byte[] assemblyBytes, string typeName, IReadOnlyList<string> methodNames, out string note)
|
||||
{
|
||||
note = null;
|
||||
var unbound = new List<string>();
|
||||
if (assemblyBytes == null || assemblyBytes.Length == 0)
|
||||
return unbound;
|
||||
|
||||
try
|
||||
{
|
||||
var binding = IlInterpreterHostBindings.CreateStandard();
|
||||
var targetType = ResolveTargetType(typeName, methodNames);
|
||||
if (targetType != null)
|
||||
binding.AllowType(targetType).AllowNonPublicInstanceMembers(targetType);
|
||||
else
|
||||
note = $"target type '{typeName}' not resolved in the editor — its own " +
|
||||
"non-public members may show up as unbound";
|
||||
|
||||
using var interpreter = new IlInterpreter.Interpreter.ScriptInterpreter(binding, _ => { });
|
||||
// Lenient like the player's Register: a method that can't lower is skipped, not
|
||||
// thrown, so the pre-check still reports the unbound members of the methods that DID
|
||||
// lower instead of failing the whole validation on the first bad method.
|
||||
interpreter.Load(new OverrideScript(assemblyBytes), lenient: true);
|
||||
unbound.AddRange(interpreter.UnboundHostMembers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
note = $"interpreter load failed during validation: {ex.Message} — the player's " +
|
||||
"load pass will likely fail the same way";
|
||||
}
|
||||
return unbound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the override's target type the way <see cref="Register"/> does (the code-reload
|
||||
/// registry, only populated in play mode), falling back to a scan of the loaded user
|
||||
/// assemblies for a unique short-name match (override extraction records short names).
|
||||
/// Returns null when unknown or ambiguous; validation then runs without the target-type overlay.
|
||||
/// </summary>
|
||||
static Type ResolveTargetType(string typeName, IReadOnlyList<string> methodNames)
|
||||
{
|
||||
if (methodNames != null && methodNames.Count > 0 &&
|
||||
CodeReloadRegistry.TryGetReloadableDeclaringType($"{typeName}.{methodNames[0]}", out var fromRegistry))
|
||||
return fromRegistry;
|
||||
|
||||
// Skip engine/BCL assemblies ([CodeReload] target types live in user code); mirrors
|
||||
// IlInterpreterHostBindings.IsAutoBindSkippedAssembly's classification intent.
|
||||
Type unique = null;
|
||||
foreach (var asm in PipelineUtils.GetLoadedAssemblies())
|
||||
{
|
||||
var name = asm.GetName().Name ?? string.Empty;
|
||||
if (name.StartsWith("Unity", StringComparison.Ordinal) ||
|
||||
name.StartsWith("System", StringComparison.Ordinal) ||
|
||||
name.StartsWith("Microsoft.", StringComparison.Ordinal) ||
|
||||
name.StartsWith("Mono.", StringComparison.Ordinal) ||
|
||||
name.StartsWith("mscorlib", StringComparison.Ordinal) ||
|
||||
name.StartsWith("netstandard", StringComparison.Ordinal) ||
|
||||
name.StartsWith("nunit.", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
Type[] types;
|
||||
try { types = asm.GetTypes(); }
|
||||
catch (System.Reflection.ReflectionTypeLoadException ex) { types = ex.Types; }
|
||||
catch { continue; }
|
||||
|
||||
foreach (var t in types)
|
||||
{
|
||||
if (t == null || t.Name != typeName) continue;
|
||||
if (unique != null && unique != t) return null; // ambiguous — skip the overlay
|
||||
unique = t;
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
/// <summary>Minimal <see cref="IlInterpreter.IScript"/> over the Roslyn-emitted override PE bytes.</summary>
|
||||
private sealed class OverrideScript : IlInterpreter.IScript
|
||||
{
|
||||
public OverrideScript(byte[] bytes) { Il = bytes; }
|
||||
public string Name => "PipelineCodeReload";
|
||||
public ReadOnlyMemory<byte> Il { get; }
|
||||
}
|
||||
|
||||
#else
|
||||
public static List<string> Register(
|
||||
byte[] assemblyBytes, string typeName, IEnumerable<string> methodNames,
|
||||
out List<string> skipped, out List<string> warnings)
|
||||
{
|
||||
skipped = new List<string> { "Interpreter code reload only supported on Desktop development builds" };
|
||||
warnings = new List<string>();
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public static List<string> ValidateBindingSurface(
|
||||
byte[] assemblyBytes, string typeName, IReadOnlyList<string> methodNames, out string note)
|
||||
{
|
||||
note = "Binding validation only supported in the editor and Desktop development builds";
|
||||
return new List<string>();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6cf557e8738ed1441a08144565d882d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,534 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityPipeline.Microsoft.CodeAnalysis;
|
||||
using UnityPipeline.Microsoft.CodeAnalysis.CSharp;
|
||||
using UnityPipeline.Microsoft.CodeAnalysis.Emit;
|
||||
using UnityPipeline.Microsoft.CodeAnalysis.Text;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Compilation
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared Roslyn compilation service for both code evaluation and code reload systems.
|
||||
/// Eliminates code duplication by providing common compilation infrastructure.
|
||||
/// Thread-safe and can be called from any thread.
|
||||
/// </summary>
|
||||
static class RoslynCompilationService
|
||||
{
|
||||
#if UNITY_EDITOR || (UNITY_STANDALONE && DEBUG)
|
||||
|
||||
/// <summary>
|
||||
/// Compile source code to assembly with customizable options.
|
||||
/// Thread-safe compilation that can run on any thread.
|
||||
/// </summary>
|
||||
/// <param name="request">The source and compilation options.</param>
|
||||
/// <returns>The compilation result.</returns>
|
||||
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. Explicit
|
||||
// symbols (e.g. run_script's `defines`) win; otherwise the project's define set
|
||||
// applies so the source's #if regions match the running assemblies.
|
||||
var emitDebug = request.EmitDebugInformation || request.EmbedDebugInformation;
|
||||
var parseOptions = ProjectParseOptions(request.PreprocessorSymbols);
|
||||
var syntaxTree = emitDebug
|
||||
? CSharpSyntaxTree.ParseText(
|
||||
SourceText.From(request.SourceCode, Encoding.UTF8),
|
||||
parseOptions,
|
||||
path: request.DocumentPath ?? request.AssemblyName + ".cs")
|
||||
: CSharpSyntaxTree.ParseText(request.SourceCode, parseOptions);
|
||||
|
||||
// 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 (emitDebug)
|
||||
options = options.WithOptimizationLevel(OptimizationLevel.Debug);
|
||||
// Non-public member access needs two pieces: MetadataImportOptions.All (the default,
|
||||
// Public, doesn't import non-public members of referenced assemblies at all — CS1061)
|
||||
// and BinderFlags.IgnoreAccessibility (the compiler does not honor
|
||||
// [assembly: IgnoresAccessChecksTo] — CS0122 otherwise). Interpreter-bound compiles
|
||||
// only (see the property doc): the interpreter executes the result by reflection,
|
||||
// which reaches non-public members. Unity's Mono JIT-enforces accessibility on
|
||||
// Assembly.Load'd IL and ignores IgnoresAccessChecksTo too (verified on Mono 6.13) —
|
||||
// see CodeReloadInPlaceTests.ReloadFileInPlace_PrivateFieldAccess_FailsValidationUpFront.
|
||||
if (request.AllowNonPublicMemberAccess)
|
||||
options = WithIgnoreAccessibility(options.WithMetadataImportOptions(MetadataImportOptions.All));
|
||||
|
||||
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.EmbedDebugInformation)
|
||||
{
|
||||
// Embed the portable PDB in the PE debug directory so the single byte[]
|
||||
// pushed to a player carries its own sequence points; the IlInterpreter
|
||||
// interpreter reads line info only from an embedded PDB.
|
||||
emitResult = compilation.Emit(
|
||||
peStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded));
|
||||
}
|
||||
else 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.
|
||||
// SkipLoad = emit-only (run_script's dry_run, and the interpreter path — see
|
||||
// the property doc).
|
||||
var assemblyBytes = peStream.ToArray();
|
||||
Assembly assembly = null;
|
||||
if (!request.SkipLoad)
|
||||
{
|
||||
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<DiagnosticInfo>
|
||||
{
|
||||
new DiagnosticInfo
|
||||
{
|
||||
Severity = "error",
|
||||
Message = $"Compilation exception: {ex.Message}",
|
||||
Line = 0,
|
||||
Column = 0,
|
||||
Id = "ROSLYN001"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async wrapper for compilation that runs on background thread.
|
||||
/// Use when you need to avoid blocking the calling thread.
|
||||
/// </summary>
|
||||
/// <param name="request">The source and compilation options.</param>
|
||||
/// <returns>The compilation result.</returns>
|
||||
public static Task<CompilationResult> CompileAsync(CompilationRequest request)
|
||||
{
|
||||
return Task.Run(() => Compile(request));
|
||||
}
|
||||
|
||||
// MetadataReference cache keyed by assembly path. Reusing the same instances, both within one
|
||||
// reload and across successive reloads, lets Roslyn cache each assembly's parsed metadata
|
||||
// instead of re-reading all ~190 DLLs per compilation (~830 ms each). Static: cleared by
|
||||
// domain reload, which is when the loaded-assembly set can change.
|
||||
static readonly System.Collections.Concurrent.ConcurrentDictionary<string, MetadataReference> s_ReferenceCache =
|
||||
new System.Collections.Concurrent.ConcurrentDictionary<string, MetadataReference>();
|
||||
|
||||
static MetadataReference GetCachedReference(string path) =>
|
||||
s_ReferenceCache.GetOrAdd(path, p => MetadataReference.CreateFromFile(p));
|
||||
|
||||
// The internal BinderFlags.IgnoreAccessibility is the only way to disable Roslyn's
|
||||
// compile-time access checks (no public API; [IgnoresAccessChecksTo] is ignored, see Compile).
|
||||
// Resolve the internal CSharpCompilationOptions.WithTopLevelBinderFlags(BinderFlags) once.
|
||||
static bool s_BinderFlagResolved;
|
||||
static MethodInfo s_WithTopLevelBinderFlags;
|
||||
static object s_IgnoreAccessibilityFlag;
|
||||
|
||||
static CSharpCompilationOptions WithIgnoreAccessibility(CSharpCompilationOptions options)
|
||||
{
|
||||
if (!s_BinderFlagResolved)
|
||||
{
|
||||
s_BinderFlagResolved = true;
|
||||
try
|
||||
{
|
||||
var binderFlags = typeof(CSharpCompilationOptions).Assembly
|
||||
.GetType(typeof(CSharpCompilationOptions).Namespace + ".BinderFlags");
|
||||
if (binderFlags != null)
|
||||
{
|
||||
s_IgnoreAccessibilityFlag = Enum.Parse(binderFlags, "IgnoreAccessibility");
|
||||
s_WithTopLevelBinderFlags = typeof(CSharpCompilationOptions).GetMethod(
|
||||
"WithTopLevelBinderFlags", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Pipeline: could not resolve IgnoreAccessibility binder flag; code-reload " +
|
||||
$"overrides touching non-public members may fail to compile. {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (s_WithTopLevelBinderFlags != null && s_IgnoreAccessibilityFlag != null)
|
||||
return (CSharpCompilationOptions)s_WithTopLevelBinderFlags.Invoke(options, new[] { s_IgnoreAccessibilityFlag });
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata references for compilation with optional additional assembly prefixes.
|
||||
/// Handles both Editor and Runtime scenarios with appropriate filtering. Reference instances
|
||||
/// are cached by path (see <see cref="s_ReferenceCache"/>) so repeated compilations are cheap.
|
||||
/// </summary>
|
||||
/// <param name="additionalPrefixes">Extra assembly name prefixes to include as references.</param>
|
||||
/// <returns>The metadata references to compile against.</returns>
|
||||
public static List<MetadataReference> GetMetadataReferences(string[] additionalPrefixes = null)
|
||||
{
|
||||
var references = new List<MetadataReference>();
|
||||
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(GetCachedReference(PipelineUtils.GetLoadedAssemblyPath(assembly)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip problematic assemblies
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Runtime: Use curated filtering for Unity + user assemblies
|
||||
var allowedPrefixes = new List<string>
|
||||
{
|
||||
"UnityEngine",
|
||||
"Assembly-CSharp",
|
||||
"netstandard",
|
||||
"mscorlib",
|
||||
"System.",
|
||||
"Unity.Pipeline"
|
||||
};
|
||||
|
||||
// Add any additional prefixes (e.g., for code reload: "UnityPipeline.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(GetCachedReference(PipelineUtils.GetLoadedAssemblyPath(assembly)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip problematic assemblies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Roslyn diagnostics to DiagnosticInfo list with optional line number adjustment.
|
||||
/// Handles line number offset for wrapped code scenarios.
|
||||
/// </summary>
|
||||
private static List<DiagnosticInfo> ConvertDiagnostics(IEnumerable<Diagnostic> diagnostics, int lineOffset = 0)
|
||||
{
|
||||
var result = new List<DiagnosticInfo>();
|
||||
|
||||
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;
|
||||
|
||||
var info = new DiagnosticInfo
|
||||
{
|
||||
Severity = diagnostic.Severity.ToString().ToLower(),
|
||||
Message = diagnostic.GetMessage(),
|
||||
Line = line,
|
||||
Column = column,
|
||||
Id = diagnostic.Id
|
||||
};
|
||||
|
||||
// #line directives (code reload maps rewritten bodies back to the user's file) give
|
||||
// the user a location they can open; the raw line text shows what was compiled.
|
||||
var mapped = diagnostic.Location.GetMappedLineSpan();
|
||||
if (mapped.HasMappedPath)
|
||||
{
|
||||
info.File = mapped.Path;
|
||||
info.FileLine = mapped.StartLinePosition.Line + 1;
|
||||
}
|
||||
var text = diagnostic.Location.SourceTree?.GetText();
|
||||
var rawLine = location.StartLinePosition.Line;
|
||||
if (text != null && rawLine >= 0 && rawLine < text.Lines.Count)
|
||||
info.Source = text.Lines[rawLine].ToString().Trim();
|
||||
|
||||
result.Add(info);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// Desktop development builds only (Windows/Mac/Linux).
|
||||
/// </summary>
|
||||
/// <param name="request">Unused on this build.</param>
|
||||
/// <returns>A failure result explaining runtime compilation isn't supported on this build.</returns>
|
||||
public static CompilationResult Compile(CompilationRequest request)
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = new List<DiagnosticInfo>
|
||||
{
|
||||
new DiagnosticInfo
|
||||
{
|
||||
Severity = "error",
|
||||
Message = "Runtime code compilation only supported on Desktop development builds",
|
||||
Line = 0,
|
||||
Column = 0,
|
||||
Id = "PLATFORM001"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// </summary>
|
||||
/// <param name="request">Unused on this build.</param>
|
||||
/// <returns>A completed task carrying a failure result.</returns>
|
||||
public static Task<CompilationResult> CompileAsync(CompilationRequest request)
|
||||
{
|
||||
return Task.FromResult(Compile(request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime compilation not supported on this platform.
|
||||
/// </summary>
|
||||
/// <param name="additionalPrefixes">Unused on this build.</param>
|
||||
/// <returns>An empty list.</returns>
|
||||
public static List<MetadataReference> GetMetadataReferences(string[] additionalPrefixes = null)
|
||||
{
|
||||
return new List<MetadataReference>();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Preprocessor symbols the surrounding project was compiled with. Reload/eval sources must
|
||||
// parse under the same defines as the assemblies they patch, or every #if UNITY_EDITOR /
|
||||
// ENABLE_INPUT_SYSTEM / UNITY_STANDALONE region resolves to the wrong branch. The editor
|
||||
// snapshots its live define set; a Mono dev player has no runtime API for it, so callers
|
||||
// that care (the editor-side push path) pass explicit symbols.
|
||||
//
|
||||
// Kept outside the desktop-dev #if region: the parse sites that consume this
|
||||
// (InPlaceReloadProcessor, SourceCodeTransformer) also compile on
|
||||
// release standalone, where the region above is compiled out.
|
||||
static string[] s_LastProjectDefines = Array.Empty<string>();
|
||||
|
||||
static string[] ProjectDefines()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// Editor API — main thread only. Off-thread callers reuse the last snapshot.
|
||||
try { s_LastProjectDefines = UnityEditor.EditorUserBuildSettings.activeScriptCompilationDefines; }
|
||||
catch { }
|
||||
#endif
|
||||
return s_LastProjectDefines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed the define snapshot from the main thread. Called at editor load
|
||||
/// (PipelineServerStartup's [InitializeOnLoad] ctor) so a background compile that runs
|
||||
/// before any main-thread parse doesn't see an EMPTY define set — its #if UNITY_EDITOR /
|
||||
/// UNITY_STANDALONE regions would resolve to the wrong branch.
|
||||
/// </summary>
|
||||
internal static void SnapshotProjectDefines() => ProjectDefines();
|
||||
|
||||
/// <summary>Parse options carrying <paramref name="preprocessorSymbols"/>, or the project's
|
||||
/// define set (see <see cref="ProjectDefines"/>) when null. Every parse of user source in the
|
||||
/// reload/eval pipeline should use this so #if regions match the running assemblies.</summary>
|
||||
internal static CSharpParseOptions ProjectParseOptions(string[] preprocessorSymbols = null)
|
||||
=> CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols ?? ProjectDefines());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for Roslyn compilation with customizable options.
|
||||
/// </summary>
|
||||
public class CompilationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Source code to compile.
|
||||
/// </summary>
|
||||
public string SourceCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name for the generated assembly (should be unique).
|
||||
/// </summary>
|
||||
public string AssemblyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional assembly prefixes to include in metadata references.
|
||||
/// Useful for code reload scenarios that need "UnityPipeline.Microsoft.CodeAnalysis" etc.
|
||||
/// </summary>
|
||||
public string[] AdditionalAssemblyPrefixes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preprocessor symbols to parse with. Null = the project's define set
|
||||
/// (<see cref="RoslynCompilationService.ProjectDefines"/>), so #if regions in the source
|
||||
/// resolve the same way they did when the running assemblies were compiled. The push-to-player
|
||||
/// path passes the target player's defines explicitly, and <c>run_script</c>'s
|
||||
/// <c>defines</c> parameter arrives here too.
|
||||
/// </summary>
|
||||
internal string[] PreprocessorSymbols { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Line number offset to subtract from diagnostic line numbers.
|
||||
/// Used when source code has wrapper code that should be hidden from diagnostics.
|
||||
/// </summary>
|
||||
public int LineNumberOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public bool EmitDebugInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, emit the portable PDB inside the assembly itself (PE debug directory) instead
|
||||
/// of as separate <see cref="CompilationResult.PdbBytes"/>. Compiles unoptimized, like
|
||||
/// <see cref="EmitDebugInformation"/>. Use for the interpreter push path, which ships a single
|
||||
/// byte[] to the player — the IlInterpreter interpreter resolves error line numbers only from an
|
||||
/// embedded PDB.
|
||||
/// </summary>
|
||||
internal bool EmbedDebugInformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Document path recorded in the syntax tree when <see cref="EmitDebugInformation"/> or
|
||||
/// <see cref="EmbedDebugInformation"/> is set.
|
||||
/// Source that is not covered by explicit <c>#line</c> directives maps to this path.
|
||||
/// </summary>
|
||||
public string DocumentPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, compile and collect diagnostics but do NOT load the emitted assembly into
|
||||
/// the domain: <see cref="CompilationResult.Assembly"/> stays null;
|
||||
/// <see cref="CompilationResult.AssemblyBytes"/> is still returned. Two callers need this:
|
||||
/// compile-only paths such as <c>run_script</c>'s <c>dry_run</c> (in-memory assemblies are
|
||||
/// not unloadable under Mono, so loading would leak one per compile), and the IL2CPP-safe
|
||||
/// interpreter path, which runs the raw bytes directly — <c>Assembly.Load</c> is
|
||||
/// unavailable under IL2CPP. Default false — existing callers keep the load behavior.
|
||||
/// </summary>
|
||||
public bool SkipLoad { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, referenced assemblies' non-public members are imported and access checks against them
|
||||
/// are relaxed (<see cref="MetadataImportOptions.All"/>). Interpreter-backend compiles only: the
|
||||
/// interpreter executes the emitted IL via reflection, which reaches non-public members. Never set
|
||||
/// this for IL that will be <c>Assembly.Load</c>'d — Mono JIT-enforces accessibility on loaded IL,
|
||||
/// so the access would compile and then throw at first dispatch. Off by default.
|
||||
/// </summary>
|
||||
internal bool AllowNonPublicMemberAccess { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from Roslyn compilation.
|
||||
/// </summary>
|
||||
public class CompilationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether compilation was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled assembly (only available if Success = true and
|
||||
/// <see cref="CompilationRequest.SkipLoad"/> was not set).
|
||||
/// </summary>
|
||||
public Assembly Assembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw assembly bytes (for potential disk saving or caching).
|
||||
/// </summary>
|
||||
public byte[] AssemblyBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Portable PDB bytes when <see cref="CompilationRequest.EmitDebugInformation"/> was set;
|
||||
/// null otherwise.
|
||||
/// </summary>
|
||||
public byte[] PdbBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compilation diagnostics (errors, warnings, info).
|
||||
/// </summary>
|
||||
public List<DiagnosticInfo> Diagnostics { get; set; } = new List<DiagnosticInfo>();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 957feb25e68236e45b15cd5e9f095c31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user