Перекомпилируйте C# во время работы без доменов приложений

Допустим, у меня есть два приложения на 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. Загрузите новую сборку. Будет ли иметь значение, что я загружаю классы с одинаковыми именами в один и тот же процесс? (У меня сложилось впечатление, что имена уточняются по имени сборки?)

    Теперь, как я уже говорил, я не могу использовать домены приложений. Но, с другой стороны, я не против утечки старых версий классов сценариев, поэтому возможность выгрузки не важна. Если это не так? Я предполагаю, что возможно загрузка нескольких сотен сборок.

  3. При воспроизведении уровня, экземпляр класса, который унаследован от LevelController от конкретной сборки, которая была только что загружена. Как это сделать?

И наконец:

Это разумный подход? Можно ли сделать это лучше?


ОБНОВЛЕНИЕ: В настоящее время я использую гораздо более простой подход для решения основной проблемы.

4 ответа

Решение

Проверьте пространства имен вокруг Microsoft.CSharp.CSharpCodeProvider и System.CodeDom.Compiler.

Скомпилируйте коллекцию файлов.cs

Должно быть довольно просто, как http://support.microsoft.com/kb/304655

Будет ли иметь значение, что я загружаю классы с одинаковыми именами в один и тот же процесс?

Не за что. Это просто имена.

экземпляр класса, который унаследован от LevelController.

Загрузите созданную вами сборку, например, Assembly.Load и т. Д. Запросите тип, который хотите создать, используя отражение. Получить конструктор и вызвать его.

В настоящее время существует довольно элегантное решение, которое стало возможным благодаря (а) новой функции в.NET 4.0 и (б) 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, а затем сборщик мусора (с учетом ряда правил).

Ну, вы хотите, чтобы иметь возможность редактировать вещи на лету, верно? это твоя цель, не так ли?

Когда вы компилируете сборки и загружаете их, теперь есть способ выгрузить их, если вы не выгрузите свой домен приложений.

Вы можете загрузить предварительно скомпилированные сборки с помощью метода Assembly.Load, а затем вызвать точку входа через отражение.

Я бы рассмотрел подход динамической сборки. Где вы через свой текущий AppDomain говорите, что хотите создать динамическую сборку. Вот как работает DLR (динамический язык исполнения). С помощью динамических сборок вы можете создавать типы, которые реализуют некоторый видимый интерфейс, и вызывать их через него. Обратная сторона работы с динамическими сборками заключается в том, что вы должны сами предоставить правильный IL, вы не можете просто сгенерировать его с помощью встроенного компилятора.NET, однако, я уверен, что в проекте Mono есть реализация компилятора C#, которую вы, возможно, захотите проверить., У них уже есть интерпретатор C#, который читает исходный файл C#, компилирует его и выполняет, и это определенно обрабатывается через API System.Reflection.Emit.

Я не уверен насчет сборки мусора здесь, потому что когда дело доходит до динамических типов, я думаю, что среда выполнения не освобождает их, потому что на них можно ссылаться в любое время. Было бы разумно освободить эту память только в том случае, если сама динамическая сборка будет уничтожена и ссылки на эту сборку отсутствуют. Если вы создаете много кода, убедитесь, что память в какой-то момент собрана GC.

Если бы язык был Java, ответом было бы использовать JRebel. Поскольку это не так, ответ состоит в том, чтобы поднять достаточно шума, чтобы показать, что есть спрос на это. Это может потребовать какого-то альтернативного CLR или "шаблона проекта движка C#" и VS IDE, работающих в координации и т. Д.

Я сомневаюсь, что есть много сценариев, где это "должно быть", но есть много, в которых это сэкономило бы много времени, поскольку вы могли бы уйти с меньшими затратами на инфраструктуру и более быстрым оборотом для вещей, которые не будут использоваться долго. (Да, есть люди, которые спорят о чрезмерной инженерии вещей, потому что они будут использоваться более 20 лет, но проблема с этим в том, что когда вам нужно сделать какие-то масштабные изменения, это, вероятно, будет столь же дорого, как и восстановление всей вещи с нуля. Таким образом, все сводится к тому, стоит ли тратить деньги сейчас или позже.Так как точно не известно, что проект станет критически важным для бизнеса позднее и, в любом случае, может потребовать больших изменений позже, аргумент для меня заключается в использовании принципа "KISS" и сложности. редактирование в реальном времени в IDE,CLR/ время выполнения и т. д. вместо того, чтобы встраивать его в каждое приложение, где оно может быть полезно позже. Конечно, для модификации некоторых живых сервисов с помощью такого рода функций потребуется некоторое защитное программирование и практики. Как Эрланг говорят, что разработчики делают)

Другие вопросы по тегам