Перекомпилируйте C# во время работы без доменов приложений
Допустим, у меня есть два приложения на C# - game.exe
(XNA, должен поддерживать Xbox 360) и editor.exe
(XNA размещается в WinForms) - они оба разделяют engine.dll
сборка, которая делает подавляющее большинство работы.
Теперь давайте скажем, что я хочу добавить какой-нибудь сценарий на основе C# (это не совсем "сценарий", но я назову это так). Каждый уровень получает свой собственный класс, унаследованный от базового класса (назовем его LevelController
).
Вот важные ограничения для этих сценариев:
Они должны быть реальными, скомпилированными на C#
Они должны требовать минимальной ручной "клеевой" работы, если таковые имеются
Они должны работать в том же AppDomain, что и все остальное
Для игры - это довольно просто: все классы сценариев могут быть скомпилированы в сборку (скажем, levels.dll
) и отдельные классы могут быть созданы с использованием отражения по мере необходимости.
Редактор намного сложнее. Редактор имеет возможность "играть в игру" в окне редактора, а затем сбрасывать все обратно туда, где он был запущен (именно поэтому редактор должен знать об этих сценариях в первую очередь).
То, чего я пытаюсь добиться, это в основном кнопка "перезагрузить скрипт" в редакторе, которая перекомпилирует и загрузит класс скрипта, связанный с редактируемым уровнем, и, когда пользователь нажмет кнопку "играть", создаст экземпляр самой последней скомпилированный скрипт.
Результатом будет быстрый рабочий процесс редактирования-тестирования в редакторе (вместо альтернативы - сохранения уровня, закрытия редактора, перекомпиляции решения, запуска редактора, загрузки уровня, тестирования).
Теперь я думаю, что я разработал потенциальный способ достижения этого - который сам по себе приводит к ряду вопросов (приведенных ниже):
Скомпилируйте коллекцию
.cs
файлы, необходимые для данного уровня (или, если необходимо, всегоlevels.dll
проект) во временную сборку с уникальным именем. На эту сборку нужно будет ссылатьсяengine.dll
, Как вызвать компилятор таким образом во время выполнения? Как заставить его выводить такую сборку (и можно ли это сделать в памяти)?Загрузите новую сборку. Будет ли иметь значение, что я загружаю классы с одинаковыми именами в один и тот же процесс? (У меня сложилось впечатление, что имена уточняются по имени сборки?)
Теперь, как я уже говорил, я не могу использовать домены приложений. Но, с другой стороны, я не против утечки старых версий классов сценариев, поэтому возможность выгрузки не важна. Если это не так? Я предполагаю, что возможно загрузка нескольких сотен сборок.
При воспроизведении уровня, экземпляр класса, который унаследован от 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/ время выполнения и т. д. вместо того, чтобы встраивать его в каждое приложение, где оно может быть полезно позже. Конечно, для модификации некоторых живых сервисов с помощью такого рода функций потребуется некоторое защитное программирование и практики. Как Эрланг говорят, что разработчики делают)