chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖

This commit is contained in:
Liubing\LB
2026-09-28 18:14:59 +08:00
parent 0eb43b2b40
commit c4ae02def9
952 changed files with 70137 additions and 11 deletions

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor.Compilation;
namespace io.github.hatayama.uLoopMCP
{
internal static class AssemblyBuilderFallbackCompilerBackend
{
public static async Task<DynamicCompilationBackendResult> CompileAsync(
string sourcePath,
string dllPath,
List<string> references,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
TaskCompletionSource<CompilerMessage[]> taskCompletionSource = new();
ct.ThrowIfCancellationRequested();
incrementBuildCount();
string[] referenceArray = references != null
? DynamicReferenceSetBuilder.MergeReferencesByAssemblyName(Array.Empty<string>(), references)
: Array.Empty<string>();
AssemblyBuilder builder = new AssemblyBuilder(dllPath, sourcePath)
{
referencesOptions = ReferencesOptions.UseEngineModules,
additionalReferences = referenceArray
};
builder.buildFinished += (string assemblyPath, CompilerMessage[] compilerMessages) =>
{
taskCompletionSource.TrySetResult(compilerMessages);
};
_ = RegisterBuildFinishedContinuation(taskCompletionSource.Task, markBuildFinished);
bool started = builder.Build();
if (!started)
{
return new DynamicCompilationBackendResult(
new CompilerMessage[]
{
new CompilerMessage
{
type = CompilerMessageType.Error,
message = "AssemblyBuilder.Build() failed to start compilation"
}
},
DynamicCompilationBackendKind.AssemblyBuilderFallback);
}
markBuildStarted();
CompilerMessage[] messages = await AwaitBuildCompletionAsync(taskCompletionSource.Task, ct).ConfigureAwait(false);
ct.ThrowIfCancellationRequested();
return new DynamicCompilationBackendResult(
messages,
DynamicCompilationBackendKind.AssemblyBuilderFallback);
}
internal static async Task<CompilerMessage[]> AwaitBuildCompletionAsync(
Task<CompilerMessage[]> buildTask,
CancellationToken ct)
{
TaskCompletionSource<CompilerMessage[]> cancellationTaskCompletionSource =
new TaskCompletionSource<CompilerMessage[]>(TaskCreationOptions.RunContinuationsAsynchronously);
using CancellationTokenRegistration cancellationRegistration =
ct.Register(
static state => ((TaskCompletionSource<CompilerMessage[]>)state).TrySetCanceled(),
cancellationTaskCompletionSource);
Task completedTask = await Task.WhenAny(buildTask, cancellationTaskCompletionSource.Task).ConfigureAwait(false);
if (completedTask == cancellationTaskCompletionSource.Task)
{
await buildTask.ConfigureAwait(false);
return await cancellationTaskCompletionSource.Task.ConfigureAwait(false);
}
return await buildTask.ConfigureAwait(false);
}
internal static Task RegisterBuildFinishedContinuation(
Task<CompilerMessage[]> buildTask,
Action markBuildFinished)
{
return buildTask.ContinueWith(
static (_, state) => ((Action)state)(),
markBuildFinished,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14b708ec67cfd4eeb877b3a9fbf85de5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Builds a type-name → namespace lookup from loaded assemblies.
/// Used by AutoUsingResolver to find missing using directives without Roslyn.
/// </summary>
internal sealed class AssemblyTypeIndex
{
private static AssemblyTypeIndex _instance;
private readonly Dictionary<string, HashSet<string>> _typeToNamespaces = new(StringComparer.Ordinal);
private readonly Dictionary<string, HashSet<string>> _typeToAssemblyLocations = new(StringComparer.Ordinal);
private readonly Dictionary<string, HashSet<string>> _qualifiedTypeToAssemblyLocations = new(StringComparer.Ordinal);
private readonly Dictionary<string, HashSet<string>> _namespaceToAssemblyLocations = new(StringComparer.Ordinal);
public static AssemblyTypeIndex Instance
{
get
{
if (_instance == null)
{
_instance = new AssemblyTypeIndex();
_instance.Build();
}
return _instance;
}
}
[InitializeOnLoadMethod]
private static void InvalidateOnDomainReload()
{
_instance = null;
}
private void Build()
{
_typeToNamespaces.Clear();
_typeToAssemblyLocations.Clear();
_qualifiedTypeToAssemblyLocations.Clear();
_namespaceToAssemblyLocations.Clear();
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.IsDynamic) continue;
string assemblyLocation = GetAssemblyLocation(assembly);
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = Array.FindAll(ex.Types, static t => t != null);
}
foreach (Type type in types)
{
if (type == null || string.IsNullOrEmpty(type.Namespace)) continue;
if (!type.IsPublic) continue;
string typeName = type.Name;
// Strip generic arity suffix (e.g. "List`1" → "List")
int backtick = typeName.IndexOf('`');
if (backtick > 0)
{
typeName = typeName.Substring(0, backtick);
}
if (!_typeToNamespaces.TryGetValue(typeName, out HashSet<string> namespaces))
{
namespaces = new HashSet<string>(StringComparer.Ordinal);
_typeToNamespaces[typeName] = namespaces;
}
namespaces.Add(type.Namespace);
if (!string.IsNullOrEmpty(assemblyLocation))
{
RegisterTypeAssemblyLocation(typeName, assemblyLocation);
RegisterQualifiedTypeAssemblyLocation(type.Namespace, typeName, assemblyLocation);
RegisterNamespaceAssemblyLocation(type.Namespace, assemblyLocation);
}
}
}
}
public List<string> FindNamespacesForType(string typeName)
{
if (string.IsNullOrEmpty(typeName)) return new List<string>();
if (_typeToNamespaces.TryGetValue(typeName, out HashSet<string> namespaces))
{
return new List<string>(namespaces);
}
return new List<string>();
}
public List<string> FindAssemblyLocationsForType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
return new List<string>();
}
if (_typeToAssemblyLocations.TryGetValue(typeName, out HashSet<string> assemblyLocations))
{
return new List<string>(assemblyLocations);
}
return new List<string>();
}
public List<string> FindAssemblyLocationsForIdentifier(string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return new List<string>();
}
int terminalSeparatorIndex = identifier.LastIndexOf('.');
string terminalTypeName = terminalSeparatorIndex >= 0
? identifier.Substring(terminalSeparatorIndex + 1)
: identifier;
string currentIdentifier = identifier;
while (true)
{
if (_qualifiedTypeToAssemblyLocations.TryGetValue(currentIdentifier, out HashSet<string> qualifiedTypeAssemblyLocations))
{
return new List<string>(qualifiedTypeAssemblyLocations);
}
if (_namespaceToAssemblyLocations.TryGetValue(currentIdentifier, out HashSet<string> namespaceAssemblyLocations))
{
return new List<string>(namespaceAssemblyLocations);
}
int separatorIndex = currentIdentifier.LastIndexOf('.');
if (separatorIndex < 0)
{
break;
}
currentIdentifier = currentIdentifier.Substring(0, separatorIndex);
}
if (_typeToAssemblyLocations.TryGetValue(terminalTypeName, out HashSet<string> typeAssemblyLocations))
{
return new List<string>(typeAssemblyLocations);
}
return new List<string>();
}
private static string GetAssemblyLocation(Assembly assembly)
{
try
{
return assembly.Location;
}
catch (NotSupportedException)
{
return string.Empty;
}
}
private void RegisterTypeAssemblyLocation(string typeName, string assemblyLocation)
{
if (!_typeToAssemblyLocations.TryGetValue(typeName, out HashSet<string> assemblyLocations))
{
assemblyLocations = new HashSet<string>(StringComparer.Ordinal);
_typeToAssemblyLocations[typeName] = assemblyLocations;
}
assemblyLocations.Add(assemblyLocation);
}
private void RegisterQualifiedTypeAssemblyLocation(
string namespaceName,
string typeName,
string assemblyLocation)
{
string qualifiedTypeName = $"{namespaceName}.{typeName}";
if (!_qualifiedTypeToAssemblyLocations.TryGetValue(qualifiedTypeName, out HashSet<string> assemblyLocations))
{
assemblyLocations = new HashSet<string>(StringComparer.Ordinal);
_qualifiedTypeToAssemblyLocations[qualifiedTypeName] = assemblyLocations;
}
assemblyLocations.Add(assemblyLocation);
}
private void RegisterNamespaceAssemblyLocation(string namespaceName, string assemblyLocation)
{
string[] namespaceParts = namespaceName.Split('.');
string currentNamespace = string.Empty;
foreach (string namespacePart in namespaceParts)
{
currentNamespace = currentNamespace.Length == 0
? namespacePart
: $"{currentNamespace}.{namespacePart}";
if (!_namespaceToAssemblyLocations.TryGetValue(currentNamespace, out HashSet<string> assemblyLocations))
{
assemblyLocations = new HashSet<string>(StringComparer.Ordinal);
_namespaceToAssemblyLocations[currentNamespace] = assemblyLocations;
}
assemblyLocations.Add(assemblyLocation);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49f2b146b90d84a68bcdc448c0678101
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,237 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor.Compilation;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Resolves missing using directives by retrying compilation with candidate namespaces.
/// Parses CS0246/CS0103 from CompilerMessage and looks up types via AssemblyTypeIndex.
/// </summary>
internal sealed class AutoUsingResolver
{
private const int MaxRetries = 3;
public async Task<AutoUsingResult> ResolveAsync(
string sourcePath,
string dllPath,
string originalSource,
List<string> currentReferences,
System.Func<string, string, List<string>, CancellationToken, Task<CompilerMessage[]>> buildFunc,
CancellationToken ct)
{
Debug.Assert(originalSource != null, "originalSource must not be null");
string currentSource = originalSource;
Dictionary<string, List<string>> ambiguousCandidates = new();
HashSet<string> addedNamespaces = new(System.StringComparer.Ordinal);
List<string> mutableReferences = currentReferences != null
? new List<string>(currentReferences)
: new List<string>();
HashSet<string> addedAssemblyReferences = new(System.StringComparer.OrdinalIgnoreCase);
double referenceResolutionMilliseconds = 0;
for (int attempt = 0; attempt < MaxRetries; attempt++)
{
File.WriteAllText(sourcePath, currentSource);
// AssemblyBuilder retries must stay on Unity's main thread because the compiler API is not thread-safe.
CompilerMessage[] messages = await buildFunc(sourcePath, dllPath, mutableReferences, ct);
List<string> unresolvedTypes = ExtractUnresolvedTypes(messages);
if (unresolvedTypes.Count == 0)
{
return new AutoUsingResult(
currentSource,
messages,
ambiguousCandidates,
addedNamespaces,
addedAssemblyReferences.ToList(),
referenceResolutionMilliseconds);
}
HashSet<string> namespacesToAdd = new(System.StringComparer.Ordinal);
List<string> assemblyReferencesToAdd = new List<string>();
AssemblyTypeIndex index = AssemblyTypeIndex.Instance;
Stopwatch resolutionStopwatch = Stopwatch.StartNew();
foreach (string typeName in unresolvedTypes)
{
if (string.IsNullOrEmpty(typeName)) continue;
List<string> candidates = index.FindNamespacesForType(typeName);
if (candidates.Count == 1 && addedNamespaces.Add(candidates[0]))
{
namespacesToAdd.Add(candidates[0]);
}
else if (candidates.Count > 1)
{
ambiguousCandidates[typeName] = candidates;
}
List<string> assemblyCandidates = index.FindAssemblyLocationsForIdentifier(typeName);
if (assemblyCandidates.Count == 1)
{
AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, mutableReferences, assemblyCandidates[0]);
}
}
resolutionStopwatch.Stop();
referenceResolutionMilliseconds += resolutionStopwatch.Elapsed.TotalMilliseconds;
if (namespacesToAdd.Count == 0 && assemblyReferencesToAdd.Count == 0)
{
break;
}
if (namespacesToAdd.Count > 0)
{
currentSource = InsertUsingDirectives(currentSource, namespacesToAdd);
}
foreach (string assemblyReference in assemblyReferencesToAdd)
{
addedAssemblyReferences.Add(assemblyReference);
mutableReferences.Add(assemblyReference);
}
}
// Final compilation with all added usings
File.WriteAllText(sourcePath, currentSource);
CompilerMessage[] finalMessages = await buildFunc(sourcePath, dllPath, mutableReferences, ct);
return new AutoUsingResult(
currentSource,
finalMessages,
ambiguousCandidates,
addedNamespaces,
addedAssemblyReferences.ToList(),
referenceResolutionMilliseconds);
}
private static List<string> ExtractUnresolvedTypes(CompilerMessage[] messages)
{
List<string> types = new();
foreach (CompilerMessage msg in messages)
{
if (msg.type != CompilerMessageType.Error) continue;
// CS0246: The type or namespace name 'X' could not be found
// CS0103: The name 'X' does not exist in the current context
if (!msg.message.Contains("CS0246")
&& !msg.message.Contains("CS0103")
&& !msg.message.Contains("CS1069"))
{
continue;
}
string typeName = CompilationDiagnosticMessageParser.ExtractTypeNameFromMessage(msg.message);
if (!string.IsNullOrEmpty(typeName))
{
types.Add(typeName);
}
}
return types;
}
internal static string InsertUsingDirectives(string source, IEnumerable<string> namespaces)
{
StringBuilder sb = new StringBuilder();
foreach (string ns in namespaces)
{
sb.AppendLine($"using {ns};");
}
sb.Append(source);
return sb.ToString();
}
internal static void AddAssemblyReferenceIfMissing(
List<string> assemblyReferencesToAdd,
List<string> currentReferences,
string assemblyReference)
{
if (string.IsNullOrEmpty(assemblyReference))
{
return;
}
if (ContainsAssemblyReference(currentReferences, assemblyReference))
{
return;
}
if (ContainsAssemblyReference(assemblyReferencesToAdd, assemblyReference))
{
return;
}
assemblyReferencesToAdd.Add(assemblyReference);
}
internal static bool ContainsAssemblyReference(List<string> references, string assemblyReference)
{
string assemblyIdentity = GetAssemblyIdentityKey(assemblyReference);
foreach (string existingReference in references)
{
if (string.Equals(
GetAssemblyIdentityKey(existingReference),
assemblyIdentity,
System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static string GetAssemblyIdentityKey(string assemblyReference)
{
if (string.IsNullOrEmpty(assemblyReference))
{
return string.Empty;
}
string fileName = Path.GetFileNameWithoutExtension(assemblyReference);
if (string.IsNullOrEmpty(fileName))
{
return assemblyReference;
}
return fileName;
}
}
internal sealed class AutoUsingResult
{
public string UpdatedSource { get; }
public CompilerMessage[] Messages { get; }
public Dictionary<string, List<string>> AmbiguousTypeCandidates { get; }
public IReadOnlyCollection<string> AddedNamespaces { get; }
public IReadOnlyCollection<string> AddedAssemblyReferences { get; }
public double ReferenceResolutionMilliseconds { get; }
public AutoUsingResult(
string updatedSource,
CompilerMessage[] messages,
Dictionary<string, List<string>> ambiguousTypeCandidates,
IReadOnlyCollection<string> addedNamespaces,
IReadOnlyCollection<string> addedAssemblyReferences,
double referenceResolutionMilliseconds)
{
UpdatedSource = updatedSource;
Messages = messages;
AmbiguousTypeCandidates = ambiguousTypeCandidates;
AddedNamespaces = addedNamespaces;
AddedAssemblyReferences = addedAssemblyReferences;
ReferenceResolutionMilliseconds = referenceResolutionMilliseconds;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee4e2b7b02dd448a6a7f1987cf21d258
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class CompiledAssemblyBuildResult
{
public string UpdatedSource { get; }
public CompilerDiagnostics Diagnostics { get; }
public Dictionary<string, List<string>> AmbiguousTypeCandidates { get; }
public List<string> AutoInjectedNamespaces { get; }
public byte[] AssemblyBytes { get; }
public double ReferenceResolutionMilliseconds { get; }
public double BuildMilliseconds { get; }
public int BuildCount { get; }
public bool ShouldCacheResult { get; }
public DynamicCompilationBackendKind CompilationBackendKind { get; }
public CompiledAssemblyBuildResult(
string updatedSource,
CompilerDiagnostics diagnostics,
Dictionary<string, List<string>> ambiguousTypeCandidates,
List<string> autoInjectedNamespaces,
byte[] assemblyBytes,
double referenceResolutionMilliseconds,
double buildMilliseconds,
int buildCount,
bool shouldCacheResult,
DynamicCompilationBackendKind compilationBackendKind)
{
UpdatedSource = updatedSource;
Diagnostics = diagnostics;
AmbiguousTypeCandidates = ambiguousTypeCandidates;
AutoInjectedNamespaces = autoInjectedNamespaces;
AssemblyBytes = assemblyBytes;
ReferenceResolutionMilliseconds = referenceResolutionMilliseconds;
BuildMilliseconds = buildMilliseconds;
BuildCount = buildCount;
ShouldCacheResult = shouldCacheResult;
CompilationBackendKind = compilationBackendKind;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7d128b9fe39f427da5d828eb3b45d58
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,374 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor.Compilation;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class CompiledAssemblyBuilder : ICompiledAssemblyBuilder
{
private static int _compileCounter;
private static readonly string[] LiteralHoistingFallbackErrorCodes = { "CS0133", "CS0150", "CS0182", "CS1736" };
private readonly ExternalCompilerPathResolutionService _externalCompilerPathResolver;
private readonly DynamicReferenceSetBuilderService _referenceSetBuilder;
private readonly DynamicCompilationBackend _compilationBackend;
private sealed class BuildAttemptResult
{
public string UpdatedSource { get; }
public CompilerDiagnostics Diagnostics { get; }
public Dictionary<string, List<string>> AmbiguousTypeCandidates { get; }
public List<string> AutoInjectedNamespaces { get; }
public byte[] AssemblyBytes { get; }
public BuildAttemptResult(
string updatedSource,
CompilerDiagnostics diagnostics,
Dictionary<string, List<string>> ambiguousTypeCandidates,
List<string> autoInjectedNamespaces,
byte[] assemblyBytes)
{
UpdatedSource = updatedSource;
Diagnostics = diagnostics;
AmbiguousTypeCandidates = ambiguousTypeCandidates;
AutoInjectedNamespaces = autoInjectedNamespaces;
AssemblyBytes = assemblyBytes;
}
}
public CompiledAssemblyBuilder(
ExternalCompilerPathResolutionService externalCompilerPathResolver,
DynamicReferenceSetBuilderService referenceSetBuilder,
DynamicCompilationBackend compilationBackend)
{
_externalCompilerPathResolver = externalCompilerPathResolver;
_referenceSetBuilder = referenceSetBuilder;
_compilationBackend = compilationBackend;
}
public bool SupportsAutoPrewarm()
{
return SupportsAutoPrewarm(_externalCompilerPathResolver.Resolve(), Application.platform);
}
internal static bool SupportsAutoPrewarm(
ExternalCompilerPaths externalCompilerPaths,
RuntimePlatform platform)
{
return externalCompilerPaths != null;
}
public async Task<CompiledAssemblyBuildResult> BuildAsync(
DynamicCompilationPlan plan,
CancellationToken ct = default)
{
Debug.Assert(plan != null, "plan must not be null");
ct.ThrowIfCancellationRequested();
ExternalCompilerPaths externalCompilerPaths = _externalCompilerPathResolver.Resolve();
string tempDirectoryPath = Path.Combine("Temp", "uLoopMCPCompilation");
int compileCounter = Interlocked.Increment(ref _compileCounter);
string uniqueName = CreateUniqueCompilationName(plan.ClassName, compileCounter);
string sourcePath = Path.Combine(tempDirectoryPath, $"{uniqueName}.cs");
string dllPath = Path.Combine(tempDirectoryPath, $"{uniqueName}.dll");
bool canDeleteTempFiles = true;
double referenceResolutionMilliseconds = 0;
double buildMilliseconds = 0;
int buildCount = 0;
DynamicCompilationBackendKind compilationBackendKind = DynamicCompilationBackendKind.Unknown;
Directory.CreateDirectory(tempDirectoryPath);
try
{
async Task<CompilerMessage[]> BuildFunc(
string resolvedSourcePath,
string resolvedDllPath,
List<string> resolvedReferences,
CancellationToken cancellationToken)
{
Stopwatch buildStopwatch = Stopwatch.StartNew();
DynamicCompilationBackendResult backendResult = await _compilationBackend.CompileAsync(
resolvedSourcePath,
resolvedDllPath,
resolvedReferences,
externalCompilerPaths,
cancellationToken,
() => canDeleteTempFiles = false,
() => canDeleteTempFiles = true,
() => buildCount++);
compilationBackendKind = backendResult.BackendKind;
buildStopwatch.Stop();
buildMilliseconds += buildStopwatch.Elapsed.TotalMilliseconds;
return backendResult.CompilerMessages;
}
BuildAttemptResult attemptResult = await BuildPreparedCodeAsync(plan.PreparedCode, ct);
bool shouldCacheResult = true;
if (ShouldRetryWithoutLiteralHoisting(plan.PreparedCode, attemptResult.Diagnostics))
{
PreparedDynamicCode fallbackPreparedCode = DynamicCodeSourcePreparer.PrepareWithoutLiteralHoisting(
plan.OriginalRequest.Code,
plan.NamespaceName,
plan.ClassName);
attemptResult = await BuildPreparedCodeAsync(fallbackPreparedCode, ct);
shouldCacheResult = false;
}
return new CompiledAssemblyBuildResult(
attemptResult.UpdatedSource,
attemptResult.Diagnostics,
attemptResult.AmbiguousTypeCandidates,
attemptResult.AutoInjectedNamespaces,
attemptResult.AssemblyBytes,
referenceResolutionMilliseconds,
buildMilliseconds,
buildCount,
shouldCacheResult,
compilationBackendKind);
async Task<BuildAttemptResult> BuildPreparedCodeAsync(
PreparedDynamicCode preparedCode,
CancellationToken cancellationToken)
{
string wrappedCode = preparedCode.PreparedSource;
string originalWrappedCode = wrappedCode;
bool preUsingAdded = false;
PreUsingResult preUsingResult = null;
Stopwatch initialReferenceResolutionStopwatch = Stopwatch.StartNew();
List<string> initialReferences = BuildInitialReferences(
plan,
externalCompilerPaths,
preparedCode,
ref wrappedCode,
ref preUsingAdded,
ref preUsingResult);
initialReferenceResolutionStopwatch.Stop();
referenceResolutionMilliseconds += initialReferenceResolutionStopwatch.Elapsed.TotalMilliseconds;
AutoUsingResolver resolver = new AutoUsingResolver();
AutoUsingResult autoResult = await resolver.ResolveAsync(
sourcePath,
dllPath,
wrappedCode,
initialReferences,
BuildFunc,
cancellationToken);
referenceResolutionMilliseconds += autoResult.ReferenceResolutionMilliseconds;
wrappedCode = autoResult.UpdatedSource;
CompilerDiagnostics diagnostics = CompilerDiagnostics.FromMessages(autoResult.Messages);
bool preUsingRolledBack = false;
if (diagnostics.Errors.Count > 0 && preUsingAdded && diagnostics.HasAmbiguityErrors)
{
Stopwatch rollbackReferenceResolutionStopwatch = Stopwatch.StartNew();
List<string> rollbackReferences = _referenceSetBuilder.BuildReferenceSet(
plan.OriginalRequest.AdditionalReferences,
null,
externalCompilerPaths);
rollbackReferenceResolutionStopwatch.Stop();
referenceResolutionMilliseconds += rollbackReferenceResolutionStopwatch.Elapsed.TotalMilliseconds;
AutoUsingResult rollbackResult = await resolver.ResolveAsync(
sourcePath,
dllPath,
originalWrappedCode,
rollbackReferences,
BuildFunc,
cancellationToken);
referenceResolutionMilliseconds += rollbackResult.ReferenceResolutionMilliseconds;
CompilerDiagnostics rollbackDiagnostics = CompilerDiagnostics.FromMessages(rollbackResult.Messages);
if (rollbackDiagnostics.Errors.Count < diagnostics.Errors.Count)
{
wrappedCode = rollbackResult.UpdatedSource;
diagnostics = rollbackDiagnostics;
autoResult = rollbackResult;
preUsingRolledBack = true;
}
}
List<string> autoInjectedNamespaces = MergeAutoInjectedNamespaces(
preUsingRolledBack,
preUsingResult,
autoResult);
byte[] assemblyBytes = null;
if (diagnostics.Errors.Count == 0)
{
assemblyBytes = File.ReadAllBytes(dllPath);
}
return new BuildAttemptResult(
wrappedCode,
diagnostics,
autoResult.AmbiguousTypeCandidates,
autoInjectedNamespaces,
assemblyBytes);
}
}
finally
{
if (canDeleteTempFiles)
{
File.Delete(sourcePath);
File.Delete(dllPath);
File.Delete(Path.ChangeExtension(dllPath, ".pdb"));
}
}
}
internal static string CreateUniqueCompilationName(string className, int compileCounter)
{
string safeClassName = SanitizeCompilationFileNameSegment(className);
return $"{safeClassName}_{compileCounter}";
}
internal static string SanitizeCompilationFileNameSegment(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return DynamicCodeConstants.DEFAULT_CLASS_NAME;
}
StringBuilder builder = new StringBuilder(value.Length);
foreach (char ch in value)
{
builder.Append(IsUnsafeFileNameCharacter(ch) ? '_' : ch);
}
string sanitized = builder.ToString().Trim('.');
if (string.IsNullOrWhiteSpace(sanitized))
{
return DynamicCodeConstants.DEFAULT_CLASS_NAME;
}
return sanitized;
}
private static bool IsUnsafeFileNameCharacter(char value)
{
if (value == Path.DirectorySeparatorChar || value == Path.AltDirectorySeparatorChar)
{
return true;
}
switch (value)
{
case '<':
case '>':
case ':':
case '"':
case '/':
case '\\':
case '|':
case '?':
case '*':
return true;
}
foreach (char invalidChar in Path.GetInvalidFileNameChars())
{
if (value == invalidChar)
{
return true;
}
}
return false;
}
private List<string> BuildInitialReferences(
DynamicCompilationPlan plan,
ExternalCompilerPaths externalCompilerPaths,
PreparedDynamicCode preparedCode,
ref string wrappedCode,
ref bool preUsingAdded,
ref PreUsingResult preUsingResult)
{
if (!preparedCode.IsScriptMode)
{
return _referenceSetBuilder.BuildReferenceSet(
plan.OriginalRequest.AdditionalReferences,
null,
externalCompilerPaths);
}
preUsingResult = PreUsingResolver.Resolve(wrappedCode, AssemblyTypeIndex.Instance);
preUsingAdded = !ReferenceEquals(preUsingResult.UpdatedSource, wrappedCode);
wrappedCode = preUsingResult.UpdatedSource;
return _referenceSetBuilder.BuildReferenceSet(
plan.OriginalRequest.AdditionalReferences,
preUsingResult.AddedAssemblyReferences,
externalCompilerPaths);
}
private static bool ShouldRetryWithoutLiteralHoisting(
PreparedDynamicCode preparedCode,
CompilerDiagnostics diagnostics)
{
if (preparedCode == null || diagnostics == null)
{
return false;
}
if (!preparedCode.IsScriptMode || preparedCode.HoistedLiteralBindings.Count == 0)
{
return false;
}
foreach (CompilationError error in diagnostics.Errors)
{
foreach (string errorCode in LiteralHoistingFallbackErrorCodes)
{
if (error.ErrorCode == errorCode)
{
return true;
}
}
}
return false;
}
private static List<string> MergeAutoInjectedNamespaces(
bool preUsingRolledBack,
PreUsingResult preUsingResult,
AutoUsingResult autoResult)
{
List<string> mergedNamespaces = new List<string>();
if (!preUsingRolledBack && preUsingResult != null && preUsingResult.AddedNamespaces.Count > 0)
{
foreach (string namespaceName in preUsingResult.AddedNamespaces)
{
mergedNamespaces.Add(namespaceName);
}
}
foreach (string namespaceName in autoResult.AddedNamespaces)
{
if (!mergedNamespaces.Contains(namespaceName))
{
mergedNamespaces.Add(namespaceName);
}
}
return mergedNamespaces;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe943af28806b49428a19a663d2b976d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Assembly = System.Reflection.Assembly;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class CompiledAssemblyLoadResult
{
public bool Success { get; }
public Assembly CompiledAssembly { get; }
public List<SecurityViolation> SecurityViolations { get; }
public double AssemblyLoadMilliseconds { get; }
public CompiledAssemblyLoadResult(
bool success,
Assembly compiledAssembly,
List<SecurityViolation> securityViolations,
double assemblyLoadMilliseconds)
{
Success = success;
CompiledAssembly = compiledAssembly;
SecurityViolations = securityViolations;
AssemblyLoadMilliseconds = assemblyLoadMilliseconds;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cbf74301aec6044fe9761f7fa20286e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
namespace io.github.hatayama.uLoopMCP
{
internal sealed class CompiledAssemblyLoadService : ICompiledAssemblyLoader
{
public CompiledAssemblyLoadResult Load(
DynamicCodeSecurityLevel securityLevel,
byte[] assemblyBytes)
{
return CompiledAssemblyLoader.Load(securityLevel, assemblyBytes);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f14bb6d62e8364e858e6e573fa56f526
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Diagnostics;
using Assembly = System.Reflection.Assembly;
using Debug = UnityEngine.Debug;
namespace io.github.hatayama.uLoopMCP
{
internal static class CompiledAssemblyLoader
{
public static CompiledAssemblyLoadResult Load(
DynamicCodeSecurityLevel securityLevel,
byte[] assemblyBytes)
{
Debug.Assert(assemblyBytes != null, "assemblyBytes must not be null");
Stopwatch stopwatch = Stopwatch.StartNew();
if (securityLevel == DynamicCodeSecurityLevel.Restricted)
{
SecurityValidationResult preloadValidationResult = ValidateBeforeAssemblyLoad(assemblyBytes);
if (!preloadValidationResult.IsValid)
{
stopwatch.Stop();
return new CompiledAssemblyLoadResult(
false,
null,
preloadValidationResult.Violations,
stopwatch.Elapsed.TotalMilliseconds);
}
}
Assembly compiledAssembly = Assembly.Load(assemblyBytes);
if (securityLevel == DynamicCodeSecurityLevel.Restricted)
{
IlSecurityValidator postLoadValidator = new IlSecurityValidator();
SecurityValidationResult postLoadValidationResult = postLoadValidator.Validate(compiledAssembly);
if (!postLoadValidationResult.IsValid)
{
stopwatch.Stop();
return new CompiledAssemblyLoadResult(
false,
null,
postLoadValidationResult.Violations,
stopwatch.Elapsed.TotalMilliseconds);
}
}
stopwatch.Stop();
return new CompiledAssemblyLoadResult(
true,
compiledAssembly,
new List<SecurityViolation>(),
stopwatch.Elapsed.TotalMilliseconds);
}
private static SecurityValidationResult ValidateBeforeAssemblyLoad(byte[] assemblyBytes)
{
SecurityValidationResult aggregatedResult = new SecurityValidationResult
{
IsValid = true,
Violations = new List<SecurityViolation>(),
CompilationErrors = new List<string>()
};
bool shouldRunMetadataFallback = true;
IPreloadAssemblySecurityValidator registeredValidator;
if (PreloadAssemblySecurityValidatorRegistry.TryGetValidator(out registeredValidator))
{
MergeValidationResult(aggregatedResult, registeredValidator.Validate(assemblyBytes));
shouldRunMetadataFallback = registeredValidator is not IOverrideDefaultPreloadAssemblySecurityValidation;
}
if (shouldRunMetadataFallback)
{
SystemReflectionMetadataPreloadValidator fallbackValidator = new SystemReflectionMetadataPreloadValidator();
MergeValidationResult(aggregatedResult, fallbackValidator.Validate(assemblyBytes));
}
PreloadIlSecurityValidator ilValidator = new PreloadIlSecurityValidator();
MergeValidationResult(aggregatedResult, ilValidator.Validate(assemblyBytes));
aggregatedResult.IsValid = aggregatedResult.Violations.Count == 0 &&
aggregatedResult.CompilationErrors.Count == 0;
return aggregatedResult;
}
private static void MergeValidationResult(
SecurityValidationResult target,
SecurityValidationResult source)
{
if (source == null)
{
return;
}
HashSet<string> seenViolations = new HashSet<string>(System.StringComparer.Ordinal);
foreach (SecurityViolation violation in target.Violations)
{
seenViolations.Add(CreateViolationKey(violation));
}
if (source.Violations != null)
{
foreach (SecurityViolation violation in source.Violations)
{
if (seenViolations.Add(CreateViolationKey(violation)))
{
target.Violations.Add(violation);
}
}
}
if (source.CompilationErrors == null)
{
return;
}
foreach (string error in source.CompilationErrors)
{
if (!target.CompilationErrors.Contains(error))
{
target.CompilationErrors.Add(error);
}
}
}
private static string CreateViolationKey(SecurityViolation violation)
{
return $"{violation.Location}|{violation.ApiName}|{violation.Message}";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84266c80ce6874ff88ed120faa159df3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using UnityEditor.Compilation;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class CompilerDiagnostics
{
public List<CompilationError> Errors { get; }
public List<string> Warnings { get; }
public bool HasAmbiguityErrors { get; }
private CompilerDiagnostics(
List<CompilationError> errors,
List<string> warnings,
bool hasAmbiguityErrors)
{
Errors = errors;
Warnings = warnings;
HasAmbiguityErrors = hasAmbiguityErrors;
}
public static CompilerDiagnostics FromMessages(CompilerMessage[] messages)
{
List<CompilationError> errors = new List<CompilationError>();
List<string> warnings = new List<string>();
bool hasAmbiguityErrors = false;
foreach (CompilerMessage message in messages)
{
if (message.type == CompilerMessageType.Warning)
{
warnings.Add(message.message);
continue;
}
if (message.type != CompilerMessageType.Error)
{
continue;
}
string errorCode = ExtractErrorCode(message.message);
if (errorCode == "CS0104" || errorCode == "CS0234")
{
hasAmbiguityErrors = true;
}
errors.Add(new CompilationError
{
Message = message.message,
ErrorCode = errorCode,
Line = message.line,
Column = message.column
});
}
return new CompilerDiagnostics(errors, warnings, hasAmbiguityErrors);
}
private static string ExtractErrorCode(string message)
{
int csIndex = message.IndexOf("CS", StringComparison.Ordinal);
if (csIndex >= 0 && csIndex + 6 <= message.Length)
{
string candidate = message.Substring(csIndex, 6);
if (candidate.Length == 6 && candidate[2] >= '0' && candidate[2] <= '9')
{
return candidate;
}
}
return "UNKNOWN";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcdd52bd855284532a70b6aac3b791c5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace io.github.hatayama.uLoopMCP
{
public sealed class DynamicCodeCompilationServiceFactory : IDynamicCompilationServiceFactory
{
public IDynamicCompilationService Create(DynamicCodeSecurityLevel securityLevel)
{
return new DynamicCodeCompiler(securityLevel);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96f75272fbc5342bcb4e3706cf884fcf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using UnityEditor;
namespace io.github.hatayama.uLoopMCP
{
public static class DynamicCodeCompilationServiceRegistration
{
private static readonly object SyncRoot = new object();
[InitializeOnLoadMethod]
private static void Register()
{
EnsureRegistered();
}
internal static void EnsureRegistered()
{
lock (SyncRoot)
{
if (DynamicCompilationServiceRegistry.HasRegisteredFactory)
{
return;
}
DynamicCompilationServiceRegistry.RegisterFactory(new DynamicCodeCompilationServiceFactory());
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 937b57e6191b647de852905df11c5b25
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,319 @@
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");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 516acc8dbb4a44cc78a6fe5901c0ad45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,863 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace io.github.hatayama.uLoopMCP
{
internal static class DynamicCodeLiteralHoister
{
private const string LiteralParameterPrefix = "__uloop_literal_";
public static HoistedLiteralRewriteResult Rewrite(string source)
{
StringBuilder rewrittenSource = new StringBuilder(source.Length);
List<HoistedLiteralBinding> bindings = new List<HoistedLiteralBinding>();
int index = 0;
while (index < source.Length)
{
if (TryCopyInterpolatedStringLiteral(source, rewrittenSource, ref index))
{
continue;
}
if (TryCopyVerbatimStringLiteral(source, rewrittenSource, ref index))
{
continue;
}
if (TryCopyCharLiteral(source, rewrittenSource, ref index))
{
continue;
}
if (TryCopyLineComment(source, rewrittenSource, ref index))
{
continue;
}
if (TryCopyBlockComment(source, rewrittenSource, ref index))
{
continue;
}
if (TryHoistRegularStringLiteral(source, rewrittenSource, bindings, ref index))
{
continue;
}
if (TryHoistIntegerLiteral(source, rewrittenSource, bindings, ref index))
{
continue;
}
rewrittenSource.Append(source[index]);
index++;
}
List<string> declarationLines = new List<string>();
foreach (HoistedLiteralBinding binding in bindings)
{
declarationLines.Add(
$"{binding.TypeName} {binding.ParameterName} = ({binding.TypeName})parameters[\"{binding.ParameterName}\"];");
}
return new HoistedLiteralRewriteResult(
rewrittenSource.ToString(),
bindings,
declarationLines);
}
private static bool TryCopyInterpolatedStringLiteral(
string source,
StringBuilder rewrittenSource,
ref int index)
{
int start = index;
if (!TryAdvanceInterpolatedStringLiteral(source, ref index))
{
return false;
}
rewrittenSource.Append(source, start, index - start);
return true;
}
private static bool TryCopyLineComment(
string source,
StringBuilder rewrittenSource,
ref int index)
{
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
{
return false;
}
while (index < source.Length)
{
char current = source[index];
rewrittenSource.Append(current);
index++;
if (current == '\n')
{
break;
}
}
return true;
}
private static bool TryCopyBlockComment(
string source,
StringBuilder rewrittenSource,
ref int index)
{
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
{
return false;
}
rewrittenSource.Append(source[index]);
rewrittenSource.Append(source[index + 1]);
index += 2;
while (index < source.Length)
{
char current = source[index];
rewrittenSource.Append(current);
index++;
if (current == '*' && index < source.Length && source[index] == '/')
{
rewrittenSource.Append('/');
index++;
break;
}
}
return true;
}
private static bool TryCopyVerbatimStringLiteral(
string source,
StringBuilder rewrittenSource,
ref int index)
{
if (source[index] != '@'
|| index + 1 >= source.Length
|| source[index + 1] != '"')
{
return false;
}
int start = index;
index += 2;
while (index < source.Length)
{
if (source[index] != '"')
{
index++;
continue;
}
if (index + 1 < source.Length && source[index + 1] == '"')
{
index += 2;
continue;
}
index++;
rewrittenSource.Append(source, start, index - start);
return true;
}
index = start;
return false;
}
private static bool TryAdvanceInterpolatedStringLiteral(string source, ref int index)
{
int start = index;
if (!TryMatchInterpolatedStringStart(source, ref index, out bool isVerbatim))
{
return false;
}
int interpolationDepth = 0;
while (index < source.Length)
{
if (interpolationDepth > 0)
{
if (TryAdvanceInterpolatedExpressionToken(source, ref index))
{
continue;
}
char expressionCharacter = source[index];
if (expressionCharacter == '{')
{
interpolationDepth++;
index++;
continue;
}
if (expressionCharacter == '}')
{
interpolationDepth--;
index++;
continue;
}
index++;
continue;
}
char current = source[index];
if (current == '{')
{
if (index + 1 < source.Length && source[index + 1] == '{')
{
index += 2;
continue;
}
interpolationDepth = 1;
index++;
continue;
}
if (current == '}')
{
if (index + 1 < source.Length && source[index + 1] == '}')
{
index += 2;
continue;
}
index++;
continue;
}
if (!isVerbatim && current == '\\')
{
AdvanceEscapedLiteralSequence(source, ref index);
continue;
}
if (current == '"')
{
if (isVerbatim && index + 1 < source.Length && source[index + 1] == '"')
{
index += 2;
continue;
}
index++;
return true;
}
index++;
}
index = start;
return false;
}
private static bool TryMatchInterpolatedStringStart(
string source,
ref int index,
out bool isVerbatim)
{
isVerbatim = false;
if (source[index] == '$')
{
if (index + 1 < source.Length && source[index + 1] == '"')
{
index += 2;
return true;
}
if (index + 2 < source.Length && source[index + 1] == '@' && source[index + 2] == '"')
{
isVerbatim = true;
index += 3;
return true;
}
return false;
}
if (source[index] != '@')
{
return false;
}
if (index + 2 < source.Length && source[index + 1] == '$' && source[index + 2] == '"')
{
isVerbatim = true;
index += 3;
return true;
}
return false;
}
private static bool TryAdvanceInterpolatedExpressionToken(string source, ref int index)
{
if (TryAdvanceInterpolatedStringLiteral(source, ref index))
{
return true;
}
if (TryAdvanceVerbatimStringLiteral(source, ref index))
{
return true;
}
if (TryAdvanceRegularStringLiteral(source, ref index))
{
return true;
}
if (TryAdvanceCharLiteral(source, ref index))
{
return true;
}
if (TryAdvanceLineComment(source, ref index))
{
return true;
}
if (TryAdvanceBlockComment(source, ref index))
{
return true;
}
return false;
}
private static bool TryAdvanceLineComment(string source, ref int index)
{
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
{
return false;
}
index += 2;
while (index < source.Length && source[index] != '\n')
{
index++;
}
if (index < source.Length)
{
index++;
}
return true;
}
private static bool TryAdvanceBlockComment(string source, ref int index)
{
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
{
return false;
}
index += 2;
while (index < source.Length)
{
if (source[index] == '*' && index + 1 < source.Length && source[index + 1] == '/')
{
index += 2;
return true;
}
index++;
}
return true;
}
private static bool TryAdvanceVerbatimStringLiteral(string source, ref int index)
{
if (source[index] != '@'
|| index + 1 >= source.Length
|| source[index + 1] != '"')
{
return false;
}
int start = index;
index += 2;
while (index < source.Length)
{
if (source[index] != '"')
{
index++;
continue;
}
if (index + 1 < source.Length && source[index + 1] == '"')
{
index += 2;
continue;
}
index++;
return true;
}
index = start;
return false;
}
private static bool TryAdvanceRegularStringLiteral(string source, ref int index)
{
if (source[index] != '"')
{
return false;
}
if (index > 0 && (source[index - 1] == '@' || source[index - 1] == '$'))
{
return false;
}
int start = index;
index++;
while (index < source.Length)
{
char current = source[index];
if (current == '\\')
{
AdvanceEscapedLiteralSequence(source, ref index);
continue;
}
if (current == '"')
{
index++;
return true;
}
index++;
}
index = start;
return false;
}
private static bool TryAdvanceCharLiteral(string source, ref int index)
{
if (source[index] != '\'')
{
return false;
}
int start = index;
index++;
while (index < source.Length)
{
char current = source[index];
if (current == '\\')
{
AdvanceEscapedLiteralSequence(source, ref index);
continue;
}
if (current == '\'')
{
index++;
return true;
}
index++;
}
index = start;
return false;
}
private static bool TryCopyCharLiteral(
string source,
StringBuilder rewrittenSource,
ref int index)
{
if (source[index] != '\'')
{
return false;
}
int start = index;
index++;
while (index < source.Length)
{
char current = source[index];
if (current == '\\')
{
AdvanceEscapedLiteralSequence(source, ref index);
continue;
}
if (current == '\'')
{
index++;
rewrittenSource.Append(source, start, index - start);
return true;
}
index++;
}
index = start;
return false;
}
private static bool TryHoistRegularStringLiteral(
string source,
StringBuilder rewrittenSource,
List<HoistedLiteralBinding> bindings,
ref int index)
{
if (source[index] != '"')
{
return false;
}
if (index > 0 && (source[index - 1] == '@' || source[index - 1] == '$'))
{
return false;
}
int start = index;
index++;
while (index < source.Length)
{
char current = source[index];
if (current == '\\')
{
AdvanceEscapedLiteralSequence(source, ref index);
continue;
}
if (current == '"')
{
index++;
string literalToken = source.Substring(start, index - start);
if (!TryUnescapeRegularStringLiteral(literalToken, out string value))
{
rewrittenSource.Append(literalToken);
return true;
}
string parameterName = CreateParameterName(bindings.Count);
bindings.Add(new HoistedLiteralBinding(parameterName, "string", value));
rewrittenSource.Append(parameterName);
return true;
}
index++;
}
index = start;
return false;
}
private static bool TryHoistIntegerLiteral(
string source,
StringBuilder rewrittenSource,
List<HoistedLiteralBinding> bindings,
ref int index)
{
char current = source[index];
if (!char.IsDigit(current))
{
return false;
}
if (index > 0)
{
char previous = source[index - 1];
if (char.IsLetterOrDigit(previous) || previous == '_' || previous == '.')
{
return false;
}
}
int start = index;
index++;
while (index < source.Length && char.IsDigit(source[index]))
{
index++;
}
if (index < source.Length && (source[index] == 'L' || source[index] == 'l'))
{
int suffixIndex = index;
index++;
if (!HasIntegerLiteralBoundary(source, index))
{
index = start;
return false;
}
string longToken = source.Substring(start, suffixIndex - start);
if (!long.TryParse(longToken, NumberStyles.None, CultureInfo.InvariantCulture, out long longValue))
{
index = start;
return false;
}
string longParameterName = CreateParameterName(bindings.Count);
bindings.Add(new HoistedLiteralBinding(longParameterName, "long", longValue));
rewrittenSource.Append(longParameterName);
return true;
}
if (!HasIntegerLiteralBoundary(source, index))
{
index = start;
return false;
}
string token = source.Substring(start, index - start);
if (!int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out int intValue))
{
index = start;
return false;
}
string parameterName = CreateParameterName(bindings.Count);
bindings.Add(new HoistedLiteralBinding(parameterName, "int", intValue));
rewrittenSource.Append(parameterName);
return true;
}
private static bool HasIntegerLiteralBoundary(string source, int index)
{
if (index >= source.Length)
{
return true;
}
char next = source[index];
return !char.IsLetter(next) && !char.IsDigit(next) && next != '_' && next != '.';
}
private static string CreateParameterName(int index)
{
return $"{LiteralParameterPrefix}{index}";
}
private static void AdvanceEscapedLiteralSequence(string source, ref int index)
{
index++;
if (index >= source.Length)
{
return;
}
char escape = source[index];
index++;
switch (escape)
{
case 'u':
index = Math.Min(source.Length, index + 4);
break;
case 'U':
index = Math.Min(source.Length, index + 8);
break;
case 'x':
index = AdvanceVariableLengthHexDigits(source, index, 4);
break;
}
}
private static int AdvanceVariableLengthHexDigits(string value, int index, int maxDigits)
{
int digitsConsumed = 0;
while (index < value.Length && digitsConsumed < maxDigits && IsHexDigit(value[index]))
{
index++;
digitsConsumed++;
}
return index;
}
private static bool TryUnescapeRegularStringLiteral(string token, out string value)
{
string inner = token.Substring(1, token.Length - 2);
StringBuilder unescaped = new StringBuilder(inner.Length);
for (int index = 0; index < inner.Length; index++)
{
char current = inner[index];
if (current != '\\')
{
unescaped.Append(current);
continue;
}
if (index + 1 >= inner.Length)
{
value = null;
return false;
}
index++;
char escape = inner[index];
switch (escape)
{
case '\'':
unescaped.Append('\'');
break;
case '"':
unescaped.Append('"');
break;
case '\\':
unescaped.Append('\\');
break;
case '0':
unescaped.Append('\0');
break;
case 'a':
unescaped.Append('\a');
break;
case 'b':
unescaped.Append('\b');
break;
case 'f':
unescaped.Append('\f');
break;
case 'n':
unescaped.Append('\n');
break;
case 'r':
unescaped.Append('\r');
break;
case 't':
unescaped.Append('\t');
break;
case 'v':
unescaped.Append('\v');
break;
case 'u':
if (!TryParseHexDigits(inner, ref index, 4, out int unicodeValue))
{
value = null;
return false;
}
unescaped.Append((char)unicodeValue);
break;
case 'U':
if (!TryParseHexDigits(inner, ref index, 8, out int codePoint) ||
!IsValidUtf32CodePoint(codePoint))
{
value = null;
return false;
}
unescaped.Append(char.ConvertFromUtf32(codePoint));
break;
case 'x':
if (!TryParseVariableLengthHexDigits(inner, ref index, out int variableLengthValue))
{
value = null;
return false;
}
unescaped.Append((char)variableLengthValue);
break;
default:
value = null;
return false;
}
}
value = unescaped.ToString();
return true;
}
private static bool TryParseHexDigits(string value, ref int index, int digitCount, out int parsedValue)
{
int start = index + 1;
if (start + digitCount > value.Length)
{
parsedValue = 0;
return false;
}
string hex = value.Substring(start, digitCount);
if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
{
return false;
}
index += digitCount;
return true;
}
private static bool TryParseVariableLengthHexDigits(string value, ref int index, out int parsedValue)
{
int start = index + 1;
int end = AdvanceVariableLengthHexDigits(value, start, 4);
if (end == start)
{
parsedValue = 0;
return false;
}
string hex = value.Substring(start, end - start);
if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
{
return false;
}
index = end - 1;
return true;
}
private static bool IsValidUtf32CodePoint(int value)
{
return value >= 0
&& value <= 0x10FFFF
&& (value < 0xD800 || value > 0xDFFF);
}
private static bool IsHexDigit(char value)
{
return (value >= '0' && value <= '9')
|| (value >= 'a' && value <= 'f')
|| (value >= 'A' && value <= 'F');
}
}
internal sealed class HoistedLiteralRewriteResult
{
public string RewrittenSource { get; }
public List<HoistedLiteralBinding> Bindings { get; }
public List<string> DeclarationLines { get; }
public HoistedLiteralRewriteResult(
string rewrittenSource,
List<HoistedLiteralBinding> bindings,
List<string> declarationLines)
{
RewrittenSource = rewrittenSource;
Bindings = bindings;
DeclarationLines = declarationLines;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5582b74341280488f9b5bcdf69463f26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace io.github.hatayama.uLoopMCP
{
internal sealed class DynamicCodeSourcePreparationService
{
public PreparedDynamicCode Prepare(
string source,
string namespaceName,
string className)
{
return DynamicCodeSourcePreparer.Prepare(source, namespaceName, className);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3bd88ecabcf9b4e7dad4f5638ad6d861
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
namespace io.github.hatayama.uLoopMCP
{
internal static class DynamicCodeSourcePreparer
{
private const string InterpolatedStringMarker = "$\"";
private const string InterpolatedVerbatimStringMarker = "$@\"";
private const string VerbatimInterpolatedStringMarker = "@$\"";
public static PreparedDynamicCode PrepareWithoutLiteralHoisting(
string source,
string namespaceName,
string className)
{
return Prepare(source, namespaceName, className, false);
}
public static PreparedDynamicCode Prepare(
string source,
string namespaceName,
string className)
{
return Prepare(source, namespaceName, className, true);
}
private static PreparedDynamicCode Prepare(
string source,
string namespaceName,
string className,
bool enableLiteralHoisting)
{
SourceShapeResult shape = SourceShaper.Analyze(source);
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && !shape.HasTopLevelStatements)
{
return new PreparedDynamicCode(source, false, new List<HoistedLiteralBinding>());
}
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && shape.HasTopLevelStatements)
{
return new PreparedDynamicCode(null, false, new List<HoistedLiteralBinding>());
}
string body = shape.TopLevelBodyBuilder.ToString().TrimEnd();
bool hasReturn = TopLevelReturnDetector.HasTopLevelReturn(body);
if (!hasReturn)
{
body = string.IsNullOrWhiteSpace(body)
? "return null;"
: body + "\nreturn null;";
}
HoistedLiteralRewriteResult hoistedResult = !enableLiteralHoisting || ShouldSkipLiteralHoisting(body)
? new HoistedLiteralRewriteResult(body, new List<HoistedLiteralBinding>(), new List<string>())
: DynamicCodeLiteralHoister.Rewrite(body);
string preparedSource = WrapperTemplate.Build(
shape.UsingDirectives,
namespaceName,
className,
hoistedResult.RewrittenSource,
hoistedResult.DeclarationLines);
return new PreparedDynamicCode(preparedSource, true, hoistedResult.Bindings);
}
private static bool ShouldSkipLiteralHoisting(string body)
{
return body.Contains(InterpolatedStringMarker)
|| body.Contains(InterpolatedVerbatimStringMarker)
|| body.Contains(VerbatimInterpolatedStringMarker)
|| ContainsInterpolatedRawStringMarker(body);
}
private static bool ContainsInterpolatedRawStringMarker(string body)
{
int quoteIndex = body.IndexOf("\"\"\"", StringComparison.Ordinal);
while (quoteIndex >= 0)
{
int dollarCount = 0;
int markerIndex = quoteIndex - 1;
while (markerIndex >= 0 && body[markerIndex] == '$')
{
dollarCount++;
markerIndex--;
}
if (dollarCount > 0)
{
return true;
}
quoteIndex = body.IndexOf("\"\"\"", quoteIndex + 3, StringComparison.Ordinal);
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 85cb11422a761428aae898d21695b9a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor.Compilation;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class DynamicCompilationBackendResult
{
public CompilerMessage[] CompilerMessages { get; }
public DynamicCompilationBackendKind BackendKind { get; }
public DynamicCompilationBackendResult(
CompilerMessage[] compilerMessages,
DynamicCompilationBackendKind backendKind)
{
CompilerMessages = compilerMessages;
BackendKind = backendKind;
}
}
internal sealed class DynamicCompilationBackend
{
public Task<DynamicCompilationBackendResult> CompileAsync(
string sourcePath,
string dllPath,
List<string> references,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
if (externalCompilerPaths != null)
{
return RoslynCompilerBackend.CompileAsync(
sourcePath,
dllPath,
references,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
}
return AssemblyBuilderFallbackCompilerBackend.CompileAsync(
sourcePath,
dllPath,
references,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b1f77be16bf24a6aa55f573f693172b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,174 @@
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace io.github.hatayama.uLoopMCP
{
internal static class DynamicCompilationHealthMonitor
{
private static readonly object ReportedIssueLock = new();
private static readonly HashSet<string> ReportedIssues = new(System.StringComparer.Ordinal);
private static readonly AsyncLocal<string> ConsoleDiagnosticSource = new();
public static void ReportFastPathUnavailable(
string editorPath,
string contentsPath,
IReadOnlyCollection<string> missingComponents)
{
string issueKey = $"fast_path_unavailable::{editorPath}";
LogErrorOnce(
issueKey,
"dynamic_code_fast_path_unavailable",
"execute-dynamic-code fast Roslyn path is unavailable; AssemblyBuilder fallback will be used",
new
{
editor_path = editorPath,
editor_contents_path = contentsPath,
missing_components = missingComponents
},
"Fast dynamic compilation path is unavailable in this Unity installation.",
"Check Unity version layout changes and ExternalCompilerPathResolver assumptions.");
}
public static void ReportSharedWorkerFallback(string reason, object context = null)
{
string issueKey = $"shared_worker_fallback::{reason}";
string message =
$"execute-dynamic-code shared Roslyn worker is unavailable; falling back to one-shot compiler execution; reason={reason}";
LogErrorOnce(
issueKey,
"dynamic_code_shared_worker_fallback",
message,
context ?? new { reason },
"Shared worker fast path is not active.",
"Investigate shared worker startup, protocol, or platform support.");
}
public static void ReportSharedWorkerFailure(string reason, object context = null)
{
string issueKey = $"shared_worker_failure::{reason}";
string message =
$"execute-dynamic-code shared Roslyn worker failed to operate correctly; reason={reason}";
LogErrorOnce(
issueKey,
"dynamic_code_shared_worker_failure",
message,
context ?? new { reason },
"Shared Roslyn worker encountered an unexpected failure.",
"Investigate worker startup, request handling, and Unity-version-specific runtime assumptions.");
}
public static void ReportOneShotCompilerStartFailure(object context)
{
LogErrorOnce(
"one_shot_compiler_start_failure",
"dynamic_code_one_shot_compiler_start_failure",
"execute-dynamic-code one-shot Roslyn compiler process failed to start",
context,
"One-shot Roslyn compiler could not be started.",
"Investigate Unity-bundled dotnet/csc availability and process launch assumptions.");
}
private static void LogErrorOnce(
string issueKey,
string operation,
string message,
object context,
string humanNote,
string aiTodo)
{
string effectiveIssueKey = CreateEffectiveIssueKey(issueKey);
lock (ReportedIssueLock)
{
if (!ReportedIssues.Add(effectiveIssueKey))
{
return;
}
}
VibeLogger.LogError(
operation,
message,
context,
humanNote: humanNote,
aiTodo: aiTodo);
Debug.LogError(FormatConsoleErrorMessage(operation, message, context));
}
private static string CreateEffectiveIssueKey(string issueKey)
{
Debug.Assert(!string.IsNullOrEmpty(issueKey), "issueKey must not be empty");
if (string.IsNullOrEmpty(ConsoleDiagnosticSource.Value))
{
return issueKey;
}
return $"{issueKey}::source::{ConsoleDiagnosticSource.Value}";
}
internal static System.IDisposable UseConsoleDiagnosticSource(string source)
{
if (string.IsNullOrEmpty(source))
{
return EmptyDisposable.Instance;
}
string previousSource = ConsoleDiagnosticSource.Value;
ConsoleDiagnosticSource.Value = source;
return new ConsoleDiagnosticSourceScope(previousSource);
}
private static string FormatConsoleErrorMessage(string operation, string message, object context)
{
Debug.Assert(!string.IsNullOrEmpty(operation), "operation must not be empty");
Debug.Assert(!string.IsNullOrEmpty(message), "message must not be empty");
string diagnosticSourceLine = string.IsNullOrEmpty(ConsoleDiagnosticSource.Value)
? string.Empty
: $"\ndiagnostic_source: {ConsoleDiagnosticSource.Value}";
if (context == null)
{
return $"[{McpConstants.PROJECT_NAME}] {message}\noperation: {operation}{diagnosticSourceLine}";
}
return $"[{McpConstants.PROJECT_NAME}] {message}\noperation: {operation}{diagnosticSourceLine}\ncontext: {context}";
}
private sealed class ConsoleDiagnosticSourceScope : System.IDisposable
{
private readonly string _previousSource;
public ConsoleDiagnosticSourceScope(string previousSource)
{
_previousSource = previousSource;
}
public void Dispose()
{
ConsoleDiagnosticSource.Value = _previousSource;
}
}
private sealed class EmptyDisposable : System.IDisposable
{
public static readonly EmptyDisposable Instance = new EmptyDisposable();
public void Dispose()
{
}
}
internal static void ResetForTests()
{
lock (ReportedIssueLock)
{
ReportedIssues.Clear();
}
ConsoleDiagnosticSource.Value = null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc51d8d05ce484405a35501c4f13ccfe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
namespace io.github.hatayama.uLoopMCP
{
internal sealed class DynamicCompilationPlan
{
public CompilationRequest OriginalRequest { get; }
public CompilationRequest NormalizedRequest { get; }
public PreparedDynamicCode PreparedCode { get; }
public string ClassName { get; }
public string NamespaceName { get; }
public DynamicCompilationPlan(
CompilationRequest originalRequest,
CompilationRequest normalizedRequest,
PreparedDynamicCode preparedCode,
string className,
string namespaceName)
{
OriginalRequest = originalRequest;
NormalizedRequest = normalizedRequest;
PreparedCode = preparedCode;
ClassName = className;
NamespaceName = namespaceName;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1997758546984a2cbb1f5237b929481
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using UnityEngine;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class DynamicCompilationPlanner : IDynamicCompilationPlanner
{
private readonly DynamicCodeSourcePreparationService _sourcePreparationService;
public DynamicCompilationPlanner(DynamicCodeSourcePreparationService sourcePreparationService)
{
_sourcePreparationService = sourcePreparationService;
}
public DynamicCompilationPlan CreatePlan(CompilationRequest request)
{
Debug.Assert(request != null, "request must not be null");
Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty");
string namespaceName = request.Namespace ?? DynamicCodeConstants.DEFAULT_NAMESPACE;
string className = request.ClassName ?? DynamicCodeConstants.DEFAULT_CLASS_NAME;
PreparedDynamicCode preparedCode = _sourcePreparationService.Prepare(
request.Code,
namespaceName,
className);
CompilationRequest normalizedRequest = new CompilationRequest
{
Code = preparedCode.PreparedSource ?? request.Code,
ClassName = className,
Namespace = namespaceName,
AdditionalReferences = request.AdditionalReferences != null
? new List<string>(request.AdditionalReferences)
: new List<string>(),
AssemblyMode = request.AssemblyMode
};
return new DynamicCompilationPlan(
request,
normalizedRequest,
preparedCode,
className,
namespaceName);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f46051bf774dd48acb16efe9055ac7f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
namespace io.github.hatayama.uLoopMCP
{
internal static class DynamicCompilationTimingFormatter
{
private const string CacheHitTimingEntry = "[Perf] CacheHit: true";
public static List<string> CreateCompilationTimings(
double referenceResolutionMilliseconds,
double buildMilliseconds,
double assemblyLoadMilliseconds,
DynamicCompilationBackendKind backendKind = DynamicCompilationBackendKind.Unknown)
{
List<string> timings = new List<string>
{
$"[Perf] ReferenceResolution: {referenceResolutionMilliseconds:F1}ms",
$"[Perf] Build: {buildMilliseconds:F1}ms",
$"[Perf] AssemblyLoad: {assemblyLoadMilliseconds:F1}ms"
};
string backendTimingEntry = CreateBackendTimingEntry(backendKind);
if (!string.IsNullOrEmpty(backendTimingEntry))
{
timings.Add(backendTimingEntry);
}
return timings;
}
public static List<string> CreateCachedCompilationTimings(
DynamicCompilationBackendKind backendKind = DynamicCompilationBackendKind.Unknown)
{
List<string> timings = CreateCompilationTimings(0, 0, 0, backendKind);
timings.Add(CacheHitTimingEntry);
return timings;
}
private static string CreateBackendTimingEntry(DynamicCompilationBackendKind backendKind)
{
return backendKind switch
{
DynamicCompilationBackendKind.SharedRoslynWorker => "[Perf] Backend: SharedRoslynWorker",
DynamicCompilationBackendKind.OneShotRoslyn => "[Perf] Backend: OneShotRoslyn",
DynamicCompilationBackendKind.AssemblyBuilderFallback => "[Perf] Backend: AssemblyBuilderFallback",
_ => null
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7ad46f4a7e8b4ed8a171b2f90432af7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using Assembly = System.Reflection.Assembly;
namespace io.github.hatayama.uLoopMCP
{
internal static class DynamicReferenceSetBuilder
{
private static readonly object ReferenceCatalogLock = new();
private static readonly string[] BaseReferenceAssemblyNames =
{
"mscorlib",
"netstandard",
"System",
"System.Core",
"System.Runtime",
"System.Collections",
"System.Net.Http",
"System.Threading",
"System.Threading.Tasks",
"UnityEngine.CoreModule",
"UnityEditor.CoreModule",
};
private static Dictionary<string, string> _cachedAssemblyLocationsByName;
private static int _cachedAssemblyCount = -1;
public static List<string> BuildReferenceSet(
List<string> additionalReferences,
IReadOnlyCollection<string> resolvedAssemblyReferences,
ExternalCompilerPaths externalCompilerPaths)
{
Dictionary<string, string> assemblyLocationsByName = GetCachedAssemblyLocationsByName();
List<string> baseReferences = new List<string>();
foreach (string assemblyName in BaseReferenceAssemblyNames)
{
string preferredReferencePath = GetPreferredBaseReferencePath(externalCompilerPaths, assemblyName);
if (!string.IsNullOrEmpty(preferredReferencePath) && File.Exists(preferredReferencePath))
{
baseReferences.Add(preferredReferencePath);
continue;
}
if (assemblyLocationsByName.TryGetValue(assemblyName, out string loadedAssemblyPath))
{
baseReferences.Add(loadedAssemblyPath);
}
}
AddLoadedAssemblyReferences(baseReferences, assemblyLocationsByName);
List<string> mergedAdditionalReferences = new List<string>();
AddExistingReferences(mergedAdditionalReferences, additionalReferences);
AddExistingReferences(mergedAdditionalReferences, resolvedAssemblyReferences);
string[] mergedReferences = MergeReferencesByAssemblyName(
baseReferences.ToArray(),
mergedAdditionalReferences);
return new List<string>(mergedReferences);
}
internal static string[] MergeReferencesByAssemblyName(string[] baseReferences, List<string> additionalReferences)
{
UnityEngine.Debug.Assert(baseReferences != null, "baseReferences must not be null");
UnityEngine.Debug.Assert(additionalReferences != null, "additionalReferences must not be null");
HashSet<string> seenNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
List<string> mergedReferences = new List<string>(baseReferences.Length + additionalReferences.Count);
foreach (string baseReference in baseReferences)
{
string assemblyName = Path.GetFileNameWithoutExtension(baseReference);
if (seenNames.Add(assemblyName))
{
mergedReferences.Add(baseReference);
}
}
foreach (string additionalReference in additionalReferences)
{
if (string.IsNullOrWhiteSpace(additionalReference) || !File.Exists(additionalReference))
{
continue;
}
string assemblyName = Path.GetFileNameWithoutExtension(additionalReference);
if (seenNames.Add(assemblyName))
{
mergedReferences.Add(additionalReference);
}
}
return mergedReferences.ToArray();
}
[InitializeOnLoadMethod]
private static void InvalidateReferenceCacheOnDomainReload()
{
lock (ReferenceCatalogLock)
{
_cachedAssemblyLocationsByName = null;
_cachedAssemblyCount = -1;
}
}
private static void AddLoadedAssemblyReferences(
List<string> destination,
IReadOnlyDictionary<string, string> assemblyLocationsByName)
{
List<string> assemblyNames = new List<string>(assemblyLocationsByName.Keys);
assemblyNames.Sort(StringComparer.OrdinalIgnoreCase);
foreach (string assemblyName in assemblyNames)
{
if (!assemblyLocationsByName.TryGetValue(assemblyName, out string assemblyPath)
|| string.IsNullOrEmpty(assemblyPath)
|| !File.Exists(assemblyPath))
{
continue;
}
destination.Add(assemblyPath);
}
}
private static Dictionary<string, string> GetCachedAssemblyLocationsByName()
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
lock (ReferenceCatalogLock)
{
if (_cachedAssemblyLocationsByName != null && _cachedAssemblyCount == assemblies.Length)
{
return _cachedAssemblyLocationsByName;
}
Dictionary<string, string> assemblyLocationsByName =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (Assembly assembly in assemblies)
{
if (assembly.IsDynamic)
{
continue;
}
string location;
try
{
location = assembly.Location;
}
catch (NotSupportedException)
{
continue;
}
string assemblyName = assembly.GetName().Name;
if (string.IsNullOrEmpty(location)
|| !File.Exists(location)
|| string.IsNullOrEmpty(assemblyName)
|| assemblyLocationsByName.ContainsKey(assemblyName))
{
continue;
}
assemblyLocationsByName.Add(assemblyName, location);
}
_cachedAssemblyLocationsByName = assemblyLocationsByName;
_cachedAssemblyCount = assemblies.Length;
return _cachedAssemblyLocationsByName;
}
}
private static string GetPreferredBaseReferencePath(
ExternalCompilerPaths externalCompilerPaths,
string assemblyName)
{
if (externalCompilerPaths == null)
{
return null;
}
return ResolvePreferredBaseReferencePath(
externalCompilerPaths.EditorContentsPath,
externalCompilerPaths.ScriptingRootPath,
assemblyName);
}
internal static string ResolvePreferredBaseReferencePath(
string editorContentsPath,
string assemblyName)
{
return ResolvePreferredBaseReferencePath(editorContentsPath, null, assemblyName);
}
internal static string ResolvePreferredBaseReferencePath(
string editorContentsPath,
string scriptingRootPath,
string assemblyName)
{
foreach (string referenceRootPath in EnumerateReferenceRootPaths(editorContentsPath, scriptingRootPath))
{
string candidatePath = BuildPreferredBaseReferencePath(referenceRootPath, assemblyName);
if (!string.IsNullOrEmpty(candidatePath) && File.Exists(candidatePath))
{
return candidatePath;
}
}
return null;
}
private static IEnumerable<string> EnumerateReferenceRootPaths(
string editorContentsPath,
string scriptingRootPath)
{
HashSet<string> emittedPaths = new HashSet<string>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(scriptingRootPath) && emittedPaths.Add(scriptingRootPath))
{
yield return scriptingRootPath;
}
if (string.IsNullOrEmpty(editorContentsPath))
{
yield break;
}
string resourcesScriptingPath = Path.Combine(editorContentsPath, "Resources", "Scripting");
if (emittedPaths.Add(resourcesScriptingPath))
{
yield return resourcesScriptingPath;
}
if (emittedPaths.Add(editorContentsPath))
{
yield return editorContentsPath;
}
}
private static string BuildPreferredBaseReferencePath(string referenceRootPath, string assemblyName)
{
switch (assemblyName)
{
case "mscorlib":
case "netstandard":
case "System":
case "System.Core":
case "System.Net.Http":
return Path.Combine(referenceRootPath, "UnityReferenceAssemblies", "unity-4.8-api", $"{assemblyName}.dll");
case "System.Runtime":
case "System.Collections":
case "System.Threading":
case "System.Threading.Tasks":
return Path.Combine(referenceRootPath, "UnityReferenceAssemblies", "unity-4.8-api", "Facades", $"{assemblyName}.dll");
case "UnityEngine":
case "UnityEditor":
return Path.Combine(referenceRootPath, "Managed", $"{assemblyName}.dll");
case "UnityEngine.CoreModule":
case "UnityEditor.CoreModule":
return Path.Combine(referenceRootPath, "Managed", "UnityEngine", $"{assemblyName}.dll");
default:
return null;
}
}
private static void AddExistingReferences(
List<string> destination,
IReadOnlyCollection<string> source)
{
if (source == null)
{
return;
}
foreach (string reference in source)
{
if (string.IsNullOrEmpty(reference) || !File.Exists(reference))
{
continue;
}
destination.Add(reference);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: abf32b53ef0d64589b3d503e67f97a28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class DynamicReferenceSetBuilderService
{
public List<string> BuildReferenceSet(
List<string> additionalReferences,
IReadOnlyCollection<string> resolvedAssemblyReferences,
ExternalCompilerPaths externalCompilerPaths)
{
return DynamicReferenceSetBuilder.BuildReferenceSet(
additionalReferences,
resolvedAssemblyReferences,
externalCompilerPaths);
}
public string[] MergeReferencesByAssemblyName(
string[] baseReferences,
List<string> additionalReferences)
{
return DynamicReferenceSetBuilder.MergeReferencesByAssemblyName(baseReferences, additionalReferences);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4fbe931b9a8844a7e9060c52680a32d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEditor.Compilation;
namespace io.github.hatayama.uLoopMCP
{
internal static class ExternalCompilerMessageParser
{
private static readonly Regex DiagnosticRegex = new(
@"^(?<file>.+)\((?<line>\d+),(?<column>\d+)\): (?<severity>error|warning) (?<code>[A-Z]+\d+): (?<message>.+)$",
RegexOptions.Compiled);
public static CompilerMessage[] Parse(
string stdout,
string stderr,
int exitCode)
{
List<CompilerMessage> messages = new List<CompilerMessage>();
AddParsedMessages(messages, stdout);
AddParsedMessages(messages, stderr);
if (messages.Count > 0)
{
if (exitCode != 0 && !HasError(messages))
{
messages.Add(CreateInfrastructureFailureMessage(stdout, stderr, exitCode));
}
return messages.ToArray();
}
if (exitCode == 0)
{
return Array.Empty<CompilerMessage>();
}
string combinedOutput = CombineOutput(stdout, stderr);
return new CompilerMessage[]
{
new CompilerMessage
{
type = CompilerMessageType.Error,
message = string.IsNullOrWhiteSpace(combinedOutput)
? "External C# compiler failed without diagnostics"
: combinedOutput
}
};
}
private static bool HasError(IReadOnlyCollection<CompilerMessage> messages)
{
foreach (CompilerMessage message in messages)
{
if (message.type == CompilerMessageType.Error)
{
return true;
}
}
return false;
}
private static CompilerMessage CreateInfrastructureFailureMessage(
string stdout,
string stderr,
int exitCode)
{
string combinedOutput = CombineOutput(stdout, stderr);
string message = $"External C# compiler exited with code {exitCode} without reporting an error diagnostic";
if (!string.IsNullOrWhiteSpace(combinedOutput))
{
message = $"{message}: {combinedOutput}";
}
return new CompilerMessage
{
type = CompilerMessageType.Error,
message = message
};
}
private static void AddParsedMessages(
List<CompilerMessage> messages,
string output)
{
if (string.IsNullOrWhiteSpace(output))
{
return;
}
string[] lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
Match match = DiagnosticRegex.Match(line.Trim());
if (!match.Success)
{
continue;
}
messages.Add(new CompilerMessage
{
type = match.Groups["severity"].Value == "warning"
? CompilerMessageType.Warning
: CompilerMessageType.Error,
message = $"{match.Groups["code"].Value}: {match.Groups["message"].Value}",
file = match.Groups["file"].Value,
line = int.Parse(match.Groups["line"].Value),
column = int.Parse(match.Groups["column"].Value)
});
}
}
private static string CombineOutput(string stdout, string stderr)
{
if (string.IsNullOrWhiteSpace(stdout))
{
return stderr?.Trim();
}
if (string.IsNullOrWhiteSpace(stderr))
{
return stdout.Trim();
}
return $"{stdout.Trim()}\n{stderr.Trim()}";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 443b319d90df34fa78eb110cb3c87cc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace io.github.hatayama.uLoopMCP
{
internal sealed class ExternalCompilerPathResolutionService
{
public ExternalCompilerPaths Resolve()
{
return ExternalCompilerPathResolver.Resolve();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9bdc0f83d884c4d58b131562f3ff6f5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
namespace io.github.hatayama.uLoopMCP
{
internal static class ExternalCompilerPathResolver
{
private const string NetCoreRuntimeDirectoryName = "NetCoreRuntime";
private const string DotNetSdkRoslynDirectoryName = "DotNetSdkRoslyn";
private const string DotNetSdkDirectoryName = "DotNetSdk";
private const string DotNetSdkSdkDirectoryName = "sdk";
private const string RoslynDirectoryName = "Roslyn";
private const string CompilerBincoreDirectoryName = "bincore";
private const string CompilerDllFileName = "csc.dll";
private const string CompilerRuntimeConfigFileName = "csc.runtimeconfig.json";
private const string CompilerDepsFileName = "csc.deps.json";
private const string CodeAnalysisDllFileName = "Microsoft.CodeAnalysis.dll";
private const string CodeAnalysisCSharpDllFileName = "Microsoft.CodeAnalysis.CSharp.dll";
private const string NetCoreRuntimeSharedDirectoryName = "shared";
private const string NetCoreRuntimeSharedFrameworkName = "Microsoft.NETCore.App";
public static ExternalCompilerPaths Resolve()
{
string editorPath = EditorApplication.applicationPath;
if (string.IsNullOrEmpty(editorPath))
{
return null;
}
string contentsPath = ResolveEditorContentsPath(editorPath);
if (string.IsNullOrEmpty(contentsPath))
{
return null;
}
string scriptingRootPath = ResolveScriptingRootPath(contentsPath);
string dotnetHostFileName = UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor
? "dotnet.exe"
: "dotnet";
string effectiveScriptingRootPath = scriptingRootPath ?? contentsPath;
string compilerDirectoryPath = ResolveCompilerDirectoryPath(effectiveScriptingRootPath);
List<string> missingComponents = new List<string>();
if (string.IsNullOrEmpty(scriptingRootPath))
{
missingComponents.Add(Path.Combine(contentsPath, NetCoreRuntimeDirectoryName));
missingComponents.Add(Path.Combine(contentsPath, DotNetSdkRoslynDirectoryName));
missingComponents.Add(Path.Combine(contentsPath, DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName, "*", RoslynDirectoryName, CompilerBincoreDirectoryName));
missingComponents.Add(Path.Combine(contentsPath, "Resources", "Scripting", NetCoreRuntimeDirectoryName));
missingComponents.Add(Path.Combine(contentsPath, "Resources", "Scripting", DotNetSdkRoslynDirectoryName));
missingComponents.Add(Path.Combine(contentsPath, "Resources", "Scripting", DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName, "*", RoslynDirectoryName, CompilerBincoreDirectoryName));
}
if (string.IsNullOrEmpty(compilerDirectoryPath))
{
compilerDirectoryPath = Path.Combine(effectiveScriptingRootPath, DotNetSdkRoslynDirectoryName);
missingComponents.Add(compilerDirectoryPath);
missingComponents.Add(Path.Combine(effectiveScriptingRootPath, DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName, "*", RoslynDirectoryName, CompilerBincoreDirectoryName));
}
string dotnetHostPath = Path.Combine(effectiveScriptingRootPath, NetCoreRuntimeDirectoryName, dotnetHostFileName);
string compilerDllPath = Path.Combine(compilerDirectoryPath, CompilerDllFileName);
string compilerRuntimeConfigPath = Path.Combine(compilerDirectoryPath, CompilerRuntimeConfigFileName);
string compilerDepsFilePath = Path.Combine(compilerDirectoryPath, CompilerDepsFileName);
string codeAnalysisDllPath = Path.Combine(compilerDirectoryPath, CodeAnalysisDllFileName);
string codeAnalysisCSharpDllPath = Path.Combine(compilerDirectoryPath, CodeAnalysisCSharpDllFileName);
string netCoreRuntimeSharedRootPath = Path.Combine(effectiveScriptingRootPath, NetCoreRuntimeDirectoryName, NetCoreRuntimeSharedDirectoryName, NetCoreRuntimeSharedFrameworkName);
string netCoreRuntimeSharedDirectoryPath = ResolveNetCoreRuntimeSharedDirectoryPath(netCoreRuntimeSharedRootPath);
if (!File.Exists(dotnetHostPath))
{
missingComponents.Add(dotnetHostPath);
}
if (!File.Exists(compilerDllPath))
{
missingComponents.Add(compilerDllPath);
}
if (!File.Exists(compilerRuntimeConfigPath))
{
missingComponents.Add(compilerRuntimeConfigPath);
}
if (!File.Exists(compilerDepsFilePath))
{
missingComponents.Add(compilerDepsFilePath);
}
if (!File.Exists(codeAnalysisDllPath))
{
missingComponents.Add(codeAnalysisDllPath);
}
if (!File.Exists(codeAnalysisCSharpDllPath))
{
missingComponents.Add(codeAnalysisCSharpDllPath);
}
if (string.IsNullOrEmpty(netCoreRuntimeSharedDirectoryPath))
{
missingComponents.Add(netCoreRuntimeSharedRootPath);
}
if (missingComponents.Count > 0)
{
DynamicCompilationHealthMonitor.ReportFastPathUnavailable(
editorPath,
contentsPath,
missingComponents);
return null;
}
return new ExternalCompilerPaths(
contentsPath,
scriptingRootPath,
dotnetHostPath,
compilerDllPath,
compilerRuntimeConfigPath,
compilerDepsFilePath,
codeAnalysisDllPath,
codeAnalysisCSharpDllPath,
netCoreRuntimeSharedDirectoryPath);
}
internal static string ResolveScriptingRootPath(string contentsPath)
{
if (string.IsNullOrEmpty(contentsPath))
{
return null;
}
string resourcesScriptingRootPath = Path.Combine(contentsPath, "Resources", "Scripting");
if (ContainsExternalCompilerLayout(resourcesScriptingRootPath))
{
return resourcesScriptingRootPath;
}
if (ContainsExternalCompilerLayout(contentsPath))
{
return contentsPath;
}
return ResolveScriptingRootPathByScan(contentsPath);
}
internal static string ResolveCompilerDirectoryPath(string scriptingRootPath)
{
if (string.IsNullOrEmpty(scriptingRootPath))
{
return null;
}
string legacyCompilerDirectoryPath = Path.Combine(scriptingRootPath, DotNetSdkRoslynDirectoryName);
if (Directory.Exists(legacyCompilerDirectoryPath))
{
return legacyCompilerDirectoryPath;
}
return ResolveDotNetSdkCompilerDirectoryPath(scriptingRootPath);
}
internal static string ResolveNetCoreRuntimeSharedDirectoryPath(string netCoreRuntimeSharedRootPath)
{
if (!Directory.Exists(netCoreRuntimeSharedRootPath))
{
return null;
}
string[] runtimeDirectories = Directory.GetDirectories(netCoreRuntimeSharedRootPath);
if (runtimeDirectories.Length == 0)
{
return null;
}
string highestVersionDirectoryPath = runtimeDirectories
.Select(runtimeDirectoryPath => new
{
Path = runtimeDirectoryPath,
VersionText = Path.GetFileName(runtimeDirectoryPath)
})
.Where(candidate => Version.TryParse(candidate.VersionText, out _))
.OrderByDescending(candidate => new Version(candidate.VersionText))
.ThenByDescending(candidate => candidate.VersionText, StringComparer.Ordinal)
.Select(candidate => candidate.Path)
.FirstOrDefault();
if (!string.IsNullOrEmpty(highestVersionDirectoryPath))
{
return highestVersionDirectoryPath;
}
return runtimeDirectories
.OrderByDescending(Path.GetFileName, StringComparer.Ordinal)
.First();
}
private static bool ContainsExternalCompilerLayout(string rootPath)
{
return Directory.Exists(Path.Combine(rootPath, NetCoreRuntimeDirectoryName))
&& !string.IsNullOrEmpty(ResolveCompilerDirectoryPath(rootPath));
}
private static string ResolveDotNetSdkCompilerDirectoryPath(string scriptingRootPath)
{
string sdkRootPath = Path.Combine(scriptingRootPath, DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName);
if (!Directory.Exists(sdkRootPath))
{
return null;
}
List<string> sdkDirectoryPaths = Directory.GetDirectories(sdkRootPath).ToList();
sdkDirectoryPaths.Sort(CompareSdkDirectoryPathsDescending);
foreach (string sdkDirectoryPath in sdkDirectoryPaths)
{
string compilerDirectoryPath = Path.Combine(sdkDirectoryPath, RoslynDirectoryName, CompilerBincoreDirectoryName);
if (Directory.Exists(compilerDirectoryPath))
{
return compilerDirectoryPath;
}
}
return null;
}
private static int CompareSdkDirectoryPathsDescending(string leftPath, string rightPath)
{
string leftVersionText = Path.GetFileName(leftPath);
string rightVersionText = Path.GetFileName(rightPath);
bool leftIsVersion = Version.TryParse(leftVersionText, out Version leftVersion);
bool rightIsVersion = Version.TryParse(rightVersionText, out Version rightVersion);
if (leftIsVersion && rightIsVersion)
{
int versionComparison = rightVersion.CompareTo(leftVersion);
if (versionComparison != 0)
{
return versionComparison;
}
}
else if (leftIsVersion)
{
return -1;
}
else if (rightIsVersion)
{
return 1;
}
return string.Compare(rightVersionText, leftVersionText, StringComparison.Ordinal);
}
private static string ResolveScriptingRootPathByScan(string contentsPath)
{
if (!Directory.Exists(contentsPath))
{
return null;
}
Queue<(string Path, int Depth)> pendingDirectories = new Queue<(string Path, int Depth)>();
pendingDirectories.Enqueue((contentsPath, 0));
while (pendingDirectories.Count > 0)
{
(string currentPath, int depth) = pendingDirectories.Dequeue();
if (ContainsExternalCompilerLayout(currentPath))
{
return currentPath;
}
if (depth >= 4)
{
continue;
}
foreach (string childDirectoryPath in Directory.GetDirectories(currentPath).OrderBy(path => path, StringComparer.Ordinal))
{
pendingDirectories.Enqueue((childDirectoryPath, depth + 1));
}
}
return null;
}
private static string ResolveEditorContentsPath(string editorPath)
{
if (editorPath.EndsWith(".app", StringComparison.OrdinalIgnoreCase))
{
return Path.Combine(editorPath, "Contents");
}
string editorDirectoryPath = Path.GetDirectoryName(editorPath);
if (string.IsNullOrEmpty(editorDirectoryPath))
{
return null;
}
string dataDirectoryPath = Path.Combine(editorDirectoryPath, "Data");
if (Directory.Exists(dataDirectoryPath))
{
return dataDirectoryPath;
}
string installRootPath = Path.GetDirectoryName(editorDirectoryPath);
return string.IsNullOrEmpty(installRootPath)
? null
: installRootPath;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f0a902466c1547459a9c96425de6aa5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
namespace io.github.hatayama.uLoopMCP
{
internal sealed class ExternalCompilerPaths
{
public string EditorContentsPath { get; }
public string ScriptingRootPath { get; }
public string DotnetHostPath { get; }
public string CompilerDllPath { get; }
public string CompilerRuntimeConfigPath { get; }
public string CompilerDepsFilePath { get; }
public string CodeAnalysisDllPath { get; }
public string CodeAnalysisCSharpDllPath { get; }
public string NetCoreRuntimeSharedDirectoryPath { get; }
public ExternalCompilerPaths(
string editorContentsPath,
string scriptingRootPath,
string dotnetHostPath,
string compilerDllPath,
string compilerRuntimeConfigPath,
string compilerDepsFilePath,
string codeAnalysisDllPath,
string codeAnalysisCSharpDllPath,
string netCoreRuntimeSharedDirectoryPath)
{
EditorContentsPath = editorContentsPath;
ScriptingRootPath = scriptingRootPath;
DotnetHostPath = dotnetHostPath;
CompilerDllPath = compilerDllPath;
CompilerRuntimeConfigPath = compilerRuntimeConfigPath;
CompilerDepsFilePath = compilerDepsFilePath;
CodeAnalysisDllPath = codeAnalysisDllPath;
CodeAnalysisCSharpDllPath = codeAnalysisCSharpDllPath;
NetCoreRuntimeSharedDirectoryPath = netCoreRuntimeSharedDirectoryPath;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bb3226a7b31442cf99c600c10958c3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
namespace io.github.hatayama.uLoopMCP
{
internal sealed class HoistedLiteralBinding
{
public string ParameterName { get; }
public string TypeName { get; }
public object Value { get; }
public HoistedLiteralBinding(string parameterName, string typeName, object value)
{
ParameterName = parameterName;
TypeName = typeName;
Value = value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d14d4c8ee615a4ab0a781cb6cdbcf63e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
namespace io.github.hatayama.uLoopMCP
{
internal interface ICompiledAssemblyBuilder
{
bool SupportsAutoPrewarm();
Task<CompiledAssemblyBuildResult> BuildAsync(
DynamicCompilationPlan plan,
CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a21ff6d17a104be1a2d9951ca23fe49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace io.github.hatayama.uLoopMCP
{
internal interface ICompiledAssemblyLoader
{
CompiledAssemblyLoadResult Load(
DynamicCodeSecurityLevel securityLevel,
byte[] assemblyBytes);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cbe1e5720235d4df18ae3f6ec2a03deb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
namespace io.github.hatayama.uLoopMCP
{
internal interface IDynamicCompilationPlanner
{
DynamicCompilationPlan CreatePlan(CompilationRequest request);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b0c124a0e80b4fcaa29ddf6226dc6e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,382 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Post-compilation security validator using IL byte inspection.
/// Scans call/callvirt/newobj/ldtoken opcodes to detect dangerous API usage
/// that source-level scanning cannot catch (e.g. using aliases, fully-qualified names).
/// </summary>
internal sealed class IlSecurityValidator
{
private static readonly OpCode[] SingleByteOpCodes = BuildOpCodeTable(singleByte: true);
private static readonly OpCode[] MultiByteOpCodes = BuildOpCodeTable(singleByte: false);
public SecurityValidationResult Validate(Assembly assembly)
{
SecurityValidationResult result = new SecurityValidationResult
{
IsValid = true,
Violations = new List<SecurityViolation>()
};
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = Array.FindAll(ex.Types, static t => t != null);
}
foreach (Type type in types)
{
ValidateTypeHierarchy(type, result);
ValidateMethodReferences(type, result);
}
result.IsValid = result.Violations.Count == 0;
return result;
}
private static void ValidateTypeHierarchy(Type type, SecurityValidationResult result)
{
ValidateReferencedType(
type.BaseType,
result,
typeName => $"Inheriting from dangerous type: {typeName}",
typeName => $"Type '{type.FullName}' inherits from blocked type '{typeName}'");
foreach (Type iface in type.GetInterfaces())
{
ValidateReferencedType(
iface,
result,
typeName => $"Implementing dangerous interface: {typeName}",
typeName => $"Type '{type.FullName}' implements blocked interface '{typeName}'");
}
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly;
foreach (FieldInfo field in type.GetFields(flags))
{
ValidateReferencedType(
field.FieldType,
result,
typeName => $"Using dangerous type as field: {typeName}",
typeName => $"Field '{field.Name}' of type '{typeName}' in {type.FullName}");
}
foreach (PropertyInfo property in type.GetProperties(flags))
{
ValidateReferencedType(
property.PropertyType,
result,
typeName => $"Using dangerous type as property: {typeName}",
typeName => $"Property '{property.Name}' of type '{typeName}' in {type.FullName}");
foreach (System.Reflection.ParameterInfo indexParameter in property.GetIndexParameters())
{
ValidateReferencedType(
indexParameter.ParameterType,
result,
typeName => $"Using dangerous type as property index parameter: {typeName}",
typeName => $"Property index parameter '{indexParameter.Name}' of type '{typeName}' in {type.FullName}.{property.Name}");
}
}
foreach (EventInfo eventInfo in type.GetEvents(flags))
{
ValidateReferencedType(
eventInfo.EventHandlerType,
result,
typeName => $"Using dangerous type as event handler: {typeName}",
typeName => $"Event '{eventInfo.Name}' of type '{typeName}' in {type.FullName}");
}
}
private static void ValidateMethodReferences(Type type, SecurityValidationResult result)
{
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly;
foreach (MethodInfo method in type.GetMethods(flags))
{
ValidateMethodBody(method, result);
}
foreach (ConstructorInfo ctor in type.GetConstructors(flags))
{
ValidateMethodBody(ctor, result);
}
}
private static void ValidateMethodBody(MethodBase method, SecurityValidationResult result)
{
MethodBody body;
try
{
body = method.GetMethodBody();
}
catch (InvalidOperationException)
{
// Abstract/extern methods have no body
return;
}
if (body == null) return;
foreach (LocalVariableInfo local in body.LocalVariables)
{
string typeName = local.LocalType?.FullName;
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = typeName,
Message = $"Using dangerous type as local variable: {typeName}",
Description = $"Local variable of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
foreach (System.Reflection.ParameterInfo param in method.GetParameters())
{
string typeName = param.ParameterType?.FullName;
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = typeName,
Message = $"Using dangerous type as parameter: {typeName}",
Description = $"Parameter '{param.Name}' of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
byte[] il = body.GetILAsByteArray();
if (il == null) return;
ValidateIlCalls(method, il, result);
}
private static void ValidateReferencedType(
Type referencedType,
SecurityValidationResult result,
Func<string, string> createMessage,
Func<string, string> createDescription)
{
foreach (string typeName in ExpandReferencedTypes(referencedType))
{
if (!DangerousApiCatalog.IsDangerousType(typeName))
{
continue;
}
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = typeName,
Message = createMessage(typeName),
Description = createDescription(typeName)
});
}
}
private static IEnumerable<string> ExpandReferencedTypes(Type referencedType)
{
if (referencedType == null)
{
yield break;
}
if (referencedType.HasElementType)
{
foreach (string typeName in ExpandReferencedTypes(referencedType.GetElementType()))
{
yield return typeName;
}
yield break;
}
string fullName = referencedType.FullName;
if (!string.IsNullOrEmpty(fullName))
{
yield return fullName;
}
if (!referencedType.IsGenericType)
{
yield break;
}
foreach (Type genericArgument in referencedType.GetGenericArguments())
{
foreach (string typeName in ExpandReferencedTypes(genericArgument))
{
yield return typeName;
}
}
}
private static void ValidateIlCalls(MethodBase method, byte[] il, SecurityValidationResult result)
{
int offset = 0;
Module module = method.Module;
Type[] typeArgs = method.DeclaringType?.IsGenericType == true
? method.DeclaringType.GetGenericArguments()
: null;
Type[] methodArgs = method.IsGenericMethod ? method.GetGenericArguments() : null;
while (offset < il.Length)
{
OpCode opCode = ReadOpCode(il, ref offset);
if (opCode.OperandType == OperandType.InlineMethod)
{
int token = ReadInt32(il, offset);
if (opCode == OpCodes.Call || opCode == OpCodes.Callvirt || opCode == OpCodes.Newobj ||
opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn)
{
MethodBase calledMethod;
try
{
calledMethod = module.ResolveMethod(token, typeArgs, methodArgs);
}
catch (ArgumentOutOfRangeException)
{
offset += 4;
continue;
}
catch (ArgumentException)
{
offset += 4;
continue;
}
string declaringType = calledMethod.DeclaringType?.FullName;
string memberName = calledMethod.Name;
if (declaringType != null && DangerousApiCatalog.IsDangerousApi(declaringType, memberName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = $"{declaringType}.{memberName}",
Message = $"Dangerous API call: {declaringType}.{memberName}",
Description = $"IL {opCode.Name} to '{declaringType}.{memberName}' in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
}
// ldtoken with InlineTok operand: detect typeof(DangerousType)
if (opCode == OpCodes.Ldtoken && opCode.OperandType == OperandType.InlineTok)
{
int token = ReadInt32(il, offset);
Type resolvedType;
try
{
resolvedType = module.ResolveType(token, typeArgs, methodArgs);
}
catch (ArgumentOutOfRangeException)
{
// ldtoken can also reference fields/methods, not just types
offset += GetOperandSize(opCode.OperandType, il, offset);
continue;
}
catch (ArgumentException)
{
// ldtoken can also reference fields/methods, not just types
offset += GetOperandSize(opCode.OperandType, il, offset);
continue;
}
string fullName = resolvedType.FullName;
if (fullName != null && DangerousApiCatalog.IsDangerousType(fullName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = fullName,
Message = $"typeof() reference to dangerous type: {fullName}",
Description = $"typeof({fullName}) in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
offset += GetOperandSize(opCode.OperandType, il, offset);
}
}
private static OpCode ReadOpCode(byte[] il, ref int offset)
{
byte first = il[offset++];
if (first != 0xFE) return SingleByteOpCodes[first];
byte second = il[offset++];
return MultiByteOpCodes[second];
}
private static int ReadInt32(byte[] il, int offset)
{
return il[offset] | (il[offset + 1] << 8) | (il[offset + 2] << 16) | (il[offset + 3] << 24);
}
private static int GetOperandSize(OperandType operandType, byte[] il, int offset)
{
switch (operandType)
{
case OperandType.InlineNone: return 0;
case OperandType.ShortInlineBrTarget:
case OperandType.ShortInlineI:
case OperandType.ShortInlineVar: return 1;
case OperandType.InlineVar: return 2;
case OperandType.InlineI:
case OperandType.InlineBrTarget:
case OperandType.InlineField:
case OperandType.InlineMethod:
case OperandType.InlineSig:
case OperandType.InlineString:
case OperandType.InlineTok:
case OperandType.InlineType:
case OperandType.ShortInlineR: return 4;
case OperandType.InlineI8:
case OperandType.InlineR: return 8;
case OperandType.InlineSwitch:
int count = ReadInt32(il, offset);
return 4 + (count * 4);
default: return 0;
}
}
private static OpCode[] BuildOpCodeTable(bool singleByte)
{
OpCode[] table = new OpCode[0x100];
foreach (FieldInfo field in typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.GetValue(null) is not OpCode opCode) continue;
ushort value = unchecked((ushort)opCode.Value);
if (singleByte && value <= 0xFF)
{
table[value] = opCode;
}
else if (!singleByte && (value & 0xFF00) == 0xFE00)
{
table[value & 0xFF] = opCode;
}
}
return table;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2ee54a51745834456a2a2b24860fc4f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,376 @@
using System.Collections.Generic;
using System.Diagnostics;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Scans wrapped source for potential type identifiers and pre-injects using directives
/// before the first compilation, avoiding AutoUsingResolver retry loops (~590ms/retry saved).
/// Only applies to script mode (wrapped source from WrapperTemplate); raw mode is excluded.
/// </summary>
internal static class PreUsingResolver
{
private static readonly HashSet<string> ExcludedIdentifiers = new(System.StringComparer.Ordinal)
{
"abstract", "as", "async", "await", "base", "bool", "break", "byte",
"case", "catch", "char", "checked", "class", "const", "continue",
"decimal", "default", "delegate", "do", "double", "else", "enum",
"event", "explicit", "extern", "false", "finally", "fixed", "float",
"for", "foreach", "goto", "if", "implicit", "in", "int", "interface",
"internal", "is", "lock", "long", "namespace", "new", "null", "object",
"operator", "out", "override", "params", "private", "protected", "public",
"readonly", "record", "ref", "return", "sbyte", "sealed", "short",
"sizeof", "stackalloc", "static", "string", "struct", "switch",
"this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked",
"unsafe", "ushort", "using", "var", "virtual", "void", "volatile", "while",
"yield",
"String", "Int32", "Int64", "Boolean", "Single", "Double", "Byte",
"Object", "Void", "Decimal", "Char", "UInt32", "UInt64", "Int16", "UInt16",
"SByte", "IntPtr", "UIntPtr",
"Task", "Dictionary", "CancellationToken", "ExecuteAsync", "Execute"
};
public static PreUsingResult Resolve(string wrappedSource, AssemblyTypeIndex index)
{
Debug.Assert(wrappedSource != null, "wrappedSource must not be null");
Debug.Assert(index != null, "index must not be null");
HashSet<string> existingNamespaces = ExtractExistingNamespaces(wrappedSource);
string userCodeSection = ExtractUserCodeSection(wrappedSource);
HashSet<string> candidateTypes = ExtractTypeIdentifiers(userCodeSection);
HashSet<string> qualifiedTypeIdentifiers = ExtractQualifiedTypeIdentifiers(userCodeSection);
HashSet<string> namespacesToAdd = new(System.StringComparer.Ordinal);
List<string> assemblyReferencesToAdd = new List<string>();
foreach (string typeName in candidateTypes)
{
List<string> namespaces = index.FindNamespacesForType(typeName);
if (namespaces.Count == 1 && !existingNamespaces.Contains(namespaces[0]))
{
namespacesToAdd.Add(namespaces[0]);
}
List<string> assemblyReferences = index.FindAssemblyLocationsForType(typeName);
if (assemblyReferences.Count == 1)
{
AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, assemblyReferences[0]);
}
}
foreach (string typeName in qualifiedTypeIdentifiers)
{
List<string> assemblyReferences = index.FindAssemblyLocationsForIdentifier(typeName);
if (assemblyReferences.Count == 1)
{
AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, assemblyReferences[0]);
}
}
if (namespacesToAdd.Count == 0)
{
return new PreUsingResult(
wrappedSource,
System.Array.Empty<string>(),
assemblyReferencesToAdd);
}
string updatedSource = AutoUsingResolver.InsertUsingDirectives(wrappedSource, namespacesToAdd);
return new PreUsingResult(updatedSource, namespacesToAdd, assemblyReferencesToAdd);
}
private static HashSet<string> ExtractExistingNamespaces(string source)
{
HashSet<string> namespaces = new(System.StringComparer.Ordinal);
int pos = 0;
int length = source.Length;
while (pos < length)
{
pos = SourceShaper.SkipWhitespace(source, pos);
if (pos >= length) break;
if (source[pos] == '#')
{
pos = SkipToEndOfLine(source, pos);
continue;
}
// Skip comments that appear before using directives
int skipped = SourceShaper.AdvanceOneTokenPublic(source, pos);
if (skipped > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_')
{
pos = skipped;
continue;
}
// Handle "global using Ns;" — advance past "global" to reach "using"
int usingPos = pos;
if (SourceShaper.StartsWithKeyword(source, pos, "global"))
{
usingPos = SourceShaper.SkipWhitespace(source, pos + 6);
}
if (!SourceShaper.StartsWithKeyword(source, usingPos, "using"))
{
break;
}
int afterUsing = SourceShaper.SkipWhitespace(source, usingPos + 5);
if (SourceShaper.StartsWithKeyword(source, afterUsing, "static") ||
SourceShaper.StartsWithKeyword(source, afterUsing, "var") ||
(afterUsing < length && source[afterUsing] == '('))
{
pos = SkipToSemicolon(source, pos);
continue;
}
int semiPos = source.IndexOf(';', afterUsing);
if (semiPos > afterUsing)
{
string ns = source.Substring(afterUsing, semiPos - afterUsing).Trim();
int eqIdx = ns.IndexOf('=');
if (eqIdx >= 0)
{
ns = ns.Substring(eqIdx + 1).Trim();
}
if (ns.Length > 0)
{
namespaces.Add(ns);
}
}
pos = semiPos >= 0 ? semiPos + 1 : length;
}
return namespaces;
}
internal static HashSet<string> ExtractTypeIdentifiers(string source)
{
HashSet<string> identifiers = new(System.StringComparer.Ordinal);
int pos = 0;
int length = source.Length;
bool prevWasDot = false;
while (pos < length)
{
char c = source[pos];
if (char.IsWhiteSpace(c))
{
pos++;
continue;
}
int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos);
if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_')
{
prevWasDot = false;
pos = advanced;
continue;
}
if (c == '.')
{
prevWasDot = true;
pos++;
continue;
}
if (char.IsLetter(c) || c == '_')
{
int start = pos;
while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_'))
{
pos++;
}
if (!prevWasDot && char.IsUpper(c))
{
string identifier = source.Substring(start, pos - start);
// Member initializers (Name = ...), named arguments (Name: ...),
// and labels (Name:) are not type candidates; skip them.
// Exclude == so comparisons are not mistakenly filtered.
int next = SourceShaper.SkipWhitespace(source, pos);
bool looksLikeMemberOrLabel =
next < length &&
(source[next] == ':' ||
(source[next] == '=' && (next + 1 >= length || source[next + 1] != '=')));
if (!looksLikeMemberOrLabel && !ExcludedIdentifiers.Contains(identifier))
{
identifiers.Add(identifier);
}
}
prevWasDot = false;
continue;
}
prevWasDot = false;
pos = advanced;
}
return identifiers;
}
internal static HashSet<string> ExtractQualifiedTypeIdentifiers(string source)
{
HashSet<string> identifiers = new(System.StringComparer.Ordinal);
int pos = 0;
int length = source.Length;
bool sawDot = false;
List<string> qualifiedChainParts = new List<string>();
while (pos < length)
{
char c = source[pos];
if (char.IsWhiteSpace(c))
{
pos++;
continue;
}
int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos);
if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_')
{
sawDot = false;
qualifiedChainParts.Clear();
pos = advanced;
continue;
}
if (c == '.')
{
sawDot = true;
pos++;
continue;
}
if (char.IsLetter(c) || c == '_')
{
int start = pos;
while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_'))
{
pos++;
}
string identifier = source.Substring(start, pos - start);
if (!sawDot)
{
qualifiedChainParts.Clear();
qualifiedChainParts.Add(identifier);
}
else
{
qualifiedChainParts.Add(identifier);
if (ShouldResolveQualifiedTypeAssembly(qualifiedChainParts))
{
identifiers.Add(string.Join(".", qualifiedChainParts));
}
}
sawDot = false;
continue;
}
sawDot = false;
qualifiedChainParts.Clear();
pos = advanced;
}
return identifiers;
}
private static bool ShouldResolveQualifiedTypeAssembly(IReadOnlyList<string> qualifiedChainParts)
{
return qualifiedChainParts != null && qualifiedChainParts.Count >= 2;
}
// WrapperTemplate marks user code with #line directives;
// scanning only this region avoids false positives from boilerplate identifiers
private static string ExtractUserCodeSection(string wrappedSource)
{
string startMarker = WrapperTemplate.UserCodeStartMarker;
string endMarker = WrapperTemplate.UserCodeEndMarker;
int startIdx = wrappedSource.IndexOf(startMarker, System.StringComparison.Ordinal);
// Wrapped source from WrapperTemplate should always contain the marker
Debug.Assert(startIdx >= 0, "UserCodeStartMarker not found in wrappedSource");
if (startIdx < 0)
{
return wrappedSource;
}
int codeStart = wrappedSource.IndexOf('\n', startIdx);
Debug.Assert(codeStart >= 0, "newline after UserCodeStartMarker not found");
if (codeStart < 0)
{
return wrappedSource;
}
codeStart++;
int endIdx = wrappedSource.IndexOf(endMarker, codeStart, System.StringComparison.Ordinal);
Debug.Assert(endIdx >= 0, "UserCodeEndMarker not found in wrappedSource");
if (endIdx < 0)
{
return wrappedSource.Substring(codeStart);
}
return wrappedSource.Substring(codeStart, endIdx - codeStart);
}
private static int SkipToEndOfLine(string s, int pos)
{
while (pos < s.Length && s[pos] != '\n') pos++;
if (pos < s.Length) pos++;
return pos;
}
private static int SkipToSemicolon(string s, int pos)
{
int semi = s.IndexOf(';', pos);
return semi >= 0 ? semi + 1 : s.Length;
}
private static void AddAssemblyReferenceIfMissing(
List<string> assemblyReferencesToAdd,
string assemblyReference)
{
if (string.IsNullOrEmpty(assemblyReference))
{
return;
}
foreach (string existingReference in assemblyReferencesToAdd)
{
if (string.Equals(existingReference, assemblyReference, System.StringComparison.OrdinalIgnoreCase))
{
return;
}
}
assemblyReferencesToAdd.Add(assemblyReference);
}
}
internal sealed class PreUsingResult
{
public string UpdatedSource { get; }
public IReadOnlyCollection<string> AddedNamespaces { get; }
public IReadOnlyCollection<string> AddedAssemblyReferences { get; }
public PreUsingResult(
string updatedSource,
IReadOnlyCollection<string> addedNamespaces,
IReadOnlyCollection<string> addedAssemblyReferences)
{
UpdatedSource = updatedSource;
AddedNamespaces = addedNamespaces;
AddedAssemblyReferences = addedAssemblyReferences;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd0c59b5504094ef0a08a56f0617524c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Compatibility wrapper that keeps the old type name while delegating to the structured metadata validator.
/// The preload security check must inspect real metadata rows so user literals do not trigger false positives.
/// </summary>
internal sealed class PreloadMetadataSecurityValidator : IPreloadAssemblySecurityValidator
{
private readonly SystemReflectionMetadataPreloadValidator _validator = new SystemReflectionMetadataPreloadValidator();
public SecurityValidationResult Validate(byte[] assemblyBytes)
{
return _validator.Validate(assemblyBytes);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd9a3932eef9a4a519b81f9b83044b52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace io.github.hatayama.uLoopMCP
{
internal sealed class PreparedDynamicCode
{
public string PreparedSource { get; }
public bool IsScriptMode { get; }
public List<HoistedLiteralBinding> HoistedLiteralBindings { get; }
public PreparedDynamicCode(
string preparedSource,
bool isScriptMode,
List<HoistedLiteralBinding> hoistedLiteralBindings)
{
PreparedSource = preparedSource;
IsScriptMode = isScriptMode;
HoistedLiteralBindings = hoistedLiteralBindings ?? new List<HoistedLiteralBinding>();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c942bf096c16b4ce581ba1ca5c5226ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,443 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
namespace io.github.hatayama.uLoopMCP
{
internal static class RoslynCompilerBackend
{
private sealed class OneShotCompileResult
{
public DynamicCompilationBackendResult BackendResult { get; }
public bool ShouldFallback { get; }
private OneShotCompileResult(DynamicCompilationBackendResult backendResult, bool shouldFallback)
{
BackendResult = backendResult;
ShouldFallback = shouldFallback;
}
public static OneShotCompileResult Successful(CompilerMessage[] compilerMessages)
{
return new OneShotCompileResult(
new DynamicCompilationBackendResult(
compilerMessages,
DynamicCompilationBackendKind.OneShotRoslyn),
false);
}
public static OneShotCompileResult Fallback()
{
return new OneShotCompileResult(null, true);
}
}
public static async Task<DynamicCompilationBackendResult> CompileAsync(
string sourcePath,
string dllPath,
List<string> references,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
string workerRequestFilePath = Path.ChangeExtension(sourcePath, ".worker");
string[] defineSymbols = GetActiveDefineSymbols();
bool allowUnsafeCode = PlayerSettings.allowUnsafeCode;
try
{
WriteWorkerRequestFile(
workerRequestFilePath,
sourcePath,
dllPath,
references,
defineSymbols,
allowUnsafeCode);
CompilerMessage[] workerMessages = SharedRoslynCompilerWorkerHost.TryCompile(
workerRequestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
if (workerMessages != null)
{
return new DynamicCompilationBackendResult(
workerMessages,
DynamicCompilationBackendKind.SharedRoslynWorker);
}
DynamicCompilationHealthMonitor.ReportSharedWorkerFallback(
"worker_unavailable",
new
{
platform = Application.platform.ToString(),
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
ct.ThrowIfCancellationRequested();
OneShotCompileResult oneShotResult = await CompileWithOneShotAsync(
sourcePath,
dllPath,
references,
defineSymbols,
allowUnsafeCode,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount).ConfigureAwait(false);
if (oneShotResult.ShouldFallback)
{
return await AssemblyBuilderFallbackCompilerBackend.CompileAsync(
sourcePath,
dllPath,
references,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount).ConfigureAwait(false);
}
return oneShotResult.BackendResult;
}
finally
{
if (File.Exists(workerRequestFilePath))
{
File.Delete(workerRequestFilePath);
}
}
}
private static async Task<OneShotCompileResult> CompileWithOneShotAsync(
string sourcePath,
string dllPath,
List<string> references,
IReadOnlyCollection<string> defineSymbols,
bool allowUnsafeCode,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
string responseFilePath = Path.ChangeExtension(sourcePath, ".rsp");
WriteCompilerResponseFile(
responseFilePath,
sourcePath,
dllPath,
references,
defineSymbols,
allowUnsafeCode);
try
{
incrementBuildCount();
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = externalCompilerPaths.DotnetHostPath,
Arguments = $"{QuoteCommandLineArgument(externalCompilerPaths.CompilerDllPath)} @{QuoteCommandLineArgument(responseFilePath)}",
WorkingDirectory = Directory.GetCurrentDirectory(),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
markBuildStarted();
try
{
using Process process = ProcessStartHelper.TryStart(startInfo);
if (process == null)
{
DynamicCompilationHealthMonitor.ReportOneShotCompilerStartFailure(new
{
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
return OneShotCompileResult.Fallback();
}
OneShotProcessCompletionResult completionResult = await WaitForOneShotCompilerAsync(process, ct)
.ConfigureAwait(false);
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
completionResult.StandardOutput,
completionResult.StandardError,
completionResult.ExitCode);
if (ShouldRetryWithAssemblyBuilder(process.ExitCode, compilerMessages))
{
ReportInfrastructureFallback(externalCompilerPaths, process.ExitCode);
return OneShotCompileResult.Fallback();
}
return OneShotCompileResult.Successful(compilerMessages);
}
finally
{
markBuildFinished();
}
}
finally
{
if (File.Exists(responseFilePath))
{
File.Delete(responseFilePath);
}
}
}
internal static void WriteCompilerResponseFile(
string responseFilePath,
string sourcePath,
string dllPath,
IReadOnlyCollection<string> references,
IReadOnlyCollection<string> defineSymbols,
bool allowUnsafeCode)
{
List<string> lines = new List<string>
{
"-nologo",
"-nostdlib+",
"-target:library",
"-optimize+",
"-debug-",
allowUnsafeCode ? "-unsafe+" : "-unsafe-",
QuoteResponseFileArgument("-out:", dllPath)
};
string defineOption = BuildDefineOption(defineSymbols);
if (!string.IsNullOrEmpty(defineOption))
{
lines.Add(defineOption);
}
foreach (string reference in references)
{
lines.Add(QuoteResponseFileArgument("-r:", reference));
}
lines.Add(QuoteResponseFilePath(sourcePath));
File.WriteAllLines(responseFilePath, lines);
}
internal static void WriteWorkerRequestFile(
string requestFilePath,
string sourcePath,
string dllPath,
IReadOnlyCollection<string> references,
IReadOnlyCollection<string> defineSymbols,
bool allowUnsafeCode)
{
List<string> lines = new List<string> { Path.GetFullPath(sourcePath), Path.GetFullPath(dllPath) };
lines.Add(allowUnsafeCode ? "unsafe:1" : "unsafe:0");
string serializedDefines = SerializeDefineSymbols(defineSymbols);
if (!string.IsNullOrEmpty(serializedDefines))
{
lines.Add($"define:{serializedDefines}");
}
foreach (string reference in references)
{
lines.Add($"ref:{Path.GetFullPath(reference)}");
}
File.WriteAllLines(Path.GetFullPath(requestFilePath), lines);
}
private static string[] GetActiveDefineSymbols()
{
string[] activeDefines = EditorUserBuildSettings.activeScriptCompilationDefines;
if (activeDefines == null || activeDefines.Length == 0)
{
return Array.Empty<string>();
}
List<string> filteredDefines = new List<string>(activeDefines.Length);
foreach (string define in activeDefines)
{
if (!string.IsNullOrWhiteSpace(define))
{
filteredDefines.Add(define);
}
}
return filteredDefines.ToArray();
}
private static string BuildDefineOption(IReadOnlyCollection<string> defineSymbols)
{
string serializedDefines = SerializeDefineSymbols(defineSymbols);
if (string.IsNullOrEmpty(serializedDefines))
{
return null;
}
return $"-define:{serializedDefines}";
}
private static string SerializeDefineSymbols(IReadOnlyCollection<string> defineSymbols)
{
if (defineSymbols == null || defineSymbols.Count == 0)
{
return string.Empty;
}
List<string> filteredDefines = new List<string>(defineSymbols.Count);
foreach (string defineSymbol in defineSymbols)
{
if (!string.IsNullOrWhiteSpace(defineSymbol))
{
filteredDefines.Add(defineSymbol);
}
}
return filteredDefines.Count == 0 ? string.Empty : string.Join(";", filteredDefines);
}
private static string QuoteResponseFileArgument(string prefix, string value)
{
return $"{prefix}{QuoteResponseFilePath(value)}";
}
private static string QuoteResponseFilePath(string path)
{
return $"\"{path}\"";
}
// Infrastructure-level failures (non-zero exit without file-specific diagnostics)
// indicate the compiler itself broke, not the user's code.
private static bool ShouldRetryWithAssemblyBuilder(
int exitCode,
IReadOnlyCollection<CompilerMessage> compilerMessages)
{
if (exitCode == 0)
{
return false;
}
foreach (CompilerMessage compilerMessage in compilerMessages)
{
if (compilerMessage.type == CompilerMessageType.Error &&
!string.IsNullOrWhiteSpace(compilerMessage.file))
{
return false;
}
}
return true;
}
private static string QuoteCommandLineArgument(string value)
{
return $"\"{value}\"";
}
internal sealed class OneShotProcessCompletionResult
{
public string StandardOutput { get; }
public string StandardError { get; }
public int ExitCode { get; }
public OneShotProcessCompletionResult(
string standardOutput,
string standardError,
int exitCode)
{
StandardOutput = standardOutput;
StandardError = standardError;
ExitCode = exitCode;
}
}
private static async Task<OneShotProcessCompletionResult> WaitForOneShotCompilerAsync(
Process process,
CancellationToken ct)
{
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
Task waitForExitTask = Task.Run(() => process.WaitForExit());
return await AwaitOneShotProcessCompletionAsync(
stdoutTask,
stderrTask,
waitForExitTask,
() => process.ExitCode,
() => RequestCancellation(process),
ct).ConfigureAwait(false);
}
private static void RequestCancellation(Process process)
{
if (!process.HasExited)
{
process.Kill();
}
}
internal static async Task<OneShotProcessCompletionResult> AwaitOneShotProcessCompletionAsync(
Task<string> stdoutTask,
Task<string> stderrTask,
Task waitForExitTask,
Func<int> getExitCode,
Action requestCancellation,
CancellationToken ct)
{
Task completionTask = Task.WhenAll(stdoutTask, stderrTask, waitForExitTask);
Task cancellationTask = Task.Delay(Timeout.Infinite, ct);
Task finishedTask = await Task.WhenAny(completionTask, cancellationTask).ConfigureAwait(false);
if (!ReferenceEquals(finishedTask, completionTask))
{
requestCancellation();
ObserveTaskFault(completionTask);
ct.ThrowIfCancellationRequested();
}
await completionTask.ConfigureAwait(false);
return new OneShotProcessCompletionResult(
stdoutTask.Result,
stderrTask.Result,
getExitCode());
}
internal static void ReportInfrastructureFallback(
ExternalCompilerPaths externalCompilerPaths,
int exitCode)
{
DynamicCompilationHealthMonitor.ReportOneShotCompilerStartFailure(new
{
reason = "infrastructure_failure",
exit_code = exitCode,
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
}
private static void ObserveTaskFault(Task task)
{
_ = task.ContinueWith(
static observedTask => _ = observedTask.Exception,
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 24f46670e12924be896455941a5956b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,946 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Compilation;
using Debug = UnityEngine.Debug;
namespace io.github.hatayama.uLoopMCP
{
internal static class SharedRoslynCompilerWorkerHost
{
private const int SharedCompilerWorkerMaxAttempts = 2;
private const string SharedCompilerWorkerResultPrefix = "__ULOOP_RESULT__";
private const string SharedCompilerWorkerEndMarker = "__ULOOP_END__";
private const string SharedCompilerWorkerQuitCommand = "__QUIT__";
internal const string CompileRequestPathPrefix = "path-base64:";
private const string RoslynWorkerSourceFileName = "RoslynCompilerWorker.cs";
private const string RoslynWorkerAssemblyFileName = "RoslynCompilerWorker.dll";
private const string RoslynWorkerCompileResponseFileName = "RoslynCompilerWorker.rsp";
private const string RoslynWorkerProgramTemplateRelativePath =
"Editor/Compilation/Templates/SharedRoslynCompilerWorkerProgram.cs.template";
private const string SharedCompilerWorkerResultPrefixToken = "{{SHARED_COMPILER_WORKER_RESULT_PREFIX}}";
private const string SharedCompilerWorkerEndMarkerToken = "{{SHARED_COMPILER_WORKER_END_MARKER}}";
private const string SharedCompilerWorkerQuitCommandToken = "{{SHARED_COMPILER_WORKER_QUIT_COMMAND}}";
private const string CompileRequestPathPrefixToken = "{{COMPILE_REQUEST_PATH_PREFIX}}";
private const int SharedCompilerWorkerResponseTimeoutMilliseconds = 30000;
private const int WorkerAssemblyBuildTimeoutMilliseconds = 30000;
internal const string DotnetMultilevelLookupEnvironmentVariableName = "DOTNET_MULTILEVEL_LOOKUP";
internal const string DotnetMultilevelLookupDisabledValue = "0";
private static readonly object SharedCompilerWorkerLock = new();
private static Action<string> s_deleteWorkerDirectory = path => Directory.Delete(path, true);
private static Action<Process, string> s_sendCompileRequest = SendCompileRequestCore;
private static Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]>
s_compileWorkerAssemblyForTests;
private static Process _sharedCompilerWorkerProcess;
private static string _workerDirectoryPath;
private sealed class WorkerAttemptResult
{
public CompilerMessage[] Messages { get; }
public bool ShouldRetry { get; }
public string FailureReason { get; }
public object FailureContext { get; }
private WorkerAttemptResult(
CompilerMessage[] messages,
bool shouldRetry,
string failureReason,
object failureContext)
{
Messages = messages;
ShouldRetry = shouldRetry;
FailureReason = failureReason;
FailureContext = failureContext;
}
public bool Succeeded => Messages != null;
public static WorkerAttemptResult Successful(CompilerMessage[] messages)
{
return new WorkerAttemptResult(messages, false, null, null);
}
public static WorkerAttemptResult RetryableFailure(string failureReason, object failureContext)
{
return new WorkerAttemptResult(null, true, failureReason, failureContext);
}
}
private sealed class WorkerStartupResult
{
public bool IsReady { get; }
public string FailureReason { get; }
public object FailureContext { get; }
private WorkerStartupResult(bool isReady, string failureReason, object failureContext)
{
IsReady = isReady;
FailureReason = failureReason;
FailureContext = failureContext;
}
public static WorkerStartupResult Ready()
{
return new WorkerStartupResult(true, null, null);
}
public static WorkerStartupResult Failure(string failureReason, object failureContext)
{
return new WorkerStartupResult(false, failureReason, failureContext);
}
}
private sealed class WorkerAssemblyBuildResult
{
public bool StartedSuccessfully { get; }
public CompilerMessage[] Messages { get; }
public string FailureReason { get; }
public object FailureContext { get; }
private WorkerAssemblyBuildResult(
bool startedSuccessfully,
CompilerMessage[] messages,
string failureReason,
object failureContext)
{
StartedSuccessfully = startedSuccessfully;
Messages = messages;
FailureReason = failureReason;
FailureContext = failureContext;
}
public static WorkerAssemblyBuildResult Started(CompilerMessage[] messages)
{
return new WorkerAssemblyBuildResult(true, messages, null, null);
}
public static WorkerAssemblyBuildResult StartFailure(string failureReason, object failureContext)
{
return new WorkerAssemblyBuildResult(false, null, failureReason, failureContext);
}
}
private sealed class WorkerPaths
{
public string DirectoryPath { get; }
public string SourcePath { get; }
public string AssemblyPath { get; }
public string CompileResponseFilePath { get; }
public WorkerPaths(
string directoryPath,
string sourcePath,
string assemblyPath,
string compileResponseFilePath)
{
DirectoryPath = directoryPath;
SourcePath = sourcePath;
AssemblyPath = assemblyPath;
CompileResponseFilePath = compileResponseFilePath;
}
}
[InitializeOnLoadMethod]
private static void RegisterLifecycle()
{
AssemblyReloadEvents.beforeAssemblyReload -= ShutdownForReload;
AssemblyReloadEvents.beforeAssemblyReload += ShutdownForReload;
EditorApplication.quitting -= ShutdownForQuit;
EditorApplication.quitting += ShutdownForQuit;
}
public static CompilerMessage[] TryCompile(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
lock (SharedCompilerWorkerLock)
{
return TryCompileWithRetries(
requestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
}
}
private static CompilerMessage[] TryCompileWithRetries(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
for (int attempt = 1; attempt <= SharedCompilerWorkerMaxAttempts; attempt++)
{
WorkerAttemptResult attemptResult = TryCompileOnce(
requestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
if (attemptResult.Succeeded)
{
return attemptResult.Messages;
}
ShutdownWorkerProcessLocked();
if (attemptResult.ShouldRetry && attempt < SharedCompilerWorkerMaxAttempts)
{
continue;
}
DynamicCompilationHealthMonitor.ReportSharedWorkerFailure(
attemptResult.FailureReason,
AppendAttempt(attemptResult.FailureContext, attempt));
return null;
}
return null;
}
private static WorkerAttemptResult TryCompileOnce(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
WorkerStartupResult startupResult = EnsureWorkerReady(externalCompilerPaths);
if (!startupResult.IsReady)
{
return WorkerAttemptResult.RetryableFailure(
startupResult.FailureReason,
startupResult.FailureContext);
}
return InvokeWorkerOnce(
requestFilePath,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
}
private static WorkerStartupResult EnsureWorkerReady(ExternalCompilerPaths externalCompilerPaths)
{
if (HasLiveWorkerProcess())
{
return WorkerStartupResult.Ready();
}
WorkerPaths workerPaths = CreateWorkerPaths();
SynchronizeWorkerSource(workerPaths);
WorkerStartupResult workerAssemblyResult = EnsureWorkerAssemblyBuilt(
externalCompilerPaths,
workerPaths);
if (!workerAssemblyResult.IsReady)
{
return workerAssemblyResult;
}
return StartWorkerProcess(externalCompilerPaths, workerPaths);
}
private static WorkerStartupResult EnsureWorkerAssemblyBuilt(
ExternalCompilerPaths externalCompilerPaths,
WorkerPaths workerPaths)
{
if (File.Exists(workerPaths.AssemblyPath))
{
return WorkerStartupResult.Ready();
}
WorkerAssemblyBuildResult buildResult = CompileWorkerAssembly(
externalCompilerPaths,
workerPaths.SourcePath,
workerPaths.AssemblyPath,
workerPaths.CompileResponseFilePath);
if (!buildResult.StartedSuccessfully)
{
return WorkerStartupResult.Failure(
buildResult.FailureReason,
buildResult.FailureContext);
}
if (!HasErrors(buildResult.Messages))
{
return WorkerStartupResult.Ready();
}
DeleteWorkerAssemblyIfPresent(workerPaths.AssemblyPath);
return WorkerStartupResult.Failure(
"worker_build_failed",
new
{
first_error = FindFirstErrorMessage(buildResult.Messages),
worker_source_path = workerPaths.SourcePath
});
}
private static WorkerStartupResult StartWorkerProcess(
ExternalCompilerPaths externalCompilerPaths,
WorkerPaths workerPaths)
{
ProcessStartInfo startInfo = CreateWorkerStartInfo(externalCompilerPaths, workerPaths);
_sharedCompilerWorkerProcess = ProcessStartHelper.TryStart(startInfo);
if (_sharedCompilerWorkerProcess == null)
{
return WorkerStartupResult.Failure(
"worker_start_failed",
new
{
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
worker_assembly_path = workerPaths.AssemblyPath
});
}
return WorkerStartupResult.Ready();
}
private static WorkerAttemptResult InvokeWorkerOnce(
string requestFilePath,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
if (!HasLiveWorkerProcess())
{
return WorkerAttemptResult.RetryableFailure(
"worker_process_missing",
new { request_file_path = requestFilePath });
}
ct.ThrowIfCancellationRequested();
incrementBuildCount();
markBuildStarted();
try
{
SendCompileRequest(requestFilePath);
return ReadWorkerResponse(requestFilePath, ct);
}
catch (IOException ex)
{
return CreateRetryableWorkerCommunicationFailure(requestFilePath, ex);
}
catch (ObjectDisposedException ex)
{
return CreateRetryableWorkerCommunicationFailure(requestFilePath, ex);
}
catch (OperationCanceledException)
{
ShutdownWorkerProcessLocked();
throw;
}
finally
{
markBuildFinished();
}
}
private static void SendCompileRequest(string requestFilePath)
{
string absoluteRequestFilePath = Path.GetFullPath(requestFilePath);
s_sendCompileRequest(_sharedCompilerWorkerProcess, absoluteRequestFilePath);
}
private static void SendCompileRequestCore(Process workerProcess, string requestFilePath)
{
string requestCommand = CreateCompileRequestCommand(requestFilePath);
workerProcess.StandardInput.WriteLine(requestCommand);
workerProcess.StandardInput.Flush();
}
private static string CreateCompileRequestCommand(string requestFilePath)
{
Debug.Assert(!string.IsNullOrEmpty(requestFilePath), "requestFilePath must not be empty");
string fullRequestFilePath = Path.GetFullPath(requestFilePath);
byte[] requestPathBytes = Encoding.UTF8.GetBytes(fullRequestFilePath);
return CompileRequestPathPrefix + Convert.ToBase64String(requestPathBytes);
}
private static WorkerAttemptResult ReadWorkerResponse(
string requestFilePath,
CancellationToken ct)
{
string responseHeader = ReadProtocolLine(
_sharedCompilerWorkerProcess.StandardOutput,
ct);
if (string.IsNullOrEmpty(responseHeader))
{
return WorkerAttemptResult.RetryableFailure(
"worker_empty_header",
new { request_file_path = requestFilePath });
}
if (!TryParseResponseHeader(responseHeader, out int exitCode))
{
return WorkerAttemptResult.RetryableFailure(
GetResponseHeaderFailureReason(responseHeader),
new { header = responseHeader });
}
List<string> outputLines = ReadDiagnosticLines(ct);
if (outputLines == null)
{
return WorkerAttemptResult.RetryableFailure(
"worker_missing_end_marker",
new { request_file_path = requestFilePath });
}
string combinedOutput = string.Join("\n", outputLines);
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(combinedOutput, string.Empty, exitCode);
return WorkerAttemptResult.Successful(compilerMessages);
}
private static bool TryParseResponseHeader(string responseHeader, out int exitCode)
{
exitCode = 0;
if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal))
{
return false;
}
string statusText = responseHeader.Substring(SharedCompilerWorkerResultPrefix.Length).Trim();
return int.TryParse(statusText, out exitCode);
}
private static string GetResponseHeaderFailureReason(string responseHeader)
{
if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal))
{
return "worker_invalid_header";
}
return "worker_invalid_exit_code";
}
private static List<string> ReadDiagnosticLines(CancellationToken ct)
{
List<string> outputLines = new List<string>();
while (true)
{
string outputLine = ReadProtocolLine(_sharedCompilerWorkerProcess.StandardOutput, ct);
if (outputLine == null)
{
return null;
}
if (outputLine == SharedCompilerWorkerEndMarker)
{
return outputLines;
}
outputLines.Add(outputLine);
}
}
private static WorkerPaths CreateWorkerPaths()
{
string workerDirectoryPath = GetWorkerDirectoryPath();
Directory.CreateDirectory(workerDirectoryPath);
_workerDirectoryPath = workerDirectoryPath;
return new WorkerPaths(
workerDirectoryPath,
Path.Combine(workerDirectoryPath, RoslynWorkerSourceFileName),
Path.Combine(workerDirectoryPath, RoslynWorkerAssemblyFileName),
Path.Combine(workerDirectoryPath, RoslynWorkerCompileResponseFileName));
}
private static string GetWorkerDirectoryPath()
{
return Path.Combine(
Path.GetTempPath(),
"uLoopMCPCompilation",
$"RoslynWorker-{Process.GetCurrentProcess().Id}");
}
private static void SynchronizeWorkerSource(WorkerPaths workerPaths)
{
string workerSource = CreateProgramSource();
if (File.Exists(workerPaths.SourcePath) && File.ReadAllText(workerPaths.SourcePath) == workerSource)
{
return;
}
File.WriteAllText(workerPaths.SourcePath, workerSource);
if (File.Exists(workerPaths.AssemblyPath))
{
File.Delete(workerPaths.AssemblyPath);
}
}
private static ProcessStartInfo CreateWorkerStartInfo(
ExternalCompilerPaths externalCompilerPaths,
WorkerPaths workerPaths)
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = externalCompilerPaths.DotnetHostPath,
Arguments = "exec"
+ " --runtimeconfig " + QuoteCommandLineArgument(externalCompilerPaths.CompilerRuntimeConfigPath)
+ " --depsfile " + QuoteCommandLineArgument(externalCompilerPaths.CompilerDepsFilePath)
+ " " + QuoteCommandLineArgument(workerPaths.AssemblyPath),
WorkingDirectory = workerPaths.DirectoryPath,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = false,
CreateNoWindow = true
};
ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
return startInfo;
}
internal static void ConfigureWorkerDotnetRuntimeEnvironment(ProcessStartInfo startInfo)
{
Debug.Assert(startInfo != null, "startInfo must not be null");
// Why: global probing can select a system .NET 6 runtime while Unity 6000.4 worker
// references come from the bundled .NET 8 runtime, which breaks assembly binding.
startInfo.EnvironmentVariables[DotnetMultilevelLookupEnvironmentVariableName] =
DotnetMultilevelLookupDisabledValue;
}
private static WorkerAssemblyBuildResult CompileWorkerAssembly(
ExternalCompilerPaths externalCompilerPaths,
string workerSourcePath,
string workerAssemblyPath,
string workerCompileResponseFilePath)
{
if (s_compileWorkerAssemblyForTests != null)
{
return WorkerAssemblyBuildResult.Started(
s_compileWorkerAssemblyForTests(
externalCompilerPaths,
workerSourcePath,
workerAssemblyPath,
workerCompileResponseFilePath));
}
WriteWorkerCompilerResponseFile(
workerCompileResponseFilePath,
workerSourcePath,
workerAssemblyPath,
BuildWorkerReferenceSet(externalCompilerPaths));
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = externalCompilerPaths.DotnetHostPath,
Arguments = $"{QuoteCommandLineArgument(externalCompilerPaths.CompilerDllPath)} @{QuoteCommandLineArgument(workerCompileResponseFilePath)}",
WorkingDirectory = Path.GetDirectoryName(workerSourcePath),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
using Process process = ProcessStartHelper.TryStart(startInfo);
if (process == null)
{
return WorkerAssemblyBuildResult.StartFailure(
"worker_compiler_start_failed",
new
{
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
}
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(WorkerAssemblyBuildTimeoutMilliseconds))
{
if (!process.HasExited)
{
process.Kill();
process.WaitForExit(500);
}
Task.WaitAll(stdoutTask, stderrTask);
return WorkerAssemblyBuildResult.StartFailure(
"worker_compiler_timeout",
new
{
timeout_ms = WorkerAssemblyBuildTimeoutMilliseconds,
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
compiler_dll_path = externalCompilerPaths.CompilerDllPath
});
}
Task.WaitAll(stdoutTask, stderrTask);
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
stdoutTask.GetAwaiter().GetResult(),
stderrTask.GetAwaiter().GetResult(),
process.ExitCode);
return WorkerAssemblyBuildResult.Started(compilerMessages);
}
private static List<string> BuildWorkerReferenceSet(ExternalCompilerPaths externalCompilerPaths)
{
string sharedRuntimeDirectoryPath = externalCompilerPaths.NetCoreRuntimeSharedDirectoryPath;
List<string> references = new List<string>
{
Path.Combine(sharedRuntimeDirectoryPath, "System.Private.CoreLib.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Console.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Collections.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.IO.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.Tasks.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Text.Encoding.Extensions.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.Extensions.dll"),
Path.Combine(sharedRuntimeDirectoryPath, "netstandard.dll"),
externalCompilerPaths.CodeAnalysisDllPath,
externalCompilerPaths.CodeAnalysisCSharpDllPath
};
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Collections.Immutable.dll"));
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Reflection.Metadata.dll"));
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.CompilerServices.Unsafe.dll"));
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Memory.dll"));
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Buffers.dll"));
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.Tasks.Extensions.dll"));
return references;
}
private static void WriteWorkerCompilerResponseFile(
string responseFilePath,
string sourcePath,
string dllPath,
IReadOnlyCollection<string> references)
{
List<string> lines = new List<string>
{
"-nologo",
"-nostdlib+",
"-target:exe",
"-optimize+",
"-debug-",
QuoteResponseFileArgument("-out:", dllPath)
};
foreach (string reference in references)
{
lines.Add(QuoteResponseFileArgument("-r:", reference));
}
lines.Add(QuoteResponseFilePath(sourcePath));
File.WriteAllLines(responseFilePath, lines);
}
private static string ReadProtocolLine(StreamReader reader, CancellationToken ct)
{
Debug.Assert(reader != null, "reader must not be null");
Task<string> readTask = Task.Run(() => reader.ReadLine());
Task timeoutTask = Task.Delay(SharedCompilerWorkerResponseTimeoutMilliseconds, ct);
Task completedTask = Task.WhenAny(readTask, timeoutTask).GetAwaiter().GetResult();
if (!ReferenceEquals(completedTask, readTask))
{
ct.ThrowIfCancellationRequested();
return null;
}
return readTask.GetAwaiter().GetResult();
}
private static bool HasLiveWorkerProcess()
{
return _sharedCompilerWorkerProcess != null && !_sharedCompilerWorkerProcess.HasExited;
}
private static bool HasErrors(IReadOnlyCollection<CompilerMessage> messages)
{
foreach (CompilerMessage message in messages)
{
if (message.type == CompilerMessageType.Error)
{
return true;
}
}
return false;
}
private static string FindFirstErrorMessage(IReadOnlyCollection<CompilerMessage> messages)
{
foreach (CompilerMessage message in messages)
{
if (message.type == CompilerMessageType.Error)
{
return message.message;
}
}
return string.Empty;
}
private static void DeleteWorkerAssemblyIfPresent(string assemblyPath)
{
if (File.Exists(assemblyPath))
{
File.Delete(assemblyPath);
}
}
private static void ShutdownForReload()
{
Shutdown();
}
private static void ShutdownForQuit()
{
Shutdown();
}
internal static void ShutdownForServerReset()
{
Shutdown();
}
internal static void ShutdownForTests()
{
Shutdown();
}
internal static Action<string> SwapWorkerDirectoryDeleterForTests(Action<string> deleter)
{
Debug.Assert(deleter != null, "deleter must not be null");
Action<string> previous = s_deleteWorkerDirectory;
s_deleteWorkerDirectory = deleter;
return previous;
}
internal static Action<Process, string> SwapCompileRequestSenderForTests(Action<Process, string> sender)
{
Debug.Assert(sender != null, "sender must not be null");
Action<Process, string> previous = s_sendCompileRequest;
s_sendCompileRequest = sender;
return previous;
}
internal static Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]>
SwapWorkerAssemblyCompilerForTests(
Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]> compiler)
{
Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]> previous =
s_compileWorkerAssemblyForTests;
s_compileWorkerAssemblyForTests = compiler;
return previous;
}
private static void Shutdown()
{
lock (SharedCompilerWorkerLock)
{
ShutdownWorkerProcessLocked();
CleanupWorkerDirectoryLocked();
}
}
private static void ShutdownWorkerProcessLocked()
{
Process workerProcess = _sharedCompilerWorkerProcess;
_sharedCompilerWorkerProcess = null;
if (workerProcess == null)
{
return;
}
try
{
if (!workerProcess.HasExited)
{
workerProcess.StandardInput.WriteLine(SharedCompilerWorkerQuitCommand);
workerProcess.StandardInput.Flush();
workerProcess.WaitForExit(500);
}
if (!workerProcess.HasExited)
{
workerProcess.Kill();
workerProcess.WaitForExit(500);
}
}
catch (IOException ex)
{
LogWorkerShutdownFailure(ex);
}
catch (ObjectDisposedException ex)
{
LogWorkerShutdownFailure(ex);
}
catch (InvalidOperationException ex)
{
LogWorkerShutdownFailure(ex);
}
finally
{
workerProcess.Dispose();
}
}
private static void CleanupWorkerDirectoryLocked()
{
string workerDirectoryPath = _workerDirectoryPath ?? GetWorkerDirectoryPath();
if (!Directory.Exists(workerDirectoryPath))
{
_workerDirectoryPath = null;
return;
}
TryDeleteWorkerDirectory(workerDirectoryPath);
_workerDirectoryPath = null;
}
private static void TryDeleteWorkerDirectory(string workerDirectoryPath)
{
try
{
s_deleteWorkerDirectory(workerDirectoryPath);
}
catch (IOException ex)
{
LogWorkerDirectoryCleanupFailure(workerDirectoryPath, ex);
}
catch (UnauthorizedAccessException ex)
{
LogWorkerDirectoryCleanupFailure(workerDirectoryPath, ex);
}
}
private static void LogWorkerDirectoryCleanupFailure(string workerDirectoryPath, Exception ex)
{
VibeLogger.LogWarning(
"dynamic_code_shared_worker_cleanup_failed",
"execute-dynamic-code shared Roslyn worker directory cleanup failed during shutdown",
new
{
worker_directory_path = workerDirectoryPath,
exception_type = ex.GetType().FullName,
exception_message = ex.Message
},
humanNote: "Shared Roslyn worker cleanup could not remove its temporary directory during shutdown.",
aiTodo: "Investigate file locks or permission issues if temporary worker directories continue to accumulate.");
Debug.LogWarning($"[{McpConstants.PROJECT_NAME}] Failed to delete shared Roslyn worker directory '{workerDirectoryPath}': {ex.Message}");
}
private static WorkerAttemptResult CreateRetryableWorkerCommunicationFailure(
string requestFilePath,
Exception ex)
{
return WorkerAttemptResult.RetryableFailure(
"worker_communication_failed",
new
{
request_file_path = requestFilePath,
exception_type = ex.GetType().FullName,
exception_message = ex.Message
});
}
private static void LogWorkerShutdownFailure(Exception ex)
{
VibeLogger.LogWarning(
"dynamic_code_shared_worker_shutdown_failed",
"execute-dynamic-code shared Roslyn worker shutdown observed a communication failure",
new
{
exception_type = ex.GetType().FullName,
exception_message = ex.Message
},
humanNote: "Shared Roslyn worker shutdown saw a broken communication channel while cleaning up a crashed worker.",
aiTodo: "Investigate repeated worker shutdown communication failures if shared compilation stops recovering cleanly.");
}
private static object AppendAttempt(object failureContext, int attempt)
{
return new
{
attempt,
details = failureContext
};
}
private static string QuoteResponseFileArgument(string prefix, string value)
{
return $"{prefix}{QuoteResponseFilePath(value)}";
}
private static string QuoteResponseFilePath(string path)
{
return $"\"{path}\"";
}
private static string QuoteCommandLineArgument(string value)
{
return $"\"{value}\"";
}
private static void AddIfExists(
List<string> destination,
string referencePath)
{
if (string.IsNullOrEmpty(referencePath) || !File.Exists(referencePath))
{
return;
}
destination.Add(referencePath);
}
private static string CreateProgramSource()
{
string templatePath = GetWorkerProgramTemplatePath();
string templateSource = File.ReadAllText(templatePath, Encoding.UTF8);
return templateSource
.Replace(SharedCompilerWorkerResultPrefixToken, SharedCompilerWorkerResultPrefix)
.Replace(SharedCompilerWorkerEndMarkerToken, SharedCompilerWorkerEndMarker)
.Replace(SharedCompilerWorkerQuitCommandToken, SharedCompilerWorkerQuitCommand)
.Replace(CompileRequestPathPrefixToken, CompileRequestPathPrefix);
}
private static string GetWorkerProgramTemplatePath()
{
return Path.Combine(McpConstants.PackageResolvedPath, RoslynWorkerProgramTemplateRelativePath);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b64646e28ac744f7384180d34bf2e5c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Pre-compilation source-level security scanner.
/// Blocks dangerous API patterns before code reaches the compiler,
/// preventing actually harmful code from executing (e.g. Process.Kill crashing Unity).
/// </summary>
internal static class SourceSecurityScanner
{
private static readonly Regex StripCommentsAndStringsRegex = new Regex(
@"
//[^\n]* | # line comment
/\*[\s\S]*?\*/ | # block comment
@""(?:""""|[^""])*"" | # verbatim string
""(?:\\.|[^\\""\n])*"" | # regular string
'(?:\\.|[^\\'\n])*' # char literal
",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private static readonly List<DangerousPattern> Patterns = new()
{
// Process manipulation
new("Process", "Start", @"\bProcess\s*\.\s*Start\b"),
new("Process", "Kill", @"\b(?:global::\s*)?(?:System\s*\.\s*Diagnostics\s*\.\s*)?Process\s*\.\s*Kill\b"),
// File system destructive operations
new("File", "Delete", @"\bFile\s*\.\s*Delete\b"),
new("File", "WriteAllText", @"\bFile\s*\.\s*WriteAllText\b"),
new("File", "WriteAllBytes", @"\bFile\s*\.\s*WriteAllBytes\b"),
new("File", "Replace", @"\bFile\s*\.\s*Replace\b"),
new("File", "OpenWrite", @"\bFile\s*\.\s*OpenWrite\b"),
new("File", "AppendAllText", @"\bFile\s*\.\s*AppendAllText\b"),
new("Directory", "Delete", @"\bDirectory\s*\.\s*Delete\b"),
// Network
new("HttpClient", "new", @"\bnew\s+HttpClient\b"),
new("HttpClient", "usage", @"\bHttpClient\s*\("),
new("WebClient", "new", @"\bnew\s+WebClient\b"),
new("WebRequest", "Create", @"\bWebRequest\s*\.\s*Create\b"),
new("Socket", "new", @"\bnew\s+Socket\b"),
new("TcpClient", "new", @"\bnew\s+TcpClient\b|\bTcpClient\s*\("),
new("UdpClient", "new", @"\bnew\s+UdpClient\b|\bUdpClient\s*\("),
new("ClientWebSocket", "new",
@"\bnew\s+(?:global::\s*)?(?:System\s*\.\s*Net\s*\.\s*WebSockets\s*\.\s*)?ClientWebSocket\b|\bClientWebSocket\s*\("),
new("Dns", "GetHostEntry", @"\bDns\s*\.\s*GetHostEntry\b"),
new("UnityWebRequest", "new", @"\bnew\s+UnityWebRequest\b"),
// Reflection execution
new("Assembly", "Load", @"\bAssembly\s*\.\s*Load\b"),
new("Assembly", "LoadFrom", @"\bAssembly\s*\.\s*LoadFrom\b"),
new("Assembly", "GetType", @"\b(?:global::\s*)?(?:System\s*\.\s*Reflection\s*\.\s*)?Assembly\s*\.\s*GetType\s*\("),
new("Type", "GetType", @"\b(?:global::\s*)?(?:System\s*\.\s*)?Type\s*\.\s*GetType\s*\("),
new("Activator", "CreateComInstanceFrom", @"\bActivator\s*\.\s*CreateComInstanceFrom\b"),
// Environment
new("Environment", "Exit", @"\bEnvironment\s*\.\s*Exit\b"),
new("Environment", "FailFast", @"\bEnvironment\s*\.\s*FailFast\b"),
// GC manipulation
new("GCSettings", "LatencyMode", @"\bGCSettings\s*\.\s*LatencyMode\s*="),
// Thread manipulation
new("Thread", "Abort", @"\bThread\s*\.\s*Abort\s*\("),
// Asset deletion
new("AssetDatabase", "DeleteAsset", @"\bAssetDatabase\s*\.\s*DeleteAsset\b"),
new("FileUtil", "DeleteFileOrDirectory", @"\bFileUtil\s*\.\s*DeleteFileOrDirectory\b"),
// Unsafe code
new("unsafe", "block", @"\bunsafe\s*\{|\bunsafe\s+\w"),
new("DllImport", "attribute", @"\bDllImport\b"),
new("extern", "declaration", @"\bextern\s+\w"),
new("ModuleInitializer", "attribute", @"\bModuleInitializer\b"),
};
public static SecurityValidationResult Scan(string sourceCode)
{
SecurityValidationResult result = new SecurityValidationResult
{
IsValid = true,
Violations = new List<SecurityViolation>()
};
if (string.IsNullOrWhiteSpace(sourceCode)) return result;
// Strip comments and string literals to avoid false positives
string stripped = StripCommentsAndStrings(sourceCode);
foreach (DangerousPattern pattern in Patterns)
{
if (pattern.Regex.IsMatch(stripped))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = $"{pattern.TypeName}.{pattern.MemberName}",
Message = $"Dangerous API detected: {pattern.TypeName}.{pattern.MemberName}",
Description = $"Source contains blocked pattern: {pattern.TypeName}.{pattern.MemberName}"
});
}
}
result.IsValid = result.Violations.Count == 0;
return result;
}
private static string StripCommentsAndStrings(string source)
{
// Replace string literals and comments with spaces to avoid false positives
// while preserving line structure for position tracking
return StripCommentsAndStringsRegex.Replace(source, match => new string(' ', match.Length));
}
private sealed class DangerousPattern
{
public string TypeName { get; }
public string MemberName { get; }
public Regex Regex { get; }
public DangerousPattern(string typeName, string memberName, string regexPattern)
{
TypeName = typeName;
MemberName = memberName;
Regex = new Regex(regexPattern, RegexOptions.Singleline | RegexOptions.Compiled);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52aa81fdca60b4deab0d69d7e58014a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,591 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Lightweight C# source scanner that determines top-level structure without Roslyn.
/// Tracks string literals, comments, and brace depth to classify source into one of three modes.
/// </summary>
internal static class SourceShaper
{
public static SourceShapeResult Analyze(string source)
{
Debug.Assert(source != null, "source must not be null");
SourceShapeResult result = new SourceShapeResult();
int length = source.Length;
int pos = 0;
int braceDepth = 0;
while (pos < length)
{
pos = SkipWhitespace(source, pos);
if (pos >= length) break;
if (braceDepth == 0)
{
if (TryMatchLineComment(source, pos, out int afterComment))
{
pos = afterComment;
continue;
}
if (TryMatchBlockComment(source, pos, out int afterBlock))
{
pos = afterBlock;
continue;
}
if (StartsWithKeyword(source, pos, "using"))
{
int segmentStart = pos;
int afterUsing = pos + 5;
afterUsing = SkipWhitespace(source, afterUsing);
if (StartsWithKeyword(source, afterUsing, "static"))
{
int end = FindSemicolon(source, segmentStart);
result.UsingDirectives.Add(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd());
pos = end + 1;
continue;
}
// "using var" and "using (" are using-statements, not using-directives
if (afterUsing < length && (StartsWithKeyword(source, afterUsing, "var") || source[afterUsing] == '('))
{
result.HasTopLevelStatements = true;
int end = FindStatementEnd(source, segmentStart, ref braceDepth);
result.TopLevelBodyBuilder.AppendLine(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd());
pos = end + 1;
continue;
}
int semiEnd = FindSemicolon(source, segmentStart);
result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd());
pos = semiEnd + 1;
continue;
}
if (StartsWithKeyword(source, pos, "namespace"))
{
result.HasNamespaceDeclaration = true;
pos = SkipBlock(source, pos, ref braceDepth);
continue;
}
if (IsTypeDeclarationKeyword(source, pos))
{
result.HasTypeDeclaration = true;
pos = SkipBlock(source, pos, ref braceDepth);
continue;
}
if (StartsWithKeyword(source, pos, "global") && StartsWithKeyword(source, SkipWhitespace(source, pos + 6), "using"))
{
int segmentStart = pos;
int semiEnd = FindSemicolon(source, segmentStart);
result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd());
pos = semiEnd + 1;
continue;
}
if (pos < length && source[pos] == '[')
{
int afterAttr = SkipAttributeBlock(source, pos);
int nextNonWs = SkipWhitespace(source, afterAttr);
if (nextNonWs < length && IsTypeDeclarationKeyword(source, nextNonWs))
{
result.HasTypeDeclaration = true;
pos = SkipBlock(source, nextNonWs, ref braceDepth);
continue;
}
}
if (IsAccessModifier(source, pos))
{
int afterMod = SkipAccessModifiers(source, pos);
if (IsTypeDeclarationKeyword(source, afterMod))
{
result.HasTypeDeclaration = true;
pos = SkipBlock(source, afterMod, ref braceDepth);
continue;
}
}
result.HasTopLevelStatements = true;
int stmtStart = pos;
int stmtEnd = FindStatementEnd(source, stmtStart, ref braceDepth);
result.TopLevelBodyBuilder.AppendLine(source.Substring(stmtStart, stmtEnd - stmtStart + 1).TrimEnd());
pos = stmtEnd + 1;
}
else
{
pos = AdvanceInsideBlock(source, pos, ref braceDepth);
}
}
return result;
}
public static string WrapIfNeeded(string source, string namespaceName, string className)
{
SourceShapeResult shape = Analyze(source);
// Raw mode: namespace or type declaration without top-level statements → pass through
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && !shape.HasTopLevelStatements)
{
return source;
}
// Mixed mode: both type declarations and top-level statements → error
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && shape.HasTopLevelStatements)
{
return null;
}
// Script mode: wrap top-level statements
string body = shape.TopLevelBodyBuilder.ToString().TrimEnd();
bool hasReturn = TopLevelReturnDetector.HasTopLevelReturn(body);
if (!hasReturn)
{
body = string.IsNullOrWhiteSpace(body)
? "return null;"
: body + "\nreturn null;";
}
return WrapperTemplate.Build(shape.UsingDirectives, namespaceName, className, body);
}
internal static int SkipWhitespace(string s, int pos)
{
Debug.Assert(s != null, "s must not be null");
Debug.Assert(pos >= 0, "pos must be non-negative");
while (pos < s.Length && char.IsWhiteSpace(s[pos])) pos++;
return pos;
}
internal static bool StartsWithKeyword(string s, int pos, string keyword)
{
Debug.Assert(s != null, "s must not be null");
Debug.Assert(keyword != null, "keyword must not be null");
Debug.Assert(pos >= 0, "pos must be non-negative");
if (pos + keyword.Length > s.Length) return false;
for (int i = 0; i < keyword.Length; i++)
{
if (s[pos + i] != keyword[i]) return false;
}
// Keyword must be followed by non-identifier char
int afterPos = pos + keyword.Length;
if (afterPos < s.Length && (char.IsLetterOrDigit(s[afterPos]) || s[afterPos] == '_'))
{
return false;
}
return true;
}
private static bool IsTypeDeclarationKeyword(string s, int pos)
{
return StartsWithKeyword(s, pos, "class") ||
StartsWithKeyword(s, pos, "struct") ||
StartsWithKeyword(s, pos, "interface") ||
StartsWithKeyword(s, pos, "enum") ||
StartsWithKeyword(s, pos, "record");
}
private static bool IsAccessModifier(string s, int pos)
{
return StartsWithKeyword(s, pos, "public") ||
StartsWithKeyword(s, pos, "internal") ||
StartsWithKeyword(s, pos, "private") ||
StartsWithKeyword(s, pos, "protected") ||
StartsWithKeyword(s, pos, "static") ||
StartsWithKeyword(s, pos, "sealed") ||
StartsWithKeyword(s, pos, "abstract") ||
StartsWithKeyword(s, pos, "partial");
}
private static int SkipAccessModifiers(string s, int pos)
{
while (pos < s.Length && IsAccessModifier(s, pos))
{
int wordEnd = pos;
while (wordEnd < s.Length && (char.IsLetterOrDigit(s[wordEnd]) || s[wordEnd] == '_')) wordEnd++;
pos = SkipWhitespace(s, wordEnd);
}
return pos;
}
private static bool TryMatchLineComment(string s, int pos, out int afterComment)
{
afterComment = pos;
if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '/')
{
int end = pos + 2;
while (end < s.Length && s[end] != '\n') end++;
if (end < s.Length) end++; // skip \n
afterComment = end;
return true;
}
return false;
}
private static bool TryMatchBlockComment(string s, int pos, out int afterBlock)
{
afterBlock = pos;
if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '*')
{
int end = pos + 2;
while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
afterBlock = end + 2 < s.Length ? end + 2 : s.Length;
return true;
}
return false;
}
private static int FindSemicolon(string s, int pos)
{
while (pos < s.Length)
{
if (s[pos] == ';') return pos;
pos = AdvanceOneToken(s, pos);
}
return s.Length - 1;
}
private static int FindStatementEnd(string s, int pos, ref int braceDepth)
{
while (pos < s.Length)
{
char c = s[pos];
if (c == '{')
{
braceDepth++;
pos++;
while (pos < s.Length && braceDepth > 0)
{
pos = AdvanceInsideBlock(s, pos, ref braceDepth);
}
return pos - 1;
}
if (c == ';') return pos;
pos = AdvanceOneToken(s, pos);
}
return s.Length - 1;
}
private static int SkipBlock(string s, int pos, ref int braceDepth)
{
// Advance past keywords until we hit the opening brace
while (pos < s.Length && s[pos] != '{')
{
if (s[pos] == ';') return pos + 1; // forward declaration
pos = AdvanceOneToken(s, pos);
}
if (pos < s.Length && s[pos] == '{')
{
braceDepth++;
pos++;
while (pos < s.Length && braceDepth > 0)
{
pos = AdvanceInsideBlock(s, pos, ref braceDepth);
}
}
return pos;
}
private static int SkipAttributeBlock(string s, int pos)
{
Debug.Assert(s[pos] == '[', "SkipAttributeBlock must start at '['");
int depth = 1;
pos++;
while (pos < s.Length && depth > 0)
{
if (s[pos] == '[') depth++;
else if (s[pos] == ']') depth--;
else pos = AdvanceOneToken(s, pos) - 1; // -1 because loop will pos++ via fallthrough
pos++;
}
return pos;
}
private static int AdvanceInsideBlock(string s, int pos, ref int braceDepth)
{
char c = s[pos];
if (c == '{') { braceDepth++; return pos + 1; }
if (c == '}') { braceDepth--; return pos + 1; }
return AdvanceOneToken(s, pos);
}
internal static int AdvanceOneTokenPublic(string s, int pos)
{
return AdvanceOneToken(s, pos);
}
private static int AdvanceOneToken(string s, int pos)
{
if (pos >= s.Length) return s.Length;
char c = s[pos];
// Line comment
if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '/')
{
int end = pos + 2;
while (end < s.Length && s[end] != '\n') end++;
return end < s.Length ? end + 1 : s.Length;
}
// Block comment
if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '*')
{
int end = pos + 2;
while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
return end + 2 < s.Length ? end + 2 : s.Length;
}
// Verbatim string (@"...")
if (c == '@' && pos + 1 < s.Length && s[pos + 1] == '"')
{
int end = pos + 2;
while (end < s.Length)
{
if (s[end] == '"')
{
if (end + 1 < s.Length && s[end + 1] == '"') { end += 2; continue; }
return end + 1;
}
end++;
}
return s.Length;
}
// Raw string literal (""" ... """)
if (c == '"' && pos + 2 < s.Length && s[pos + 1] == '"' && s[pos + 2] == '"')
{
int end = pos + 3;
while (end + 2 < s.Length)
{
if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"') return end + 3;
end++;
}
return s.Length;
}
// Regular string literal ("...")
if (c == '"')
{
int end = pos + 1;
while (end < s.Length && s[end] != '"')
{
if (s[end] == '\\') end++; // skip escaped char
end++;
}
return end < s.Length ? end + 1 : s.Length;
}
// Char literal ('x')
if (c == '\'')
{
int end = pos + 1;
while (end < s.Length && s[end] != '\'')
{
if (s[end] == '\\') end++;
end++;
}
return end < s.Length ? end + 1 : s.Length;
}
// Interpolated string ($"...")
if (c == '$' && pos + 1 < s.Length && s[pos + 1] == '"')
{
return SkipInterpolatedString(s, pos + 2);
}
return pos + 1;
}
private static int SkipInterpolatedString(string s, int pos)
{
int end = pos;
while (end < s.Length)
{
if (s[end] == '\\')
{
end += 2;
continue;
}
if (s[end] == '{')
{
if (end + 1 < s.Length && s[end + 1] == '{')
{
end += 2;
continue;
}
end = SkipInterpolationHole(s, end + 1);
continue;
}
if (s[end] == '"' && end + 1 < s.Length && s[end + 1] == '"')
{
end += 2;
continue;
}
if (s[end] == '"')
{
return end + 1;
}
end++;
}
return s.Length;
}
private static int SkipInterpolationHole(string s, int pos)
{
int depth = 1;
int end = pos;
while (end < s.Length && depth > 0)
{
if (s[end] == '\\')
{
end += 2;
continue;
}
if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"')
{
end = SkipVerbatimString(s, end + 2);
continue;
}
if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"')
{
end = SkipInterpolatedString(s, end + 2);
continue;
}
if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"')
{
end = SkipRawString(s, end + 3);
continue;
}
if (s[end] == '"')
{
end = SkipRegularString(s, end + 1);
continue;
}
if (s[end] == '\'')
{
end = SkipCharLiteral(s, end + 1);
continue;
}
if (s[end] == '{')
{
depth++;
end++;
continue;
}
if (s[end] == '}')
{
depth--;
end++;
continue;
}
end++;
}
return end;
}
private static int SkipRegularString(string s, int pos)
{
int end = pos;
while (end < s.Length && s[end] != '"')
{
if (s[end] == '\\')
{
end++;
}
end++;
}
return end < s.Length ? end + 1 : s.Length;
}
private static int SkipVerbatimString(string s, int pos)
{
int end = pos;
while (end < s.Length)
{
if (s[end] == '"')
{
if (end + 1 < s.Length && s[end + 1] == '"')
{
end += 2;
continue;
}
return end + 1;
}
end++;
}
return s.Length;
}
private static int SkipRawString(string s, int pos)
{
int end = pos;
while (end + 2 < s.Length)
{
if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"')
{
return end + 3;
}
end++;
}
return s.Length;
}
private static int SkipCharLiteral(string s, int pos)
{
int end = pos;
while (end < s.Length && s[end] != '\'')
{
if (s[end] == '\\')
{
end++;
}
end++;
}
return end < s.Length ? end + 1 : s.Length;
}
}
internal sealed class SourceShapeResult
{
public List<string> UsingDirectives { get; } = new List<string>();
public bool HasNamespaceDeclaration { get; set; }
public bool HasTypeDeclaration { get; set; }
public bool HasTopLevelStatements { get; set; }
public StringBuilder TopLevelBodyBuilder { get; } = new StringBuilder();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 660677c36e0474b3fb33948f49201a04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ae70dd3da0e7d5745b800c341d218e0d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,322 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
public static class Program
{
private const string ResultPrefix = "{{SHARED_COMPILER_WORKER_RESULT_PREFIX}}";
private const string EndMarker = "{{SHARED_COMPILER_WORKER_END_MARKER}}";
private const string QuitCommand = "{{SHARED_COMPILER_WORKER_QUIT_COMMAND}}";
private const string RequestPathPrefix = "{{COMPILE_REQUEST_PATH_PREFIX}}";
private const string UnsafePrefix = "unsafe:";
private const string DefinePrefix = "define:";
private const string ReferencePrefix = "ref:";
private static readonly object SyncRoot = new object();
private static readonly Dictionary<string, CachedReference> Cache = new Dictionary<string, CachedReference>(StringComparer.OrdinalIgnoreCase);
public static int Main()
{
string requestPath;
while ((requestPath = Console.ReadLine()) != null)
{
if (requestPath == QuitCommand)
{
return 0;
}
CompileResponse response = Compile(DecodeRequestPath(requestPath));
Console.WriteLine(ResultPrefix + " " + response.ExitCode);
foreach (string diagnosticLine in response.DiagnosticLines)
{
Console.WriteLine(diagnosticLine);
}
Console.WriteLine(EndMarker);
Console.Out.Flush();
}
return 0;
}
private static string DecodeRequestPath(string requestPath)
{
int encodedPathIndex = FindRequestPathPrefixIndex(requestPath);
if (encodedPathIndex >= 0)
{
string encodedPath = requestPath.Substring(encodedPathIndex + RequestPathPrefix.Length);
if (!IsBase64Payload(encodedPath))
{
return RecoverRawRequestPath(requestPath);
}
byte[] requestPathBytes = Convert.FromBase64String(encodedPath);
return Encoding.UTF8.GetString(requestPathBytes);
}
return RecoverRawRequestPath(requestPath);
}
private static bool IsBase64Payload(string value)
{
if (string.IsNullOrEmpty(value) || value.Length % 4 != 0 || !HasValidBase64Padding(value))
{
return false;
}
for (int i = 0; i < value.Length; i++)
{
char character = value[i];
bool isBase64Character = character >= 'A' && character <= 'Z'
|| character >= 'a' && character <= 'z'
|| character >= '0' && character <= '9'
|| character == '+'
|| character == '/'
|| character == '=';
if (!isBase64Character)
{
return false;
}
}
return true;
}
private static bool HasValidBase64Padding(string value)
{
int firstPaddingIndex = value.IndexOf('=');
if (firstPaddingIndex < 0)
{
return true;
}
int paddingLength = value.Length - firstPaddingIndex;
if (paddingLength > 2)
{
return false;
}
for (int i = firstPaddingIndex; i < value.Length; i++)
{
if (value[i] != '=')
{
return false;
}
}
return true;
}
private static int FindRequestPathPrefixIndex(string requestPath)
{
int encodedPathIndex = requestPath.IndexOf(RequestPathPrefix, StringComparison.Ordinal);
if (encodedPathIndex <= 0)
{
return encodedPathIndex;
}
return HasDirectorySeparatorBeforePrefix(requestPath, encodedPathIndex) ? -1 : encodedPathIndex;
}
private static bool HasDirectorySeparatorBeforePrefix(string requestPath, int prefixIndex)
{
for (int i = 0; i < prefixIndex; i++)
{
if (requestPath[i] == '\\' || requestPath[i] == '/')
{
return true;
}
}
return false;
}
private static string RecoverRawRequestPath(string requestPath)
{
string trimmedPath = requestPath.TrimStart('\uFEFF', '\uFFFD');
int drivePathIndex = FindWindowsDrivePathIndex(trimmedPath);
return drivePathIndex > 0 ? trimmedPath.Substring(drivePathIndex) : trimmedPath;
}
private static int FindWindowsDrivePathIndex(string value)
{
for (int i = 0; i <= value.Length - 3; i++)
{
bool isDriveLetter = (value[i] >= 'A' && value[i] <= 'Z') || (value[i] >= 'a' && value[i] <= 'z');
if (isDriveLetter && value[i + 1] == ':' && (value[i + 2] == '\\' || value[i + 2] == '/'))
{
return i;
}
}
return -1;
}
private static CompileResponse Compile(string requestPath)
{
string[] requestLines = File.ReadAllLines(requestPath);
string sourcePath = requestLines[0];
string dllPath = requestLines[1];
bool allowUnsafe = ParseAllowUnsafe(requestLines);
string[] defineSymbols = ParseDefineSymbols(requestLines);
string source = File.ReadAllText(sourcePath, Encoding.UTF8);
SourceText sourceText = SourceText.From(source, Encoding.UTF8);
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceText, CreateParseOptions(defineSymbols), sourcePath);
List<MetadataReference> references = BuildReferences(requestLines);
CSharpCompilation compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(dllPath), new[] { syntaxTree }, references, CreateCompilationOptions(allowUnsafe));
using (FileStream peStream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
EmitResult emitResult = compilation.Emit(peStream);
int exitCode = 0;
List<string> diagnosticLines = new List<string>();
foreach (Diagnostic diagnostic in emitResult.Diagnostics)
{
if (diagnostic.Severity != DiagnosticSeverity.Error && diagnostic.Severity != DiagnosticSeverity.Warning)
{
continue;
}
if (diagnostic.Severity == DiagnosticSeverity.Error)
{
exitCode = 1;
}
FileLinePositionSpan lineSpan = diagnostic.Location != Location.None ? diagnostic.Location.GetMappedLineSpan() : default(FileLinePositionSpan);
int line = lineSpan.StartLinePosition.Line + 1;
int column = lineSpan.StartLinePosition.Character + 1;
string file = string.IsNullOrEmpty(lineSpan.Path) ? sourcePath : lineSpan.Path;
string severity = diagnostic.Severity == DiagnosticSeverity.Warning ? "warning" : "error";
diagnosticLines.Add(file + "(" + line + "," + column + "): " + severity + " " + diagnostic.Id + ": " + diagnostic.GetMessage());
}
return new CompileResponse(exitCode, diagnosticLines);
}
}
private static CSharpParseOptions CreateParseOptions(string[] defineSymbols)
{
return defineSymbols == null || defineSymbols.Length == 0
? CSharpParseOptions.Default
: CSharpParseOptions.Default.WithPreprocessorSymbols(defineSymbols);
}
private static CSharpCompilationOptions CreateCompilationOptions(bool allowUnsafe)
{
return new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
allowUnsafe: allowUnsafe);
}
private static bool ParseAllowUnsafe(string[] requestLines)
{
for (int i = 2; i < requestLines.Length; i++)
{
string line = requestLines[i];
if (!line.StartsWith(UnsafePrefix, StringComparison.Ordinal))
{
continue;
}
return string.Equals(line.Substring(UnsafePrefix.Length), "1", StringComparison.Ordinal);
}
return false;
}
private static string[] ParseDefineSymbols(string[] requestLines)
{
for (int i = 2; i < requestLines.Length; i++)
{
string line = requestLines[i];
if (!line.StartsWith(DefinePrefix, StringComparison.Ordinal))
{
continue;
}
string serializedSymbols = line.Substring(DefinePrefix.Length);
if (string.IsNullOrWhiteSpace(serializedSymbols))
{
return new string[0];
}
return serializedSymbols.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
}
return new string[0];
}
private static List<MetadataReference> BuildReferences(string[] requestLines)
{
List<MetadataReference> references = new List<MetadataReference>(Math.Max(0, requestLines.Length - 2));
lock (SyncRoot)
{
for (int i = 2; i < requestLines.Length; i++)
{
string referencePath = requestLines[i];
if (referencePath.StartsWith(UnsafePrefix, StringComparison.Ordinal)
|| referencePath.StartsWith(DefinePrefix, StringComparison.Ordinal))
{
continue;
}
if (referencePath.StartsWith(ReferencePrefix, StringComparison.Ordinal))
{
referencePath = referencePath.Substring(ReferencePrefix.Length);
}
if (string.IsNullOrWhiteSpace(referencePath) || !File.Exists(referencePath))
{
continue;
}
FileInfo info = new FileInfo(referencePath);
long writeTicks = info.LastWriteTimeUtc.Ticks;
long fileLength = info.Length;
CachedReference cachedReference;
if (Cache.TryGetValue(referencePath, out cachedReference)
&& cachedReference.WriteTicks == writeTicks
&& cachedReference.FileLength == fileLength)
{
references.Add(cachedReference.Reference);
continue;
}
PortableExecutableReference createdReference = MetadataReference.CreateFromFile(referencePath);
Cache[referencePath] = new CachedReference(createdReference, writeTicks, fileLength);
references.Add(createdReference);
}
}
return references;
}
private sealed class CachedReference
{
public CachedReference(PortableExecutableReference reference, long writeTicks, long fileLength)
{
Reference = reference;
WriteTicks = writeTicks;
FileLength = fileLength;
}
public PortableExecutableReference Reference { get; }
public long WriteTicks { get; }
public long FileLength { get; }
}
private sealed class CompileResponse
{
public CompileResponse(int exitCode, List<string> diagnosticLines)
{
ExitCode = exitCode;
DiagnosticLines = diagnosticLines;
}
public int ExitCode { get; }
public List<string> DiagnosticLines { get; }
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a0645dfa092114f4e87246f7d1da5e0c
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Detects whether top-level code contains a return statement.
/// Excludes returns inside lambdas and local functions by tracking brace depth.
/// Delegates literal/comment skipping to SourceShaper.AdvanceOneToken to avoid duplication.
/// </summary>
internal static class TopLevelReturnDetector
{
public static bool HasTopLevelReturn(string body)
{
if (string.IsNullOrWhiteSpace(body)) return false;
int length = body.Length;
int pos = 0;
int braceDepth = 0;
while (pos < length)
{
char c = body[pos];
if (c == '{') { braceDepth++; pos++; continue; }
if (c == '}') { braceDepth--; pos++; continue; }
if (braceDepth == 0 && c == 'r' && StartsWithReturn(body, pos, length))
{
return true;
}
// Delegate all literal/comment skipping to shared scanner
int next = SourceShaper.AdvanceOneTokenPublic(body, pos);
if (next == pos) { pos++; continue; }
pos = next;
}
return false;
}
private static bool StartsWithReturn(string s, int pos, int length)
{
// "return" must not be part of a longer identifier (e.g. "myreturn", "returnValue")
if (pos > 0 && (char.IsLetterOrDigit(s[pos - 1]) || s[pos - 1] == '_'))
{
return false;
}
if (pos + 6 > length) return false;
if (s[pos + 1] != 'e' || s[pos + 2] != 't' || s[pos + 3] != 'u' ||
s[pos + 4] != 'r' || s[pos + 5] != 'n')
{
return false;
}
int afterReturn = pos + 6;
if (afterReturn < length && (char.IsLetterOrDigit(s[afterReturn]) || s[afterReturn] == '_'))
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45f8daddb6cae422d9c819746fbf1e46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Text;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Builds the namespace/class/method wrapper for top-level user code.
/// Method signatures are compatible with CommandRunner's FindExecuteAsyncMethod.
/// </summary>
internal static class WrapperTemplate
{
internal const string UserCodeStartMarker = "#line 1 \"user-snippet.cs\"";
internal const string UserCodeEndMarker = "#line default";
public static string Build(
IReadOnlyList<string> usingDirectives,
string namespaceName,
string className,
string body,
IReadOnlyList<string> preambleLines = null)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("#pragma warning disable CS0162");
sb.AppendLine("#pragma warning disable CS1998");
sb.AppendLine("using System;");
sb.AppendLine("using System.Linq;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using UnityEngine;");
sb.AppendLine("using UnityEditor;");
foreach (string directive in usingDirectives)
{
sb.AppendLine(directive);
}
sb.AppendLine();
sb.AppendLine($"namespace {namespaceName}");
sb.AppendLine("{");
sb.AppendLine($" public class {className}");
sb.AppendLine(" {");
sb.AppendLine(" public async System.Threading.Tasks.Task<object> ExecuteAsync(");
sb.AppendLine(" System.Collections.Generic.Dictionary<string, object> parameters = null,");
sb.AppendLine(" System.Threading.CancellationToken ct = default)");
sb.AppendLine(" {");
if (preambleLines != null)
{
foreach (string preambleLine in preambleLines)
{
sb.AppendLine($" {preambleLine}");
}
}
sb.AppendLine(UserCodeStartMarker);
foreach (string line in body.Split('\n'))
{
sb.AppendLine($" {line.TrimEnd('\r')}");
}
sb.AppendLine(UserCodeEndMarker);
sb.AppendLine("#line hidden");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" public object Execute(");
sb.AppendLine(" System.Collections.Generic.Dictionary<string, object> parameters = null)");
sb.AppendLine(" {");
sb.AppendLine(" return ExecuteAsync(parameters, default).GetAwaiter().GetResult();");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0e9c4bb3166948f7a6879b104383c33
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: