20

假设我有两个 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 的类。这该怎么做?

最后:

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


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

4

4 回答 4

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# 代码编译成动态程序集。这是一个示例,受这两篇博客 文章的启发,已更新以使用最新的 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然后进行垃圾收集(遵守许多规则)。

于 2013-05-27T10:14:21.977 回答
3

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

编译 .cs 文件的集合

应该非常简单,例如http://support.microsoft.com/kb/304655

将具有相同名称的类加载到同一进程中是否重要?

一点也不。这只是名字。

实例化从 LevelController 继承的类。

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

于 2009-08-30T12:19:31.330 回答
1

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

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

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

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

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

于 2009-08-30T09:22:46.927 回答
0

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

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

于 2015-05-02T05:24:31.423 回答