How to create a dynamic plugin
From OpenSimulator
Line 2: | Line 2: | ||
{{Template:Quicklinks}} | {{Template:Quicklinks}} | ||
<br /> | <br /> | ||
+ | |||
+ | '''NOTE: This tutorial now refers to a very old OpenSim and so is only partially useful. See [[IRegionModule]] for something more up to date.''' | ||
== Quick Start == | == Quick Start == |
Revision as of 14:27, 6 May 2011
NOTE: This tutorial now refers to a very old OpenSim and so is only partially useful. See IRegionModule for something more up to date.
Quick Start
See Dynamic Plugin Quickstart for a (hopefully) short introduction.
Current state of Plugin loading
The .NET runtime system has always made reflecting and introspecting of assemblies at runtime very easy to do, making dynamic loading of modules possible with very little effort.
This has unfortunately lead to a situation in OpenSim where historically module loading was hard-coded and done by hand, using copy-and-paste code. Clearly this is a suboptimal situation.
Effort was made to find a reusable, cross-platform dynamic assembly loader that could be used as-is, instead of reinventing our own. The only current candidate is Mono.Addins. MS's .NET runtime has its own Addin class, however as of this writing it is not yet implemented in Mono.
A class called PluginLoader has been created to thinly wrap Mono.Addins, and the assemblies loaded by this class have been standardized on the IPlugin interface hierarchy. With the exception of RegionModules, all dynamically loaded assemblies should be called Plugins, and inherit from this base class.
Currently the following interfaces have been converted to IPlugin:
- OpenSim.IApplicationPlugin
- OpenSim.Grid.GridServer.IGridPlugin
- OpenSim.Data.IGridDataPlugin
- OpenSim.Data.ILogDataPlugin
And the following Assemblies are being loaded by PluginLoader:
- OpenSim.ApplicationPlugins.LoadRegions.LoadRegionsPlugin
- OpenSim.ApplicationPlugins.Rest.Regions.RestRegionPlugin
- OpenSim.ApplicationPlugins.Rest.Inventory.RestHandler
- OpenSim.ApplicationPlugins.RemoteController.RemoteAdminPlugin
- OpenSim.Data.MySQL.MySQLGridData
- OpenSim.Data.MySQL.MySQLLogData
Why Mono.Addins
Mono.Addins offers features outside of being upstream maintained by the Mono project:
- logically splits up module interfaces from implementations in a cross platform way through the use of text-based "extension points" and "addin manifests"
- lazily loads assemblies only when finally necessary
- tracks module dependencies
- allows customize the addin manifest XML
- automatically handles module sharing across application domains
- provides support for downloading assemblies from remote repositories
For more information, please read the Mono.Addins FAQ
Mono.Addin concepts
Although effort has been made to make PluginLoader independent of the module loading mechanism, there are some concepts that should be understood when learning how to learn the system.
A more detailed (and authoritative!) explanation can be found here and here.
Separating Male and Female
A good system provides a means of loosely coupling two connected parts with a common interface so that one can be changed easily without affecting the other.
The assembly that consumes a service is called a host, and the provider of a service is called an addin.
A service that can be split into producer/consumer role is called an extension. An extension consists of an extension point which is essentially a text "path" that give a hierarchical name to the service a consumer requires, and an extension node which represents an object that can provide that service. If a host asks for the addin(s) corresponding to extension point "/Foo", and an addin extension node claims to implement "/Foo", then the two can be joined to make a functioning whole.
All OpenSim extension points start at the root "/OpenSim". If you wished to create a new service "Foo", you might choose to name it "/OpenSim/Foo".
In operation the consumer of a service, the host, requests that the system load all addins that claim to extend an extension point, and a list is returned to the consumer, who can choose to load any subset of those providers, the addins, into memory.
Describing an Addin
Clearly metadata must be associated with both host and addin, in our case this is an XML file that has the extension *.addin.xml, called a manifest.
In practice, the host manifest and addin manifest are so similar as to be almost exactly the same. The principle difference is that host manifests tell Mono.Addins where to find abstract interfaces, and addin manifests tell Mono.Addins where to find concrete implementations of those interfaces.
The manifest has 3 critical parts
- Name and Version information
this is used to check inter-addin dependencies - Filename of assemblies where the consumer interfaces or provider implementations can be found
this is needed to know which assemblies must be searched - Extension Point path and Fully Qualified .NET Class Name of the consumer interface or provider implementation
this is needed to load the actual classes into memory
The way to tell a host manifest from an addin manifest is that host manifests will *always* contain an isroot=true attribute in the <Addin> tag. However this is complicated by the fact that an addin can act as host for another addin in a recursive manner.
Building a model
When the PluginLoader is first created within the host, Mono.Addins will search the directory the host was executed and search for all files that end in *.addin.xml. It uses the information to build a registry about the addins in addin-db-${VERSION}/ directory. The purpose of the registry is to collect all information once, then lazily load assemblies into memory only at the last moment when it is precisely known exactly which class is required by the user.
How to make your own plugin
- Decide on the service you wish to separate into provider and consumer. Ex: "LartMe"
- Create an extension point to identify that service. Ex: "/OpenSim/LartMe"
- Create an consumer interface that has the methods needed, and derive it from OpenSim.Framework.IPlugin. Ex: "OpenSim.ILart"
- If that interface needs a constructor that takes parameters, you will also have to create a class derived from PluginInitialiserBase that can act as a closure for calling a parameterized constructor. See IApplicationPlugin for an example. Ex: "OpenSim.LartInitializer"
- Write a provider class that implements that interface. Ex: "OpenSim.LartPlugin"
- Write a host manifest. See OpenSim.addin.xml as an example.
- Ensure that /Addin@isroot=true
- Ensure that /Addin@id is set to a unique name. Ex: "LartMe"
- Ensure that /Addin/Runtime/Import@assembly exists for each assembly that either hosts the application (host.EXE) or defines an addin consumer interface (Lart.dll) used by the host
- Ensure that /Addin/ExtensionPoint@path is set. Ex: "<ExtensionPoint path="/OpenSim/LartMe">..."
- Ensure that /Addin/ExtensionPoint/ExtensionNode@name is set to "Plugin". This is a custom derived node type (OpenSim.Framework.PluginExtensionNode).
- Ensure that /Addin/ExtensionPoint/ExtensionNode@type is set to "OpenSim.Framework.PluginExtensionNode"
- Ensure that /Addin/ExtensionPoint/ExtensionNode@objectType is set to the fully qualified name of the consumer interface found in the above assembly import. Ex: "OpenSim.ILart"
- Write a addin manifest. See LoadRegionsPlugin.addin.xml as an example.
- Ensure that /Addin/Runtime/Import@assembly exists for the assembly that implements the producer class.
- Ensure that /Addin/Dependencies/Addin@id exists and is the same as the id of the host manifest. Ex: "Lart"
- Ensure that /Addin/ExtensionPoint@path is set and is the same as that of the host manifest. Ex: "<ExtensionPoint path="/OpenSim/LartMe">..."
- Ensure that /Addin/Extension/Plugin@type is set to the fully qualified name of the implementing provider class. Ex: "OpenSim.LartPlugin"
- Optionally, if you wish to discriminate on "provider" you can set /Addin/Extension/Plugin@provider
- When you need to load the producer, create a PluginLoader of the type defined by the consumer interface. Ex: "new PluginLoader <ILart> ();"
- If the provider requires a Initializer, it should be passed to the PluginLoader constructor. All plugins will be initialized using this initializer. Ex: "new PluginLoader <ILart> (new LartInitializer ("l4rt"));"
- Call PluginLoader.Load using the required extension point. Ex: "loader.Load ("/OpenSim/LartMe")"
- Instead of putting the plugin manifest into the bin/ directory, it is cleaner to embed it into the plugin dll by specifying buildAction="EmbeddedResource" in prebuild.xml to the Files section of your project
- eg <Match pattern="*.addin.xml" path="Resources" buildAction="EmbeddedResource" recurse="true"/>
- For host manifests, it usually makes more sense to have them in the bin/ directory (they would otherwise have to be embedded into a number of dlls and exes)
All loaded plugins can now be found in "loader.Plugins" and used as necessary.
If that looks like a lot of work, consider that outside of the normal work of splitting a class into interface and implementation, its really mostly just boilerplate, and can be accomplished by just comparing how things are currently implemented within OpenSim.
Filtering and Constraining
When you ask to load an extension point, you are potentially bringing a lot of assemblies into memory. Before you do that, you will most likely want to have a say about how many assemblies you expect, and which one you need.
You can do this by assigning a IPluginConstraint or IPluginFilter to an extension point.
An IPluginConstraint is applied each time Load() is called on an extension point, but the behavior of the constraint is up to the implementor. It is assumed that a constraint will ask something of Mono.Addins, and throw an exception if it is not happy with the answer.
Loading Critical Plugins
For some applications we expect precisely a certain amount of plugins to be loaded. Often exactly one. PluginCountConstraint implements precisely this:
int n = 1; PluginLoader <ILart> loader = new PluginLoader(); loader.AddConstraint ("/OpenSim/LartMe", new PluginCountConstraint (n)); loader.Load ("/OpenSim/LartMe");
If the above code is run, and more or less than n plugin could be loaded, the constraint will throw a PluginConstraintViolatedException.
Loading Named Plugins
Often the reason why more than one plugin is available to load is because we wish to afford choice to the user as to which plugin he prefers, exposed in a configuration file. We read from the file the user's preference, and want to only load the desired plugin.
If we have some addin manifests that look like these:
<Addin id="OpenSim.Lart" version="0.1"> <Runtime> <Import assembly="LartCorp.dll"/> </Runtime> <Dependencies> <Addin id="OpenSim" version="0.5" /> </Dependencies> <Extension path = "/OpenSim/LartMe"> <Plugin provider="LartCorp" type="Corp.LartMatic" /> </Extension> </Addin> ... <Addin id="OpenSim.Lart" version="0.1"> <Runtime> <Import assembly="OpenSim.Lart.dll"/> </Runtime> <Dependencies> <Addin id="OpenSim" version="0.5" /> </Dependencies> <Extension path = "/OpenSim/LartMe"> <Plugin provider="OpenLart" type="OpenSim.LartPlugin" /> </Extension> </Addin>
And loading code that looks like this:
PluginLoader <ILart> loader = new PluginLoader(); loader.AddConstraint ("/OpenSim/LartMe", new PluginProviderFilter ("LartCorp")); loader.Load ("/OpenSim/LartMe");
Only the "LartCorp" plugin will be loaded.