Recompila C#mientras se ejecuta, sin AppDomains
compilation assembly.load (4)
Digamos que tengo dos aplicaciones C # - game.exe
(XNA, necesita ser compatible con Xbox 360) y editor.exe
(XNA alojado en WinForms) - ambos comparten un ensamblado engine.dll
que hace la mayor parte del trabajo.
Ahora digamos que quiero agregar algún tipo de script basado en C # (no es un "script" pero lo llamaré así). Cada nivel obtiene su propia clase heredada de una clase base (lo llamaremos LevelController
).
Estas son las restricciones importantes para estos scripts:
Necesitan ser reales, compilados en código C #
Deben requerir un trabajo manual mínimo de "pegamento", si lo hay
Deben ejecutarse en el mismo dominio de aplicación que todo lo demás.
Para el juego, esto es bastante sencillo: todas las clases de script se pueden compilar en un ensamblaje (por ejemplo, levels.dll
) y las clases individuales se pueden instanciar utilizando la reflexión según sea necesario.
El editor es mucho más difícil. El editor tiene la capacidad de "jugar el juego" dentro de la ventana del editor, y luego restablecer todo de nuevo a donde comenzó (por lo que el editor necesita conocer estos scripts en primer lugar).
Lo que estoy tratando de lograr es básicamente un botón de "recargar guión" en el editor que recompilará y cargará la clase de guión asociada con el nivel que se está editando y, cuando el usuario presiona el botón "jugar", creará una instancia del más reciente. guión compilado.
El resultado será un rápido flujo de trabajo de edición de prueba dentro del editor (en lugar de la alternativa, que es guardar el nivel, cerrar el editor, recompilar la solución, iniciar el editor, cargar el nivel, probar).
Ahora creo que he encontrado una forma potencial de lograr esto, que a su vez conduce a una serie de preguntas (que se presentan a continuación):
Compile la colección de archivos
.cs
necesarios para un nivel determinado (o, si es necesario, todo el proyecto delevels.dll
) en un ensamblado temporal con un nombre único. Ese montaje tendrá que hacer referencia aengine.dll
. ¿Cómo invocar el compilador de esta manera en tiempo de ejecución? ¿Cómo hacer que salga tal ensamblaje (y puedo hacerlo en la memoria)?Cargue el nuevo conjunto. ¿Importa que esté cargando clases con el mismo nombre en el mismo proceso? (¿Tengo la impresión de que los nombres están calificados por el nombre del ensamblado?)
Ahora, como mencioné, no puedo usar AppDomains. Pero, por otro lado, no me importa filtrar versiones antiguas de clases de script, por lo que la capacidad de descargar no es importante. ¿A menos que sea? Supongo que cargar unos pocos cientos de ensamblajes es factible.
Al reproducir el nivel, instale la clase que se hereda de LevelController del ensamblado específico que se acaba de cargar. ¿Como hacer esto?
Y finalmente:
¿Es este un enfoque sensato? ¿Se podría hacer de una mejor manera?
ACTUALIZACIÓN: en estos días utilizo un enfoque mucho más simple para resolver el problema subyacente.
Ahora existe una solución bastante elegante, que es posible gracias a (a) una nueva característica en .NET 4.0 y (b) Roslyn.
Asambleas de colección
En .NET 4.0, puede especificar AssemblyBuilderAccess.RunAndCollect
al definir un ensamblaje dinámico, lo que hace que la recolección de elementos no utilizados del ensamblado dinámico:
AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
new AssemblyName("Foo"), AssemblyBuilderAccess.RunAndCollect);
Con vanilla .NET 4.0, creo que debe completar el ensamblaje dinámico escribiendo métodos en IL sin formato.
Roslyn
Ingrese Roslyn: Roslyn le permite compilar código C # sin procesar en un ensamblaje dinámico. Aquí hay un ejemplo, inspirado en estas dos posts blog , actualizado para trabajar con los últimos binarios de 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();
}
}
}
En resumen: con ensamblajes de colección y Roslyn, puede compilar el código C # en un ensamblaje que se puede cargar en un AppDomain
y luego recolectar basura (sujeto a una serie de rules ).
Bueno, quieres poder editar cosas sobre la marcha, ¿verdad? ese es tu objetivo aquí, ¿no?
Cuando compila ensamblajes y los carga, ahora hay una manera de descargarlos a menos que descargue su dominio de aplicación.
Puede cargar ensamblajes precompilados con el método Assembly.Load y luego invocar el punto de entrada a través de la reflexión.
Consideraría el enfoque de montaje dinámico. En su lugar, a través de su AppDomain actual, dice que desea crear un ensamblaje dinámico. Así es como funciona el DLR (tiempo de ejecución de lenguaje dinámico). Con los ensamblajes dinámicos puede crear tipos que implementan alguna interfaz visible y llamarlos a través de eso. La parte posterior de trabajar con ensamblajes dinámicos es que usted debe proporcionar el IL correcto, no puede simplemente generar eso con el compilador incorporado .NET, sin embargo, apuesto a que el proyecto Mono tiene una implementación de compilador C # que podría querer revisar . Ya tienen un intérprete de C # que lee un archivo fuente de C # y lo compila y lo ejecuta, y eso se maneja definitivamente a través de la API System.Reflection.Emit.
Sin embargo, no estoy seguro de la recolección de basura aquí, porque cuando se trata de tipos dinámicos, creo que el tiempo de ejecución no los libera porque pueden ser referenciados en cualquier momento. Solo si el conjunto dinámico se destruye y no existen referencias a ese conjunto sería razonable liberar esa memoria. Si está generando una gran cantidad de código, asegúrese de que la memoria sea recogida en algún momento por el GC.
Echa un vistazo a los espacios de nombres alrededor de Microsoft.CSharp.CSharpCodeProvider y System.CodeDom.Compiler.
Compilar la colección de archivos .cs
Debería ser bastante sencillo como http://support.microsoft.com/kb/304655
¿Importa que esté cargando clases con el mismo nombre en el mismo proceso?
De ningún modo. Son solo nombres.
Instalar la clase que se hereda de LevelController.
Cargue el ensamblaje que creó algo como Assembly.Load, etc. Consulte el tipo que desea crear utilizando la reflexión. Consigue el constructor y llámalo.
Si el lenguaje fuera Java, la respuesta sería usar JRebel. Como no lo es, la respuesta es aumentar el ruido para mostrar que hay demanda para esto. Podría requerir algún tipo de CLR alternativo o ''plantilla de proyecto del motor c #'' y VS IDE trabajando en coordinación, etc.
Dudo que haya muchos escenarios en los que esto sea "imprescindible", pero hay muchos en los que ahorraría mucho tiempo, ya que podría salirse con menos infraestructura y una respuesta más rápida para cosas que no se van a usar por mucho tiempo. (Sí, hay algunos que argumentan que hay que diseñar demasiado las cosas porque se usarán por más de 20 años, pero el problema es que cuando se necesita hacer un cambio masivo, es probable que sea tan costoso como reconstruirlo todo desde cero. Así que todo depende de si gastar dinero ahora o más adelante. Como no sabemos con seguridad si el proyecto se volverá crítico para el negocio más adelante y, de todos modos, tal vez requiera grandes cambios, el argumento para mí es usar el principio "KISS" y tener las complejidades de edición en vivo en el IDE, CLR / runtime, etc. en lugar de integrarlo en cada aplicación donde pueda ser útil más adelante. Ciertamente, se requerirá cierta programación y prácticas defensivas para modificar algún servicio en vivo utilizando este tipo de característica. Como Erlang se dice que los desarrolladores están haciendo