Plugins in Mono

Miguel de Icaza
(miguel@ximian.com)

Plugins in Mono

The Common Language Infrastructure has various mechanisms that developers can use to extend their applications with plugins. This document describes the facilities available and some possible implementation strategies.

Plugins are dynamically loaded modules that extend the functionality of an application. Plugins can be used to enable developers to extend an application after it has shipped, and also as a modularization technique. Applications like the GIMP use a plugin architecture to implement transformation and file formats.

CLI Foundations

When a program or a library are compiled into assemblies (the binary file format used by the CLI)the information about the available classes, methods, variables, names, parameters are stored into the resulting .exe or .dll. This information can be later retrieved through the Reflection API.

Technically, the only difference between a .exe and a .dll in the CLI world is that executable files contain an entry point, and libraries do not.

For example, the following program will list all of the types available in an assembly. Both a .exe or a .dll will work:

using System;
using System.Reflection;

class ListTypes {
	static void Main (string [] args)
	{
		foreach (string file in args){
			Assembly a = Assembly.LoadFrom (file);

			Type [] types = a.GetTypes ();

			Console.WriteLine ("Assembly: " + file);
			foreach (Type t in types)
				Console.WriteLine ("   Type: " + t.Name);
		}
	}
}

Compile and run:

$ mcs query.cs
Compilation succeeded
$ mono query.exe demo.exe
Assembly: query.exe
   Type: Class1
$ _

Once an assembly is loaded, it is also possible to extract the available methods or dynamically invoke methods in the recently loaded assembly. For example, the following program will invoke the method "Boot" on any types passed on the command line.

using System;
using System.Reflection;

class ListTypes {
	static void Main (string [] args)
	{
		foreach (string file in args){
			Assembly a = Assembly.LoadFrom (file);

			Type [] types = a.GetTypes ();

			Console.WriteLine ("Assembly: " + file);
			foreach (Type t in types){
				MethodInfo boot = t.GetMethod ("Boot");
				if (boot == null)
					continue;

				if (!boot.IsStatic)
					continue;
					
				boot.Invoke (null, new Type [0]);
			}
		}
	}
}

We are using the Type's GetMethod to fetch the public method named "Boot". If we find such a method, we invoke it. In this particular example, we want to invoke static methods, so we test for this and we also pass `null' as the first argument to Invoke.

The following sample program can be used to test our new loader:

using System;
	
public class Demo {
	public static void Boot ()
	{
		Console.WriteLine ("Demo.Boot invoked");
	}
}

We compile and run:

$ mcs loader.cs
Compilation succeeded
$ mcs -target:library demo.cs
Compilation succeeded
$ mono loader.exe demo.dll
Assembly: demo.dll
Demo.Boot invoked
$ _

Access to the Main Application

It is now possible to load code dynamically, but a plugin-system often will need to access the internal data structures and methods to carry out a more interesting job. Our plugins can invoke easily any methods that is available to thme at compilation time.

The following sample program shows a tiny spreadsheet application which provides support for invoking plugins.

using System;
using System.Reflection;

public class Spreadsheet {
	string [,] cells = new string [10, 10];

	public string this [int col, int row] {
		get {
			return cells [col, row];
		}

		set {
			cells [col,row] = value;
		}
	}
}

public class Driver {
	static public Spreadsheet sheet = new Spreadsheet ();
	
	static void Main ()
	{
		string cmd, r;

		do {
			Console.Write ("cmd> ");
			cmd = Console.ReadLine ();

			string [] args = cmd.Split (new char [] {' '});
			
			switch (args [0]){
			case "set": 
				sheet [Byte.Parse (args [1]),
				       Byte.Parse (args [2])] = args [3];
				break;

			case "get":
			        r = sheet [Byte.Parse (args [1]),
				           Byte.Parse (args [2])];
				Console.WriteLine ("value: {0}", r);
				break;

			case "plugin":
				InvokePlugin (args [1]);
				break;
			}
		} while (cmd != "quit");
	}

	static void InvokePlugin (string plugin)
	{
		Assembly a = Assembly.LoadFrom (plugin);

		foreach (Type t in a.GetTypes ()){
			MethodInfo method = t.GetMethod ("SheetPlugin");

			if (method != null && method.IsStatic){
				method.Invoke (null, new Type [0]);
				break;
			}
		}
	}
}

Compile the above like this:

$ mcs sheet.cs
Compilation succeeded
$ _

Here is the `export.cs' program that we will compile as a plugin:

using System;
	
public class DumpSheet {
	public static void SheetPlugin ()
	{
		for (int r = 0; r < 10; r++){
			for (int c = 0; c < 10; c++)
				Console.Write ("{0}, ", Driver.sheet [r, c]);
			Console.WriteLine ();
		}
	}
}

Notice that this plugin needs to access the sheet object in the Driver class from the main program. We will compile this as a library which references the main application. This effectively treats the main application as a library consumed by the plugin.

$ mcs export.cs -r:sheet.exe -target:library
Compilation succeeded
$ _

A sample session looks like this:

$ mono sheet.exe
cmd> set 0 0 hello
cmd> set 0 1 world
cmd> set 8 8 Eight
cmd> plugin export.dll
hello, world, , , , , , , , , 
, , , , , , , , , , 
, , , , , , , , , , 
, , , , , , , , , , 
 , , , , , , , , , 
, , , , , , , , , , 
, , , , , , , , , , 
, , , , , , , , , , 
, , , , , , , , Eight, , 
, , , , , , , , , , 
cmd> _

Details

There are plenty of other scenarios and ways of structuring your program to have a plugin system. Currently part of the contract used in this document was to use the public classes, methods and fields exposed by the host application. A common pattern used in plugin systems is to provide a class method to register objects and handlers with the host application.