c# 10 and assembly unloading for in-game scripts
This commit is contained in:
231
Torch/Managers/ScriptCompilationManager.cs
Normal file
231
Torch/Managers/ScriptCompilationManager.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Sandbox.Game.Entities.Blocks;
|
||||
using Sandbox.Game.EntityComponents;
|
||||
using Sandbox.ModAPI;
|
||||
using Sandbox.ModAPI.Ingame;
|
||||
using Torch.API;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.ModAPI;
|
||||
using VRage.Scripting;
|
||||
|
||||
namespace Torch.Managers;
|
||||
|
||||
public class ScriptCompilationManager : Manager
|
||||
{
|
||||
[ReflectedSetter(Name = "m_terminationReason")]
|
||||
private static Action<MyProgrammableBlock, MyProgrammableBlock.ScriptTerminationReason> TerminationReasonSetter = null!;
|
||||
|
||||
[ReflectedGetter(Name = "ScriptComponent")]
|
||||
private static Func<MyProgrammableBlock, MyIngameScriptComponent> ScriptComponentGetter = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Action<MyProgrammableBlock, string> SetDetailedInfo = null!;
|
||||
|
||||
[ReflectedSetter(Name = "m_instance")]
|
||||
private static Action<MyProgrammableBlock, IMyGridProgram> InstanceSetter = null!;
|
||||
|
||||
[ReflectedSetter(Name = "m_assembly")]
|
||||
private static Action<MyProgrammableBlock, Assembly> AssemblySetter = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyProgrammableBlock, Assembly, IEnumerable<string>, string, bool> CreateInstance = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_compilerErrors")]
|
||||
private static Func<MyProgrammableBlock, List<string>> CompilerErrorsGetter = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_modApiWhitelistDiagnosticAnalyzer")]
|
||||
private static Func<MyScriptCompiler, DiagnosticAnalyzer> ModWhitelistAnalyzer = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_ingameWhitelistDiagnosticAnalyzer")]
|
||||
private static Func<MyScriptCompiler, DiagnosticAnalyzer> ScriptWhitelistAnalyzer = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, CSharpCompilation, SyntaxTree, int, SyntaxTree> InjectMod = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, CSharpCompilation, SyntaxTree, SyntaxTree> InjectInstructionCounter = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, CompilationWithAnalyzers, EmitResult, List<Message>, bool, Task<bool>> EmitDiagnostics = null!;
|
||||
|
||||
[ReflectedMethod]
|
||||
private static Func<MyScriptCompiler, MyApiTarget, string, IList<SyntaxTree>, string, Task> WriteDiagnostics = null!;
|
||||
|
||||
[ReflectedMethod(Name = "WriteDiagnostics")]
|
||||
private static Func<MyScriptCompiler, MyApiTarget, string, IEnumerable<Message>, bool, Task> WriteDiagnostics2 = null!;
|
||||
|
||||
[ReflectedGetter(Name = "m_metadataReferences")]
|
||||
private static Func<MyScriptCompiler, List<MetadataReference>> MetadataReferencesGetter = null!;
|
||||
|
||||
private readonly ConditionalWeakTable<MyProgrammableBlock, AssemblyLoadContext> _contexts = new();
|
||||
|
||||
public ScriptCompilationManager(ITorchBase torchInstance) : base(torchInstance)
|
||||
{
|
||||
}
|
||||
|
||||
public async void CompileAsync(MyProgrammableBlock block, string program, string storage, bool instantiate)
|
||||
{
|
||||
TerminationReasonSetter(block, MyProgrammableBlock.ScriptTerminationReason.None);
|
||||
|
||||
var component = ScriptComponentGetter(block);
|
||||
component.NextUpdate = UpdateType.None;
|
||||
component.NeedsUpdate = MyEntityUpdateEnum.NONE;
|
||||
|
||||
try
|
||||
{
|
||||
if (_contexts.TryGetValue(block, out var context))
|
||||
{
|
||||
InstanceSetter(block, null);
|
||||
AssemblySetter(block, null);
|
||||
context!.Unload();
|
||||
}
|
||||
|
||||
_contexts.AddOrUpdate(block, context = new AssemblyLoadContext(null, true));
|
||||
|
||||
var messages = new List<Message>();
|
||||
var assembly = await CompileAsync(context, MyApiTarget.Ingame,
|
||||
$"pb_{block.EntityId}_{Random.Shared.NextInt64()}",
|
||||
new[]
|
||||
{
|
||||
MyVRage.Platform.Scripting.GetIngameScript(
|
||||
program, "Program", nameof(MyGridProgram))
|
||||
}, messages, $"PB: {block.DisplayName} ({block.EntityId})");
|
||||
AssemblySetter(block, assembly);
|
||||
var errors = CompilerErrorsGetter(block);
|
||||
|
||||
errors.Clear();
|
||||
errors.AddRange(messages.Select(b => b.Text));
|
||||
|
||||
if (instantiate)
|
||||
await Torch.InvokeAsync(() => CreateInstance(block, assembly, errors, storage));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await Torch.InvokeAsync(() => SetDetailedInfo(block, e.ToString()));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Assembly> CompileAsync(AssemblyLoadContext context, MyApiTarget target, string assemblyName, IEnumerable<Script> scripts, List<Message> messages, string friendlyName, bool enableDebugInformation = false)
|
||||
{
|
||||
friendlyName ??= "<No Name>";
|
||||
Func<CSharpCompilation, SyntaxTree, SyntaxTree> syntaxTreeInjector;
|
||||
DiagnosticAnalyzer whitelistAnalyzer;
|
||||
switch (target)
|
||||
{
|
||||
case MyApiTarget.None:
|
||||
whitelistAnalyzer = null;
|
||||
syntaxTreeInjector = null;
|
||||
break;
|
||||
case MyApiTarget.Mod:
|
||||
{
|
||||
var modId = MyModWatchdog.AllocateModId(friendlyName);
|
||||
whitelistAnalyzer = ModWhitelistAnalyzer(MyScriptCompiler.Static);
|
||||
syntaxTreeInjector = (c, st) => InjectMod(MyScriptCompiler.Static, c, st, modId);
|
||||
break;
|
||||
}
|
||||
case MyApiTarget.Ingame:
|
||||
syntaxTreeInjector = (c, t) => InjectInstructionCounter(MyScriptCompiler.Static, c, t);
|
||||
whitelistAnalyzer = ScriptWhitelistAnalyzer(MyScriptCompiler.Static);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(target), target, "Invalid compilation target");
|
||||
}
|
||||
var compilation = CreateCompilation(assemblyName, scripts);
|
||||
|
||||
await WriteDiagnostics(MyScriptCompiler.Static, target, assemblyName, compilation.SyntaxTrees, null).ConfigureAwait(false);
|
||||
var injectionFailed = false;
|
||||
var compilationWithoutInjection = compilation;
|
||||
if (syntaxTreeInjector != null)
|
||||
{
|
||||
SyntaxTree[] newSyntaxTrees = null;
|
||||
try
|
||||
{
|
||||
var syntaxTrees = compilation.SyntaxTrees;
|
||||
if (syntaxTrees.Length == 1)
|
||||
{
|
||||
newSyntaxTrees = new[] { syntaxTreeInjector(compilation, syntaxTrees[0]) };
|
||||
}
|
||||
else
|
||||
{
|
||||
newSyntaxTrees = await Task
|
||||
.WhenAll(syntaxTrees.Select(
|
||||
x => Task.Run(() => syntaxTreeInjector(compilation, x))))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
injectionFailed = true;
|
||||
}
|
||||
if (!injectionFailed)
|
||||
{
|
||||
await WriteDiagnostics(MyScriptCompiler.Static, target, assemblyName, newSyntaxTrees, ".injected").ConfigureAwait(false);
|
||||
compilation = compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(newSyntaxTrees);
|
||||
}
|
||||
}
|
||||
CompilationWithAnalyzers analyticCompilation = null;
|
||||
if (whitelistAnalyzer != null)
|
||||
{
|
||||
analyticCompilation = compilation.WithAnalyzers(ImmutableArray.Create(whitelistAnalyzer));
|
||||
compilation = (CSharpCompilation)analyticCompilation.Compilation;
|
||||
}
|
||||
|
||||
using var assemblyStream = new MemoryStream();
|
||||
|
||||
var emitResult = compilation.Emit(assemblyStream);
|
||||
var success = emitResult.Success;
|
||||
var myBlacklistSyntaxVisitor = new MyBlacklistSyntaxVisitor();
|
||||
foreach (var syntaxTree in compilation.SyntaxTrees)
|
||||
{
|
||||
myBlacklistSyntaxVisitor.SetSemanticModel(compilation.GetSemanticModel(syntaxTree, false));
|
||||
myBlacklistSyntaxVisitor.Visit(await syntaxTree.GetRootAsync());
|
||||
}
|
||||
if (myBlacklistSyntaxVisitor.HasAnyResult())
|
||||
{
|
||||
myBlacklistSyntaxVisitor.GetResultMessages(messages);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = await EmitDiagnostics(MyScriptCompiler.Static, analyticCompilation, emitResult, messages, success).ConfigureAwait(false);
|
||||
await WriteDiagnostics2(MyScriptCompiler.Static, target, assemblyName, messages, success).ConfigureAwait(false);
|
||||
assemblyStream.Seek(0, SeekOrigin.Begin);
|
||||
if (injectionFailed) return null;
|
||||
if (success)
|
||||
return context.LoadFromStream(assemblyStream);
|
||||
|
||||
await EmitDiagnostics(MyScriptCompiler.Static, analyticCompilation, compilationWithoutInjection.Emit(assemblyStream), messages, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private readonly CSharpCompilationOptions _compilationOptions = new(OutputKind.DynamicallyLinkedLibrary);
|
||||
private readonly CSharpParseOptions _parseOptions = new(LanguageVersion.CSharp10, DocumentationMode.None);
|
||||
|
||||
private CSharpCompilation CreateCompilation(string assemblyFileName, IEnumerable<Script> scripts)
|
||||
{
|
||||
if (scripts == null)
|
||||
return CSharpCompilation.Create(assemblyFileName, null, MetadataReferencesGetter(MyScriptCompiler.Static),
|
||||
_compilationOptions);
|
||||
|
||||
var parseOptions = _parseOptions.WithPreprocessorSymbols(MyScriptCompiler.Static.ConditionalCompilationSymbols);
|
||||
var enumerable = scripts.Select(s => CSharpSyntaxTree.ParseText(s.Code, parseOptions, s.Name, Encoding.UTF8));
|
||||
|
||||
return CSharpCompilation.Create(assemblyFileName, enumerable, MetadataReferencesGetter(MyScriptCompiler.Static), _compilationOptions);
|
||||
}
|
||||
}
|
33
Torch/Patches/ProgramableBlockPatch.cs
Normal file
33
Torch/Patches/ProgramableBlockPatch.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Sandbox.Game.Entities.Blocks;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Managers;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Patches;
|
||||
|
||||
[PatchShim]
|
||||
public static class ProgramableBlockPatch
|
||||
{
|
||||
[ReflectedMethodInfo(typeof(MyProgrammableBlock), "Compile")]
|
||||
private static MethodInfo CompileMethod = null!;
|
||||
|
||||
public static void Patch(PatchContext context)
|
||||
{
|
||||
context.GetPattern(CompileMethod).AddPrefix();
|
||||
}
|
||||
|
||||
private static bool Prefix(MyProgrammableBlock __instance, string program, string storage, bool instantiate)
|
||||
{
|
||||
if (!MySession.Static.EnableIngameScripts || __instance.CubeGrid is {IsPreview: true} or {CreatePhysics: false})
|
||||
return false;
|
||||
|
||||
#pragma warning disable CS0618
|
||||
TorchBase.Instance.CurrentSession.Managers.GetManager<ScriptCompilationManager>().CompileAsync(__instance, program, storage, instantiate);
|
||||
#pragma warning restore CS0618
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -124,6 +124,7 @@ namespace Torch
|
||||
sessionManager.AddFactory((x) => Sync.IsServer ? new ChatManagerServer(this) : new ChatManagerClient(this));
|
||||
sessionManager.AddFactory((x) => Sync.IsServer ? new CommandManager(this) : null);
|
||||
sessionManager.AddFactory((x) => new EntityManager(this));
|
||||
sessionManager.AddFactory((x) => new ScriptCompilationManager(this));
|
||||
|
||||
Managers.AddManager(sessionManager);
|
||||
Managers.AddManager(new PatchManager(this));
|
||||
|
Reference in New Issue
Block a user