c# - 在运行时重新编译 C#,没有 AppDomains

标签 c# compilation assembly.load

假设我有两个 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解决根本问题。

最佳答案

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

关于c# - 在运行时重新编译 C#,没有 AppDomains,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1353456/

相关文章:

java - JRE 是否比带有普通 Java 解释器的 JRE 快很多?

c# - 如何提供后备程序集而不是无法加载的程序集?

.net - 使用 Assembly.LoadFrom 和 Assembly.Load 混合加载程序集时的奇怪行为

c# - 以编程方式和从命令行 (DTEXEC) 运行 SSIS 包时遇到问题

c# - 多线程存储过程可以访问彼此的临时表吗?

c# - 如何在c#中加密和解密pdf文件?

c# - 我如何在基于 Android-Java 的应用程序中使用 DataTable。

c - 如果我排除库和编译器扩展,C 中还剩下什么?

compilation - 如何将多个 lisp-fasl 文件链接到一个文件中?