Adam's blog formerly game development, now just casual madness

Plugin Architecture

I recently spent some thought about the framework in which I will develop engine and editor. All that follows is concept talk, planned but not implemented yet. First of all, there will be a visual editor. It will ressemble the one that came with Nullpunkt but with a lot more emphasis on usability. As before, it will consist of a main runtime which will provide a basic editor environment and any number of dynamically loaded Plugins.

It is ridicolously easy to set up a very basic dynamic Plugin loader in .Net. In the most simple approach you will have one host assembly (here: The editor, executable) and an unknown number of client assemblies (here: Each plugin, dll). The host assembly defines classes or interfaces from which a client assemblies classes will derive from.

public interface ISomePluginInterface
{
	void DoSomething();
}

public abstract class SomePluginBaseClass
{
	private int someValue;

	public void SomeBasicBehaviour();
	public abstract void SomeSpecialBehaviour();
}

After dynamically loading an Assembly, we can easily walk the Types it exports via Reflection and check if any of them derives from one of our hosts base classes / interfaces. If one does, create an instance via Reflection and cast its reference to its base class / interface known to the host.

public ISomePluginInterface LoadSomeSinglePluginFrom(string dllFile)
{
	Assembly dllAssembly = Assembly.LoadFile(dllFile);
	Type[] exportedTypes = dllAssembly.GetExportedTypes();
	foreach (Type t in exportedTypes)
	{
		if (typeof(ISomePluginInterface).IsAssignableFrom(t))
		{
			return Activator.CreateInstance(t, true) as ISomePluginInterface;
		}
	}
}

Of course, this is only the most basic version of plugin management, but it is sufficient for the editors purposes: Retrieving available plugins at startup and integrating them into the development environment by using behaviour abstractly defined in a base class.

However, this job is only done half. When using the editor, a developer will need to access and interact with his own game code: Custom Components, Scripts, etc. will be integrated into a game level just as if they were part of the engines core package. To be able to do that, they need to be located in a plugin themselves, a plugin that is client of the engine core instead of the editor. So there are two types of plugins: Core plugins and editor plugins. Other than editor plugins, core plugins are loaded by the engines core itsself and may not access the editor. They will be deployed together with the game application. When running the final game version, a plugin concept similar to the editors would be sufficient: Load all plugins once, then run the game using the Types supplied by them.

While this would also work when loading the core inside the editor, it wouldn’t be very usable. As all scripting and a lot of game logic takes place inside core plugins, the editor needs the ability to reload modified core plugins. Why? Because you don’t want to restart it every time you recompile your game logic. Roughly sketched, this means saving the current game scene and ressources, abandoning everything, unloading core plugins, reloading core plugins and restoring the previous scene and ressource state from our temporary snapshot.

Unfortunately, this won’t work. .Net does not allow you to unload an assembly once it has been loaded, so reloading plugins is not as easy as it sounds. The only thing .Net does allow you to unload is a whole AppDomain i.e. the context in which code is executed. To abandon a plugin, it would need to be loaded into its own AppDomain, but as far as I know, inter-domain communication is a bit tricky and has some restrictions: When retrieving a foreign object from an AppDomain, all you have is a proxy reference without the ability to use Reflection in the way you are used to it: As the Type information is defined in an unknown Assembly (only the other AppDomain has loaded it!) GetType() becomes useless and will only return the Type of the reference you are holding. The only thing I can think of doing about it would require a lot of prototyping, involve a lot of proxy object indirection and make the whole plugin framework a lot harder to maintain. I’m also almost certain that it will introduce a lot of problems I can’t even think of with my limited experience in AppDomains right now. It seems to me that the AppDomain approach to make core plugins reloadable at runtime just isn’t the way to go here.

There is another approach and I’m probably going with that even though I’m not really 100% content with it. Assemblies can’t only be loaded dynamically from file. It is also possible to pass just a block of bytes and keep the Assembly as an isolated chunk of memory. When loading plugins this way, their .dll files won’t get locked and I can load an Assembly into the current AppDomain more than once. In fact, even as often as I want. Having an Assembly loaded multiple times that way is a little dangerous because they do not connect or relate in any way. When instanciating a Type from the first Assembly instance and trying to pass it to a Type instanciated in the second Assembly instance, it will result in an InvalidCastException – even though it will look like the types are perfectly matching. The “dirty” approach to reload a modified game plugin is not to reload it at all but to additionally load the new version of it into the AppDomain. Serialize the scene, clear everything, load the new plugin Assembly and deserialize the scene. All I need is a custom Type Binder for the deserializer that will always map a requested Type to the one defined in the newest loaded Assembly version. Type conversion problems shouldn’t occur here at all, because of the serialize / deserialize step that needs to resolve all Types anew.

  • Good: Yay, I can reload Plugins in the editor while not at all affecting the core or game!
  • Bad: Well, they actually stack up in memory until restarting the editor. I will prototype this as soon as I get around to it and check if the downside is insignificant enough to ignore it.
67 of 74