Перекомпилируйте С# во время работы, без AppDomains
Предположим, что у меня есть два приложения С# - game.exe
(XNA, требуется поддержка Xbox 360) и editor.exe
(XNA, размещенная в WinForms) - они совместно используют сборку engine.dll
, которая подавляет подавляющее большинство работа.
Теперь скажем, что я хочу добавить какой-то скрипт на С# (его не совсем "скриптинг", но я его назову). Каждый уровень получает свой собственный класс, унаследованный от базового класса (назовите его LevelController
).
Это важные ограничения для этих скриптов:
-
Они должны быть реальными, скомпилированными С# -кодами
-
Они должны требовать минимальной ручной работы с клеем, если есть
-
Они должны запускаться в том же AppDomain, что и все остальные
Для игры - это довольно прямолинейно: все классы script могут быть скомпилированы в сборку (скажем, levels.dll
), и отдельные классы могут быть созданы с использованием рефлексии по мере необходимости.
Редактор намного сложнее. Редактор имеет возможность "играть в игру" в окне редактора, а затем reset все обратно туда, где он был запущен (поэтому редактор должен знать об этих сценариях в первую очередь).
То, что я пытаюсь достичь, это в основном "перезагрузить script" в редакторе, который будет перекомпилировать и загрузить класс script, связанный с редактируемым уровнем, и, когда пользователь нажимает кнопку "play", создайте экземпляр последнего скомпилированного script.
Результатом этого будет быстрый рабочий процесс редактирования в редакторе (вместо альтернативы - сохранение уровня, закрытие редактора, перекомпилирование решения, запуск редактора, загрузка уровня, тест).
Теперь я думаю, что я разработал потенциальный способ достижения этого - что само по себе приводит к ряду вопросов (см. ниже):
-
Скомпилируйте коллекцию файлов .cs
, необходимых для заданного уровня (или, если нужно, весь проект levels.dll
) во временную сборку с уникальным именем. Эта сборка должна будет ссылаться на engine.dll
. Как вызвать компилятор таким образом во время выполнения? Как получить его для вывода такой сборки (и могу ли я сделать это в памяти)?
-
Загрузите новую сборку. Будет ли иметь значение, что я загружаю классы с тем же именем в один и тот же процесс? (У меня создается впечатление, что имена квалифицируются по имени сборки?)
Теперь, как я уже говорил, я не могу использовать AppDomains. Но, с другой стороны, я не против утечки старых версий классов script, поэтому возможность разгрузки не важна. Разве это не так? Я предполагаю, что загрузка может быть несколько сотен сборок.
-
При воспроизведении уровня введите экземпляр класса, унаследованного от LevelController, из только что загруженной конкретной сборки. Как это сделать?
И наконец:
Это разумный подход? Может ли это сделать лучше?
UPDATE: в наши дни я использую гораздо более простой подход для решения основной проблемы.
Ответы
Ответ 1
Проверьте пространства имен вокруг Microsoft.CSharp.CSharpCodeProvider и System.CodeDom.Compiler.
Скомпилировать коллекцию файлов .cs
Должно быть довольно просто, как http://support.microsoft.com/kb/304655
Будет ли иметь значение, что я загружаю классы с тем же именем в один и тот же процесс?
Совсем нет. Он просто называет.
экземпляр класса, который унаследован от LevelController.
Загрузите сборку, которую вы создали, например, Assembly.Load и т.д. Запросите тип, который вы хотите использовать, используя отражение. Получите конструктор и назовите его.
Ответ 2
В настоящее время существует довольно элегантное решение, которое стало возможным благодаря (а) новой функции в .NET 4.0 и (b) Roslyn.
Коллекционные сборки
В .NET 4.0 вы можете указать AssemblyBuilderAccess.RunAndCollect
при определении динамической сборки, что делает сборку мусора динамической сборки:
AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
new AssemblyName("Foo"), AssemblyBuilderAccess.RunAndCollect);
С vanilla.NET 4.0, я думаю, вам нужно заполнить динамическую сборку, написав методы в raw IL.
Рослин
Введите Roslyn: Roslyn позволяет вам скомпилировать исходный код С# в динамическую сборку. Вот пример, вдохновленный этими двумя блоками сообщения, обновленный для работы с последними бинарниками 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 вы можете скомпилировать код С# в сборку, которая может быть загружена в AppDomain
, а затем собрана мусор (с учетом количества правила).
Ответ 3
Ну, ты хочешь иметь возможность редактировать вещи "на лету", верно? что ваша цель здесь не так ли?
Когда вы собираете сборки и загружаете их там, теперь их можно выгрузить, если вы не разгрузите AppDomain.
Вы можете загрузить предварительно скомпилированные сборки с помощью метода Assembly.Load, а затем вызвать точку входа через отражение.
Я бы рассмотрел подход динамической сборки. Когда вы через свой текущий AppDomain говорите, что хотите создать динамическую сборку. Так работает DLR (динамическое языковое исполнение). С помощью динамических сборок вы можете создавать типы, которые реализуют некоторый видимый интерфейс и вызывать их через это. Задняя сторона работы с динамическими сборками заключается в том, что вы сами должны предоставить правильный IL, вы не можете просто генерировать это со встроенным компилятором .NET, однако, я уверен, проект Mono имеет реализацию компилятора С#, которую вы можете захотеть проверить, У них уже есть интерпретатор С#, который читает в исходном файле С# и компилирует его и выполняет, и определенно обрабатывается через API System.Reflection.Emit.
Я не уверен в сборке мусора здесь, потому что, когда дело доходит до динамических типов, я думаю, что среда исполнения не освобождает их, потому что в любой момент они могут ссылаться. Только если сама динамическая сборка будет уничтожена, и ссылки на эту сборку не будут доступны, было бы разумно освободить эту память. Если вы и повторно генерируете много кода, убедитесь, что память в какой-то момент собрана GC.
Ответ 4
Если языком был Java, то ответ должен был бы использовать JRebel. Поскольку это не так, ответ заключается в том, чтобы набрать достаточный шум, чтобы показать, что это требует. Это может потребовать какой-то альтернативный CLR или "шаблон проекта двигателя С#" и VS IDE, работающие в координации и т.д.
Я сомневаюсь, что существует множество сценариев, где это "должно быть", но есть многие, в которых это экономит много времени, поскольку вы могли бы уйти с меньшей инфраструктурой и более быстрым поворотом для вещей, которые не будут использоваться для длинный. (Да, есть некоторые, которые утверждают, что они слишком сложны, потому что они будут использоваться более 20 лет, но проблема в том, что когда вам нужно провести массовые изменения, это, вероятно, будет так же дорого, как и восстановление всего этого с нуля. Таким образом, дело доходит до того, тратить ли деньги сейчас или позже. Поскольку он не знает наверняка, что проект станет критическим для бизнеса позже и может потребовать больших изменений позже, аргумент для меня состоит в том, чтобы использовать "KISS" -принцип и иметь сложности живого редактирования в среде IDE, CLR/runtime и т.д. вместо того, чтобы создавать его в каждом приложении, где это может быть полезно позже. Конечно, некоторые защитные программирования и практики будут необходимы для модификации некоторых живых сервисов с использованием такого рода функций. devs, как говорят, делают)