【问题标题】:Recompile C# while running, without AppDomains运行时重新编译 C#,不使用 AppDomains
【发布时间】:2010-11-24 02:50:33
【问题描述】:

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

现在假设我想添加某种基于 C# 的脚本(它不完全是“脚本”,但我会这么称呼它)。每个级别都有自己的类继承自基类(我们称之为LevelController)。

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

  1. 它们必须是真实的、经过编译的 C# 代码

  2. 他们应该需要最少的手动“胶水”工作(如果有的话)

  3. 它们必须和其他所有东西在同一个 AppDomain 中运行

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

编辑器更难。编辑器可以在编辑器窗口中“玩游戏”,然后将所有内容重置回它开始的位置(这就是编辑器首先需要了解这些脚本的原因)。

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

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


现在我认为我已经找到了一种可能的方法来实现这一点 - 这本身会导致一些问题(如下所示):

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

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

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

  3. 播放关卡时,实例化从刚刚加载的特定程序集的 LevelController 继承的类。如何做到这一点?

最后:

这是一种明智的做法吗?能不能做得更好?


更新:这些天我使用far simpler approach 来解决根本问题。

【问题讨论】:

  • 为什么必须避免使用 AppDomains?
  • “脚本”需要操作必须在默认 AppDomain 中的对象(因为 XNA)。性能也是一个问题(至少对于 game.exe)。
  • 您无法从 AppDomain 中卸载程序集,因此您可能最终会将大量“临时”程序集加载到您的 AppDomain 中。这可能会变得非常混乱,因为命名空间/类名可能是相同的!
  • @DanTup 并非如此 - 类由程序集和名称标识。只要您不意外地保留对“旧”程序集的类的引用,在您开始使用“新”程序集之后,这并不是真正的问题。
  • Russel - 这当然是可能的,只是可能“令人困惑”。某些错误(例如 InvalidCastExceptions)可能不会显示程序集名称,因此您最终会遇到有趣的“无法将 Namespace1.Class1 转换为 Namespace1.Class1”错误。当然有可能!

标签: c# compilation assembly.load


【解决方案1】:

嗯,您希望能够即时编辑内容,对吧?这就是你的目标不是吗?

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

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

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

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

【讨论】:

    【解决方案2】:

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

    编译 .cs 文件的集合

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

    将同名的类加载到同一个进程中是否重要?

    一点也不。只是名字。

    实例化继承自 LevelController 的类。

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

    【讨论】:

    • 我说得对吗,为了实例化类型,在调用 CodeDomProvider.CompileAssemblyFromFile 之后,我会有类似的东西:compilerResults.CompiledAssembly.GetType("MyLevelController").GetConstructor(...) .Invoke(...);
    • 是的,一种方法是:Assembly generatedAssembly = Assembly.Load(fromMemory);或程序集生成Assembly = Assembly.LoadFrom(assemblyFile); Type[] types = generatedAssembly.GetTypes(); object instance = types[0].GetConstructor(new Type[0]).Invoke(new object[0]);
    • 快捷方式可能是“generatedAssembly.CreateInstance(typeName)
    • CodeDom 确实是一个已弃用的 API,避免使用它并非没有限制。
    • 好的,这是个有趣的消息。但是,什么是合适的、同样复杂的方法呢?我的意思是你在回答中写的对我来说听起来要复杂得多。
    【解决方案3】:

    现在有一个相当优雅的解决方案,通过 (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 为限)。

    【讨论】:

    • 以防万一其他人对 Tim 的回答感到兴奋(就像我一样),请参阅 Tomas 的回答 here。截至目前,Roslyn CTP 已删除对这种可收集程序集创建方法的支持。
    • @Jasper 哦,真可惜!我从对您链接到的答案的评论中看到,有一个 UserVoice 建议重新添加该功能:visualstudio.uservoice.com/forums/121579-visual-studio/…
    【解决方案4】:

    如果语言是 Java,答案是使用 JRebel。既然不是,答案是发出足够的噪音来表明对此有需求。它可能需要某种替代 CLR 或“c# 引擎项目模板”和 VS IDE 协同工作等。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多