Перекомпилируйте C # во время работы без доменов приложений - PullRequest
20 голосов
/ 30 августа 2009

Допустим, у меня есть два приложения на C # - game.exe (XNA, требуется поддержка Xbox 360) и editor.exe (XNA, размещенный в WinForms) - они оба используют сборку engine.dll, которая выполняет подавляющее большинство работа.

Теперь допустим, что я хочу добавить какой-нибудь сценарий на основе C # (это не совсем "сценарий", но я назову это так). Каждый уровень получает свой собственный класс, унаследованный от базового класса (назовем его LevelController).

Вот важные ограничения для этих скриптов:

  1. Они должны быть реальными, скомпилированный код C #

  2. Они должны требовать минимальной ручной «клеевой» работы, если таковые имеются

  3. Они должны работать в том же домене приложений, что и все остальное

Для игры - это довольно просто: все классы сценариев могут быть скомпилированы в сборку (скажем, levels.dll), а отдельные классы могут быть созданы с использованием отражения по мере необходимости.

Редактор намного сложнее. Редактор имеет возможность «играть в игру» в окне редактора, а затем сбрасывать все обратно туда, где он был запущен (именно поэтому редактор должен знать об этих сценариях в первую очередь).

То, чего я пытаюсь добиться, это в основном кнопка «перезагрузить скрипт» в редакторе, которая перекомпилирует и загрузит класс скрипта, связанный с редактируемым уровнем, и, когда пользователь нажмет кнопку «играть», создаст экземпляр последний скомпилированный скрипт.

Результатом будет быстрый рабочий процесс редактирования-тестирования в редакторе (вместо альтернативы - сохранения уровня, закрытия редактора, перекомпиляции решения, запуска редактора, загрузки уровня, тестирования).


Теперь я думаю Я разработал потенциальный способ достижения этого - что само по себе приводит к ряду вопросов (приведенных ниже):

  1. Скомпилируйте коллекцию файлов .cs, необходимых для данного уровня (или, если необходимо, всего проекта levels.dll), во временную сборку с уникальным именем. Эта сборка должна ссылаться на engine.dll. Как вызвать компилятор таким образом во время выполнения? Как заставить его выводить такую ​​сборку (и можно ли это сделать в памяти)?

  2. Загрузите новую сборку. Будет ли иметь значение, что я загружаю классы с одинаковыми именами в один и тот же процесс? (У меня сложилось впечатление, что имена уточняются по имени сборки?)

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

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

И наконец:

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


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

Ответы [ 4 ]

4 голосов
/ 27 мая 2013

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

3 голосов
/ 30 августа 2009

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

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

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

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

Совсем нет. Это просто имена.

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

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

1 голос
/ 30 августа 2009

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

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

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

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

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

0 голосов
/ 02 мая 2015

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

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

...