Writing Plugins for Art of Illusion

Copyright 2000-2014 by Peter Eastman

Author: Peter Eastman
Last Modified: Jan. 18, 2014
Modified For: v3.0
Original Release Date: Oct. 4, 2000
Originally Written For: v0.6

Contents

  1. Overview
  2. Types of Plugins
  3. Packaging Plugins
  4. The XML File
  5. Plugin Processing
  6. Resources
  7. Using Plugins in Scripts

Overview

Art of Illusion is based around a plugin architecture that allows it to be extended in many ways. Plugins typically: More precisely, a plugin is a jar file containing one or more Java classes that implement known interfaces or extend known classes. The core application defines many such interfaces and classes (known as plugin types), but plugins can also define their own types. The jar file also contains an XML file called "extensions.xml". This file lists all the plugin classes found in that jar file. When Art of Illusion starts up, it looks at every jar file in its Plugins directory, reads the extensions.xml file in each one, and registers the plugin classes.

Types of Plugins

The following plugin types are defined by the core application: In addition to the plugin types defined by the core application, the Raytracer (which is itself a plugin) defines two plugin types: For details on how to write plugins of each of these types, see the API documentation for their respectives classes and interfaces. There also is a separate tutorial specifically on writing Texture and Material plugins. Be aware that plugin objects get instantiated by calling newInstance() on the class object. This means that plugin classes always need to have a constructor that takes no arguments.

Packaging Plugins

To package your Java classes as a plugin, place the main plugin class (the one which implements the appropriate interface or extends the appropriate class) and any other classes or files it needs into a single jar file. You may include multiple plugins in a single jar file. In addition, the jar file must contain an XML file called "extensions.xml" (all lower case) that provides information about the contents of the jar file.

Each class file must be stored in the correct directory within the jar file that corresponds to the package it is in. Otherwise, the class loader will not be able to locate it.

To make all of this clearer, suppose that you wish to package two plugins into a jar file, whose fully qualified class names are com.mycompany.GoodPlugin and com.mycompany.BetterPlugin. The jar file should then contain three entries (in addition to any other classes or files which may be used by the plugins):

com/mycompany/GoodPlugin.class
com/mycompany/BetterPlugin.class
extensions.xml

The XML File

The content of the extensions.xml file will look something like this:
<?xml version="1.0" standalone="yes"?>
<extension name="AmazingPlugins" version="1.0">
  <author>Peter Eastman</author>
  <date>Oct. 15, 1842</date>
  <description>Plugins that will totally change your life.</description>
  <plugin class="com.mycompany.GoodPlugin"/>
  <plugin class="com.mycompany.BetterPlugin"/>
</extension>
The key part of this file is the two <plugin> tags. Each one identifies a plugin class contained in the jar file. Most of the other content, such as the <author>, <date>, and <description> tags are just for information. They are shown to the user in the Scripts and Plugins Manager window but have no other effect on the behavior of the plugin.

Plugins can also define new plugin types by which they can themselves be extended. To define a new plugin type, include a <category> tag in extensions.xml:

<category class="com.mycompany.AmazingExtension"/>
The value of the "class" attribute may be a class or interface. Other plugins may then extend the class or implement the interface. This is discussed in more detail below.

By default, every plugin jar file is loaded with an independent classloader, so the classes defined in one cannot "see" the classes defined in any other. Usually this is exactly what you want. It eliminates the risk of class name conflicts between plugins, and reduces the possibilities for one plugin to interfere with the operation of another one. But sometimes you need plugins to interact more directly. For example, you might want the code in one jar file to directly use some classes that were defined in another one. Also, if a jar file defines a new plugin type, other jar files must be able to implement the corresponding interface or subclass the corresponding class.

You can do this by including an <import> tag in extensions.xml:

<import name="Renderers"/>
The value of the "name" attribute is the name of the plugin to import, as specified by its extensions.xml file. In this case we are importing the Renderers plugin that comes bundled with Art of Illusion. This would allow you, for example, to define a raytracer object factory by implementing RTObjectFactory. Each plugin is still loaded with its own classloader, but the classloader for the imported plugin becomes a parent of the one for the plugin that imports it.

