在没有AppDomains的情况下运行时重新编译C#

假设我有两个C#应用程序 – game.exe (XNA,需要支持Xbox 360)和editor.exe (在WinForms中托管的XNA) – 它们共享一个engine.dll程序集,完成绝大部分工作。

现在让我们说我想添加某种基于C#的脚本(它不是“脚本”,但我会称之为)。 每个级别都有自己inheritance自基类的类(我们称之为LevelController )。

这些是这些脚本的重要约束:

  1. 它们需要是真实的,编译好的C#代码

  2. 他们应该需要最少的手动“粘合”工作,如果有的话

  3. 它们必须与其他所有内容在同一AppDomain中运行

对于游戏 – 这非常简单:所有脚本类都可以编译成程序集(比如, levels.dll ),并且可以根据需要使用reflection对各个类进行实例化。

编辑要困难得多。 编辑器能够在编辑器窗口中“玩游戏”,然后将所有内容重置回原来的位置(这就是编辑器首先需要了解这些脚本的原因)。

我想要实现的基本上是编辑器中的“重新加载脚本”按钮,它将重新编译并加载与正在编辑的级别相关联的脚本类,并且当用户按下“播放”按钮时,创建最近的实例编译脚本。

其结果将是编辑器中的快速编辑测试工作流程(而不是替代方案 – 保存级别,关闭编辑器,重新编译解决方案,启动编辑器,加载级别,测试)。


现在我我已经找到了实现这一目标的潜在方法 – 这本身就会产生一些问题(如下所示):

  1. 将给定级别(或者,如果需要,整个levels.dll项目)所需的.cs文件集合编译为临时的,唯一命名的程序集。 该程序集将需要引用engine.dll 。 如何在运行时以这种方式调用编译器? 如何让它输出这样的程序集(我可以在内存中执行)吗?

  2. 加载新assembly。 我在同一个进程中加载​​具有相同名称的类是否重要? (我的印象是名称是由程序集名称限定的?)

    现在,正如我所提到的,我无法使用AppDomains。 但是,另一方面,我不介意泄漏旧版本的脚本类,因此卸载的能力并不重要。 除非是吗? 我假设加载可能几百个组件是可行的。

  3. 在播放关卡时,实例是从刚刚加载的特定程序集inheritance自LevelController的类。 这该怎么做?

最后:

这是一种明智的做法吗? 它可以做得更好吗?


更新:这些天我使用一种更简单的方法来解决潜在的问题。

查看Microsoft.CSharp.CSharpCodeProvider和System.CodeDom.Compiler周围的命名空间。

编译.cs文件的集合

应该像http://support.microsoft.com/kb/304655一样非常简单

我在同一个进程中加载​​具有相同名称的类是否重要?

一点也不。 这只是名字。

实例从LevelControllerinheritance的类。

加载您创建的程序集,如Assembly.Load等。使用reflection查询要实例化的类型。 获取构造函数并调用它。

现在有了一个相当优雅的解决方案,可以通过(a).NET 4.0中的新function和(b)Roslyn实现。

collections集会

在.NET 4.0中,您可以在定义动态程序集时指定AssemblyBuilderAccess.RunAndCollect ,这会使动态程序集垃圾变为可收集:

 AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("Foo"), AssemblyBuilderAccess.RunAndCollect); 

使用vanilla .NET 4.0,我认为您需要通过在原始IL中编写方法来填充动态程序集。

罗斯林

输入Roslyn:Roslyn允许您将原始C#代码编译为动态程序集。 这是一个例子,受这两篇博客 文章的启发,更新后可与最新的Roslyn二进制文件一起使用:

 using System; using System.Reflection; using System.Reflection.Emit; using Roslyn.Compilers; using Roslyn.Compilers.CSharp; namespace ConsoleApplication1 { public static class Program { private static Type CreateType() { SyntaxTree tree = SyntaxTree.ParseText( @"using System; namespace Foo { public class Bar { public static void Test() { Console.WriteLine(""Hello World!""); } } }"); var compilation = Compilation.Create("Hello") .WithOptions(new CompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddReferences(MetadataReference.CreateAssemblyReference("mscorlib")) .AddSyntaxTrees(tree); ModuleBuilder helloModuleBuilder = AppDomain.CurrentDomain .DefineDynamicAssembly(new AssemblyName("FooAssembly"), AssemblyBuilderAccess.RunAndCollect) .DefineDynamicModule("FooModule"); var result = compilation.Emit(helloModuleBuilder); return helloModuleBuilder.GetType("Foo.Bar"); } static void Main(string[] args) { Type fooType = CreateType(); MethodInfo testMethod = fooType.GetMethod("Test"); testMethod.Invoke(null, null); WeakReference weak = new WeakReference(fooType); fooType = null; testMethod = null; Console.WriteLine("type = " + weak.Target); GC.Collect(); Console.WriteLine("type = " + weak.Target); Console.ReadKey(); } } } 

总结:使用可收集的程序集和Roslyn,您可以将C#代码编译为可以加载到AppDomain的程序集,然后进行垃圾回收(受许多规则约束 )。

好吧,你希望能够动态编辑,对吧? 那你的目标不是吗?

当您编译程序集并加载它们时,除非您卸载AppDomain,否则现在可​​以卸载它们。

您可以使用Assembly.Load方法加载预编译的程序集,然后通过reflection调用入口点。

我会考虑动态assembly方法。 您通过当前的AppDomain说您要创建动态程序集。 这就是DLR(动态语言运行时)的工作方式。 使用动态程序集,您可以创建实现某些可见接口的类型,并通过它来调用它们。 使用动态程序集的背面是你必须自己提供正确的IL,你不能简单地使用内置的.NET编译器生成它,但是,我敢打赌Mono项目有一个C#编译器实现你可能想看看。 他们已经有一个C#解释器,它读入一个C#源文件并编译并执行它,这肯定是通过System.Reflection.Emit API处理的。

我不确定这里的垃圾收集,因为当谈到动态类型时,我认为运行时不会释放它们,因为它们可以随时引用。 只有当动态程序集本身被销毁并且该程序集不存在引用时才释放该内存是合理的。 如果您正在重新生成大量代码,请确保GC在某些时候收集内存。

如果语言是Java,那么答案就是使用JRebel。 既然不是,答案就是提高噪音,以表明对此的需求。 它可能需要某种备用CLR或“c#引擎项目模板”和VS IDE协同工作等。

我怀疑有很多情况下这是“必须有的”,但有很多情况会节省大量的时间,因为你可以通过减少基础设施和更快的周转时间而不会长时间使用的东西。 (是的,有些人争论过度工程,因为他们将被使用20多年,但问题是当你需要做一些大规模的改变时,它可能会像从头开始重建整个事情一样昂贵。因此,无论是现在还是以后都要花钱。由于不确定该项目是否会在以后成为关键业务,以后可能需要进行大量更改,我的论点是使用“KISS”原则并具有复杂性在IDE,CLR /运行时等中进行实时编辑,而不是将其构建到以后可能有用的每个应用程序中。当然,使用这种function修改一些实时服务需要一些防御性编程和实践。据说开发者正在做)