320 lines
14 KiB
C#
320 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Debug = UnityEngine.Debug;
|
|
using Stopwatch = System.Diagnostics.Stopwatch;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
/// <summary>
|
|
/// Dynamic compilation service for execute-dynamic-code.
|
|
/// Keeps the orchestration flow in one place and delegates low-level compiler concerns to helpers.
|
|
/// </summary>
|
|
public sealed class DynamicCodeCompiler : IDynamicCompilationService, IDisposable
|
|
{
|
|
private readonly DynamicCodeSecurityLevel _securityLevel;
|
|
private readonly IDynamicCompilationPlanner _planner;
|
|
private readonly ICompiledAssemblyBuilder _assemblyBuilder;
|
|
private readonly ICompiledAssemblyLoader _assemblyLoader;
|
|
private readonly CompilationCacheManager _cacheManager = new();
|
|
private bool _disposed;
|
|
|
|
internal int LastBuildCount { get; private set; }
|
|
|
|
public DynamicCodeCompiler(DynamicCodeSecurityLevel securityLevel)
|
|
: this(
|
|
securityLevel,
|
|
DynamicCodeServices.CompilationPlanner,
|
|
DynamicCodeServices.AssemblyBuilder,
|
|
DynamicCodeServices.AssemblyLoadService)
|
|
{
|
|
}
|
|
|
|
internal DynamicCodeCompiler(
|
|
DynamicCodeSecurityLevel securityLevel,
|
|
IDynamicCompilationPlanner planner,
|
|
ICompiledAssemblyBuilder assemblyBuilder,
|
|
ICompiledAssemblyLoader assemblyLoader)
|
|
{
|
|
_securityLevel = securityLevel;
|
|
_planner = planner ?? throw new ArgumentNullException(nameof(planner));
|
|
_assemblyBuilder = assemblyBuilder ?? throw new ArgumentNullException(nameof(assemblyBuilder));
|
|
_assemblyLoader = assemblyLoader ?? throw new ArgumentNullException(nameof(assemblyLoader));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cacheManager.ClearCache();
|
|
_disposed = true;
|
|
}
|
|
|
|
public async Task<CompilationResult> CompileAsync(CompilationRequest request, CancellationToken ct = default)
|
|
{
|
|
Debug.Assert(request != null, "request must not be null");
|
|
Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty");
|
|
|
|
ct.ThrowIfCancellationRequested();
|
|
LastBuildCount = 0;
|
|
Stopwatch compilerTotalStopwatch = Stopwatch.StartNew();
|
|
Stopwatch planStopwatch = Stopwatch.StartNew();
|
|
|
|
DynamicCompilationPlan plan = _planner.CreatePlan(request);
|
|
planStopwatch.Stop();
|
|
|
|
Stopwatch cacheStopwatch = Stopwatch.StartNew();
|
|
CompilationResult cachedResult = TryGetCachedResult(plan);
|
|
cacheStopwatch.Stop();
|
|
if (cachedResult != null)
|
|
{
|
|
AppendCompilerStageTimings(
|
|
cachedResult.Timings,
|
|
planStopwatch.Elapsed.TotalMilliseconds,
|
|
cacheStopwatch.Elapsed.TotalMilliseconds,
|
|
0,
|
|
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
|
return cachedResult;
|
|
}
|
|
|
|
Stopwatch sourceSecurityStopwatch = Stopwatch.StartNew();
|
|
CompilationResult sourceSecurityFailure = CreateSourceSecurityFailure(request.Code);
|
|
sourceSecurityStopwatch.Stop();
|
|
if (sourceSecurityFailure != null)
|
|
{
|
|
AppendCompilerStageTimings(
|
|
sourceSecurityFailure.Timings,
|
|
planStopwatch.Elapsed.TotalMilliseconds,
|
|
cacheStopwatch.Elapsed.TotalMilliseconds,
|
|
0,
|
|
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
|
return sourceSecurityFailure;
|
|
}
|
|
|
|
if (plan.PreparedCode.PreparedSource == null)
|
|
{
|
|
CompilationResult mixedModeFailure = CreateMixedModeFailureResult(request.Code, 0, 0);
|
|
AppendCompilerStageTimings(
|
|
mixedModeFailure.Timings,
|
|
planStopwatch.Elapsed.TotalMilliseconds,
|
|
cacheStopwatch.Elapsed.TotalMilliseconds,
|
|
0,
|
|
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
|
return mixedModeFailure;
|
|
}
|
|
|
|
Stopwatch builderTotalStopwatch = Stopwatch.StartNew();
|
|
CompiledAssemblyBuildResult buildResult = await _assemblyBuilder.BuildAsync(plan, ct);
|
|
builderTotalStopwatch.Stop();
|
|
LastBuildCount = buildResult.BuildCount;
|
|
|
|
if (buildResult.Diagnostics.Errors.Count > 0)
|
|
{
|
|
CompilationResult failureResult = new CompilationResult
|
|
{
|
|
Success = false,
|
|
Errors = buildResult.Diagnostics.Errors,
|
|
Warnings = buildResult.Diagnostics.Warnings,
|
|
UpdatedCode = buildResult.UpdatedSource,
|
|
FailureReason = CompilationFailureReason.CompilationError,
|
|
AmbiguousTypeCandidates = buildResult.AmbiguousTypeCandidates,
|
|
AutoInjectedNamespaces = buildResult.AutoInjectedNamespaces,
|
|
AdvisoryLogs = BuildAdvisoryLogs(buildResult.CompilationBackendKind),
|
|
CompilationBackendKind = buildResult.CompilationBackendKind,
|
|
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
|
buildResult.ReferenceResolutionMilliseconds,
|
|
buildResult.BuildMilliseconds,
|
|
0,
|
|
buildResult.CompilationBackendKind)
|
|
};
|
|
AppendCompilerStageTimings(
|
|
failureResult.Timings,
|
|
planStopwatch.Elapsed.TotalMilliseconds,
|
|
cacheStopwatch.Elapsed.TotalMilliseconds,
|
|
builderTotalStopwatch.Elapsed.TotalMilliseconds,
|
|
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
|
return failureResult;
|
|
}
|
|
|
|
CompiledAssemblyLoadResult assemblyLoadResult = _assemblyLoader.Load(
|
|
_securityLevel,
|
|
buildResult.AssemblyBytes);
|
|
if (!assemblyLoadResult.Success)
|
|
{
|
|
CompilationResult assemblySecurityFailure = CreateAssemblySecurityFailure(
|
|
assemblyLoadResult.SecurityViolations,
|
|
buildResult.Diagnostics.Warnings,
|
|
buildResult.UpdatedSource,
|
|
buildResult.AmbiguousTypeCandidates,
|
|
buildResult.AutoInjectedNamespaces,
|
|
buildResult.ReferenceResolutionMilliseconds,
|
|
buildResult.BuildMilliseconds,
|
|
assemblyLoadResult.AssemblyLoadMilliseconds,
|
|
buildResult.CompilationBackendKind);
|
|
AppendCompilerStageTimings(
|
|
assemblySecurityFailure.Timings,
|
|
planStopwatch.Elapsed.TotalMilliseconds,
|
|
cacheStopwatch.Elapsed.TotalMilliseconds,
|
|
builderTotalStopwatch.Elapsed.TotalMilliseconds,
|
|
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
|
return assemblySecurityFailure;
|
|
}
|
|
|
|
CompilationResult result = new CompilationResult
|
|
{
|
|
Success = true,
|
|
CompiledAssembly = assemblyLoadResult.CompiledAssembly,
|
|
Warnings = buildResult.Diagnostics.Warnings,
|
|
UpdatedCode = buildResult.UpdatedSource,
|
|
AmbiguousTypeCandidates = buildResult.AmbiguousTypeCandidates,
|
|
AutoInjectedNamespaces = buildResult.AutoInjectedNamespaces,
|
|
AdvisoryLogs = BuildAdvisoryLogs(buildResult.CompilationBackendKind),
|
|
CompilationBackendKind = buildResult.CompilationBackendKind,
|
|
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
|
buildResult.ReferenceResolutionMilliseconds,
|
|
buildResult.BuildMilliseconds,
|
|
assemblyLoadResult.AssemblyLoadMilliseconds,
|
|
buildResult.CompilationBackendKind)
|
|
};
|
|
AppendCompilerStageTimings(
|
|
result.Timings,
|
|
planStopwatch.Elapsed.TotalMilliseconds,
|
|
cacheStopwatch.Elapsed.TotalMilliseconds,
|
|
builderTotalStopwatch.Elapsed.TotalMilliseconds,
|
|
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
|
|
|
if (buildResult.ShouldCacheResult)
|
|
{
|
|
_cacheManager.CacheResultIfSuccessful(result, plan.NormalizedRequest);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private CompilationResult TryGetCachedResult(
|
|
DynamicCompilationPlan plan)
|
|
{
|
|
return _cacheManager.CheckCache(plan.NormalizedRequest);
|
|
}
|
|
|
|
private CompilationResult CreateSourceSecurityFailure(string originalCode)
|
|
{
|
|
if (_securityLevel != DynamicCodeSecurityLevel.Restricted)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
SecurityValidationResult sourceSecurityResult = SourceSecurityScanner.Scan(originalCode);
|
|
if (sourceSecurityResult.IsValid)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new CompilationResult
|
|
{
|
|
Success = false,
|
|
HasSecurityViolations = true,
|
|
SecurityViolations = sourceSecurityResult.Violations,
|
|
UpdatedCode = originalCode,
|
|
FailureReason = CompilationFailureReason.SecurityViolation,
|
|
CompilationBackendKind = DynamicCompilationBackendKind.Unknown,
|
|
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(0, 0, 0)
|
|
};
|
|
}
|
|
|
|
private static CompilationResult CreateMixedModeFailureResult(
|
|
string originalCode,
|
|
double referenceResolutionMilliseconds,
|
|
double buildMilliseconds)
|
|
{
|
|
return new CompilationResult
|
|
{
|
|
Success = false,
|
|
Errors = new List<CompilationError>
|
|
{
|
|
new CompilationError
|
|
{
|
|
Message = "Mixed top-level statements and type declarations are not supported. Use a full class definition instead.",
|
|
ErrorCode = "MIXED_MODE",
|
|
Line = 0,
|
|
Column = 0
|
|
}
|
|
},
|
|
FailureReason = CompilationFailureReason.CompilationError,
|
|
UpdatedCode = originalCode,
|
|
CompilationBackendKind = DynamicCompilationBackendKind.Unknown,
|
|
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
|
referenceResolutionMilliseconds,
|
|
buildMilliseconds,
|
|
0)
|
|
};
|
|
}
|
|
|
|
private static CompilationResult CreateAssemblySecurityFailure(
|
|
List<SecurityViolation> securityViolations,
|
|
List<string> warnings,
|
|
string updatedCode,
|
|
Dictionary<string, List<string>> ambiguousTypeCandidates,
|
|
List<string> autoInjectedNamespaces,
|
|
double referenceResolutionMilliseconds,
|
|
double buildMilliseconds,
|
|
double assemblyLoadMilliseconds,
|
|
DynamicCompilationBackendKind compilationBackendKind)
|
|
{
|
|
return new CompilationResult
|
|
{
|
|
Success = false,
|
|
HasSecurityViolations = true,
|
|
SecurityViolations = securityViolations,
|
|
Warnings = warnings,
|
|
UpdatedCode = updatedCode,
|
|
FailureReason = CompilationFailureReason.SecurityViolation,
|
|
AmbiguousTypeCandidates = ambiguousTypeCandidates,
|
|
AutoInjectedNamespaces = autoInjectedNamespaces,
|
|
AdvisoryLogs = BuildAdvisoryLogs(compilationBackendKind),
|
|
CompilationBackendKind = compilationBackendKind,
|
|
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
|
referenceResolutionMilliseconds,
|
|
buildMilliseconds,
|
|
assemblyLoadMilliseconds,
|
|
compilationBackendKind)
|
|
};
|
|
}
|
|
|
|
private static List<string> BuildAdvisoryLogs(DynamicCompilationBackendKind compilationBackendKind)
|
|
{
|
|
return compilationBackendKind == DynamicCompilationBackendKind.AssemblyBuilderFallback
|
|
? new List<string>
|
|
{
|
|
"Warning: Fast Roslyn path is unavailable; execute-dynamic-code is using AssemblyBuilder fallback, so new snippets compile slower."
|
|
}
|
|
: new List<string>();
|
|
}
|
|
|
|
private static void AppendCompilerStageTimings(
|
|
List<string> timings,
|
|
double planMilliseconds,
|
|
double cacheMilliseconds,
|
|
double builderTotalMilliseconds,
|
|
double compilerTotalMilliseconds)
|
|
{
|
|
if (timings == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
timings.Add($"[Perf] CompilePlan: {planMilliseconds:F1}ms");
|
|
timings.Add($"[Perf] CompileCacheCheck: {cacheMilliseconds:F1}ms");
|
|
if (builderTotalMilliseconds > 0)
|
|
{
|
|
timings.Add($"[Perf] BuilderTotal: {builderTotalMilliseconds:F1}ms");
|
|
}
|
|
timings.Add($"[Perf] CompilerTotal: {compilerTotalMilliseconds:F1}ms");
|
|
}
|
|
|
|
}
|
|
}
|