views:

796

answers:

2

Let’s say that I have two C# applications - game.exe (XNA, needs to support Xbox 360) and editor.exe (XNA hosted in WinForms) - they both share an engine.dll assembly that does the vast majority of the work.

Now let’s say that I want to add some kind of C#-based scripting (it’s not quite "scripting" but I’ll call it that). Each level gets its own class inherited from a base class (we’ll call it LevelController).

These are the important constraints for these scripts:

  1. They need to be real, compiled C# code

  2. They should require minimal manual "glue" work, if any

  3. They must run in the same AppDomain as everything else

For the game - this is pretty straight forward: All the script classes can be compiled into an assembly (say, levels.dll) and the individual classes can be instanced using reflection as needed.

The editor is much harder. The editor has the ability to "play the game" within the editor window, and then reset everything back to where it started (which is why the editor needs to know about these scripts in the first place).

What I am trying to achieve is basically a "reload script" button in the editor that will recompile and load the script class associated with the level being edited and, when the user presses the "play" button, create an instance of the most recently compiled script.

The upshot of which will be a rapid edit-test workflow within the editor (instead of the alternative - which is to save the level, close the editor, recompile the solution, launch the editor, load the level, test).


Now I think I have worked out a potential way to achieve this - which itself leads to a number of questions (given below):

  1. Compile the collection of .cs files required for a given level (or, if need be, the whole levels.dll project) into a temporary, unique-named assembly. That assembly will need to reference engine.dll. How to invoke the compiler this way at runtime? How to get it to output such an assembly (and can I do it in memory)?

  2. Load the new assembly. Will it matter that I am loading classes with the same name into the same process? (I am under the impression that the names are qualified by assembly name?)

    Now, as I mentioned, I can’t use AppDomains. But, on the other hand, I don’t mind leaking old versions of script classes, so the ability to unload isn’t important. Unless it is? I’m assuming that loading maybe a few hundred assemblies is feasible.

  3. When playing the level, instance the class that is inherited from LevelController from the specific assembly that was just loaded. How to do this?

And finally:

Is this a sensible approach? Could it be done a better way?

A: 

Well, you want to be able to edit things on the fly, right? that's your goal here isn't it?

When you compile assemblies and load them there's now way to unload them unless you unload your AppDomain.

You can load pre-compiled assemblies with the Assembly.Load method and then invoke the entry point through reflection.

I would consider the dynamic assembly approach. Where you through your current AppDomain say that you want to create a dynamic assembly. This is how the DLR (dynamic language runtime) works. With dynamic assemblies you can create types that implement some visible interface and call them through that. The back side of working with dynamic assemblies is that you have to provide correct IL yourself, you can't simply generate that with the built in .NET compiler, however, I bet the Mono project has a C# compiler implementation you might wanna check out. They already have a C# interpreter which reads in a C# source file and compiles that and executes it, and that's definitely handled through the System.Reflection.Emit API.

I'm not sure about the garbage collection here though, because when it comes to dynamic types I think the runtime doesn't release them because they can be referenced at any time. Only if the dynamic assembly itself is destroyed and no references exist to that assembly would it be reasonable to free that memory. If you're and re-generating a lot of code make sure that the memory is, at some point, collected by the GC.

John Leidegren
+1  A: 

Check out the namespaces around Microsoft.CSharp.CSharpCodeProvider and System.CodeDom.Compiler.

Compile the collection of .cs files

Should be pretty straightforward like http://support.microsoft.com/kb/304655

Will it matter that I am loading classes with the same name into the same process?

Not at all. It's just names.

instance the class that is inherited from LevelController.

Load the assembly that you created something like Assembly.Load etc. Query the type you want to instanciate using reflection. Get the constructor and call it.

Alex
Would I be right in saying that, to instance the type, after calling CodeDomProvider.CompileAssemblyFromFile, I would have something like: compilerResults.CompiledAssembly.GetType("MyLevelController").GetConstructor(...).Invoke(...);
Andrew Russell
Yes, one way is like: Assembly generatedAssembly = Assembly.Load(fromMemory);OR Assembly generatedAssembly = Assembly.LoadFrom(assemblyFile); Type[] types = generatedAssembly.GetTypes(); object instance = types[0].GetConstructor(new Type[0]).Invoke(new object[0]);
Alex
A shortcut might be "generatedAssembly.CreateInstance(typeName)
Alex
CodeDom i s really a deprecated API, avoid using it is not without it's limitations.
John Leidegren
Ok, that's interesting news. But what is the appropriate, equally complex approach then? I mean what you wrote in your answer sounds a lot more complicated to me.
Alex