Importing a plugin completely breaks down the barriers between plugins, so you lose all the protections you normally get from plugin isolation. If you only need limited interaction between them, an alternative is to export only the particular methods that should be callable from other plugins. Here is an example taken from the Tools plugin bundled with Art of Illusion:

  <plugin class="artofillusion.tools.LatheTool">
    <export method="latheCurve" id="artofillusion.tools.LatheTool.latheCurve"/>
  </plugin>
The class artofillusion.tools.LatheTool has a static method latheCurve(). This method is exported so it can be called by any other plugin without needing to import the whole plugin. This is discussed more below.

Plugin Processing

When you start up Art of Illusion, one of the first things it does is to look through every file in the Plugins directory. For each one, it first determines whether it is a zip or jar file, and if so, looks for an entry called "extensions.xml". It loads that entry and uses it to identify the main plugin classes for that file. It loads each plugin class, then creates an instance of it by calling newInstance() on the class object. For some plugin types, such as Renderers, only that one instance is ever created and used repeatedly. For other types, such as Textures, many other instances may be created later.

This procedure has a few consequences which you should keep in mind. First, it means that every plugin class must have a constructor that takes no arguments . This allows it to be instantiated with a call to Class.newInstance().

Second, when many plugins are present, the time required to load and instantiate all of them can be significant. It also means that memory must be used to store every plugin class, even if the user never actually uses that plugin. To alleviate both of these problems, it often is a good idea to minimize the size of the main plugin class. By placing the bulk of the code into helper classes which do not get loaded until the plugin is actually used, you can save memory and reduce the startup time.

All this processing is done by the class artofillusion.PluginRegistry. It provides methods you can use to interact with plugins from your own code.

In one of the examples above, we defined a new plugin type com.mycompany.AmazingExtension. Here is how you can retrieve a list of every registered plugin of that type:

List<AmazingExtension> extensions = PluginRegistry.getPlugins(AmazingExtension.class);
You can then make use of them in whatever way is appropriate for your code. It also allows you to invoke methods that were exported by plugins. Here is how you would invoke the latheCurve() method that was exported by LatheTool:
Mesh mesh = (Mesh) PluginRegistry.invokeExportedMethod("artofillusion.tools.LatheTool.latheCurve", curve, 3, 8, 360.0, 0.0)
The first argument is the id that was specified in the <export> tag. Any additional arguments are simply passed on to the method.

Resources

Plugins can extend Art of Illusion in two ways. First, they can provide new executable code. That is how all the plugin types discussed above work. Second, they can provide new data. This is done by providing resources.

A resource typically represents a data file stored in the plugin jar. It is defined by the following information:

PluginRegistry provides static methods for accessing resources. Call getResources() to get a list of all resources of a given type, and getResource() to access the resource with a particular ID and type. See the API documentation for details.

To define a resource, include a <resource> tag in your extensions.xml file. For example,

<resource type="TranslateBundle" id="amazingPlugin" name="amazing"/>
If you want to include multiple versions of a resource for different locales, use a separate <resource> tag for each one and include an attribute specifying the code for the language, or optionally, language and country: locale="es" or locale="en_GB".

There are two standard resource types used by Art of Illusion: translate bundles and UI themes. Translate bundles are used to provide localizable text. They have the type code "TranslateBundle". See the API documentation for artofillusion.ui.Translate for details. UI themes are used to define icons and other information that can be customized by the user. They have the type code "UITheme". See the API documentation for artofillusion.ui.ThemeManager for details. Of course, you can also define entirely new resource types. Just select a value for the "type" attribute, then use PluginRegistry to look up resources with that type code.

Using Plugins in Scripts

When a Groovy or Beanshell script is executed, it uses a specially constructed classloader that includes all classloaders for all plugins as parents. This means that scripts can directly access any class defined in any plugin with no special effort.

Although the script can see classes in different plugins, those classes still cannot see each other. So although this does create a danger of class name conflicts, it is fairly small. If a script tries to invoke a class, and two different plugins each define a class with that name, it is unpredictable which one it will get. But if a plugin class invokes another class in the same jar, it is always guaranteed to get the right one, even if another jar happens to define a class with the same name. It is only the script itself that can see everything, not plugin code invoked by the script.