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

时间:2009-08-30 08:50:52

标签: c# compilation assembly.load

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

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

这些是这些脚本的重要限制因素:

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

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

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

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

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

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

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


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

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

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

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

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

    4. 最后:

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


      更新:这些天我使用far simpler approach来解决潜在的问题。

4 个答案:

答案 0 :(得分:4)

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

收藏品装配

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

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

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

罗斯林

输入Roslyn:Roslyn允许您将原始C#代码编译为动态程序集。这是一个例子,受这两个blog posts的启发,更新后可以使用最新的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的程序集,然后进行垃圾回收(受rules个数量限制。)< / p>

答案 1 :(得分:3)

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

  
    

编译.cs文件的集合

  

应该像http://support.microsoft.com/kb/304655

一样非常简单
  
    

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

  

完全没有。这只是名字。

  
    

实例从LevelController继承的类。

  

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

答案 2 :(得分:1)

嗯,你希望能够即时编辑,对吧?那你的目标不是吗?

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

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

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

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

答案 3 :(得分:0)

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

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