The Plugin Architecture

Contents

For Fungimol, a plugin is a shared object that implements a modification to Fungimol's behavior. On Unix systems shared objects have names ending in ".so". Shared objects are similar to executables in that they are generated by a compiler from source code; they are different in that they must be loaded into another executable before they can be used.

Goals

Fungimol's plugin architecture meets these goals:

Plugins when Fungimol Starts

When Fungimol starts, it looks for the environment variable FUNGIMOL_PATH. If this is set, it is interpreted as a colon-separated list of directory names. Each of these directories is examined in turn, and Fungimol attempts to load all shared objects found in each directory.

Then it repeats this process with the default fungimol path, which is /home/tim/brennerc/brennerc/fungimol.

The order that the objects are loaded within one directory is unpredictable, so if you write plugins that require other plugins to be present before you can load them, you'll have to put them in multiple directories and control the order that they load by controlling the order of the entries in FUNGIMOL_PATH.

Example of a Predefined Plugin

For this example to have any meaning to you, you'll need to be familiar with how DesignAtom's work, as described in the tutorial. When you press shift-comma and indicate that a DesignAtom should be constructed, the following dialogue in the text window can happen. Briefly giving the story that will be given in detail below, the bold portion of the dialogue is editing an instance of the C++ class DesignAtomConfiguration, and at the end of this dialogue the DesignAtomConfiguration is given to a method on the C++ class DesignAtomFactory, which constructs an instance of the C++ class DesignAtom. The instance of DesignAtom is the actual object that is inserted into the scene.
The configuration has these read/write fields:
Number  Field name      Field type  Field value
1       Atom to create  Atom        DesignAtom//C
Give the number of a field to edit, or press 0 to exit: 1
This slot implements the protocol Atom.
The currently chosen factory is DesignAtom.
Do you want to change the factory and reset to the new factory's
default configuration? n
Editing the configuration...
The configuration has these read/write fields:
Number  Field name     Field type        Field value
1       Atomic Number  Positive Integer  6
2       Material       ColorSlotValue    (0.588235, 0.588235, 0.588235, 0.750000)
3       Radius         Positive Float    1.548000
Give the number of a field to edit, or press 0 to exit: 1
The old value of the slot is 6.
What do you want the new value to be? 1
The configuration has these read/write fields:
Number  Field name     Field type        Field value
1       Atomic Number  Positive Integer  1
2       Material       ColorSlotValue    (0.588235, 0.588235, 0.588235, 0.750000)
3       Radius         Positive Float    1.548000
Give the number of a field to edit, or press 0 to exit: 0
The configuration has these read/write fields:
Number  Field name      Field type  Field value
1       Atom to create  Atom        DesignAtom//H
Give the number of a field to edit, or press 0 to exit: 0

Namespaces

In C++, there are several different kinds of names. For example, consider type names, variable names, and label names. None of these have anything to do with each other. If a variable has the same name as a type, that has no special significance at all. It is necessary when learning C++ to figure out what the different kinds of names are and to realize that they have nothing to do with each other.

Similarly, there are several different kinds of names in the plugin architecture. We haven't yet described what sorts of things have these names, but it's important to say what the namespaces are at first and to make it clear that they are entirely different from each other. The namespaces in the list have absolutely nothing to do with each other.

All of the namespaces listed below happen at run time. Thus they are definitely unconnected (except by convention) with names that the C++ compiler uses directly: class names, variable names, label names, file names, etc., since all of those names happen at compile time and generally are not available at run time.

Slot names
The string given to the user when prompted to edit a slot in a configuration. For example, in the dialogue above, "Atomic Number" is a slot name.
Slot types
The string given to the user to explain the type of a slot. For example, "Positive Integer" is a slot type in the dialogue above.
Factory names
A string used to identify a Factory at run time. For example, "DesignAtom" is a factory name in the dialogue above. Although we haven't said what a Factory is yet, it might help to say that every Factory has a getName() method that returns its factory name.
Protocol names
A string used to identify a group of factories that produce objects that provide a similar interface to each other. For example, "Atom" in the dialogue above is a protocol name. The factories named "DesignAtom", "BoringAtom", and "BrennerAtom" are all in the protocol "Atom".

C++ Classes in the Plugin Architecture

The plugin architecture uses these C++ classes to do its work:
Configurable
Configurable is an abstract base class that represents an object that can be built by a plugin. The Configurable class itself has nothing more than the reference count for the object. All useful Configurables are instances of some subclass of Configurable. For example, a DesignAtom is one particular instance of the C++ class DesignAtom, which is (indirectly) a subclass of Configurable.
Factory
Factory is an abstract base class that represents an object that knows how to configure and make a Configurable. Specifically, every Factory has a name (which is a String), a method that gives a default Configuration (see below), and a method that can take the default Configuration (or any edited version of the default Configuration) and returns a Configurable. All useful Factories are instances of some subclass of Factory. For example, there is a class DesignAtomFactory defined in the source code in Chemistry/DesignAtom.cpp that is a Factory that generates DesignAtom's.

Note that this source file does not provide any .h files for use by other parts of Fungimol. All use of DesignAtom's is by invoking the factory and by manipulating the base class BoringAtom.

It's important to be clear on the difference between the Factory and the Configurable it makes. For example, the C++ class DesignAtom is a subclass of Configurable; it has a bunch of methods that deal with the physics of how DesignAtom's should work in the scene. The C++ class DesignAtomFactory is a subclass of Factory; it has methods that describe how to build a DesignAtom.

Unfortunately for the purposes of generating a clear explanation, the C++ class DesignAtomFactory has a getName() method which returns the factory name "DesignAtom". Recall the discussion of namespaces above; the result from the getName() method is a factory name, but the name of the C++ class DesignAtomFactory is a C++ name, so only convention connects the two. All of the occurrences of the word "DesignAtom" in the terminal dialogue above ultimately came from this string, and that terminal dialogue would have been less clear if they all said "DesignAtomFactory".

Configuration
A Configuration contains information about how to make a specific instance of some subclass of Configurable. A Configuration has a number of fields (called SlotValue's; see below) that have values the user can edit, and it can also have instance variables that are invisible to the user. You give a Configuration to a Factory, and get back a Configurable.

For example, the Configuration that is given to DesignAtomFactory to make a DesignAtom is an instance of the C++ class DesignAtomConfiguration, which is (indirectly) a subclass of Configuration. In the dialogue above, the bold part of the dialogue is editing a DesignAtomConfiguration. DesignAtomConfiguration is defined in the source code in Chemistry/DesignAtomConfiguration.h and Chemistry/DesignAtomConfiguration.cpp.

FactoryTable
Unlike the other classes listed here, FactoryTable is not a base class that one uses by taking subclasses; it is a complete class that is simply used. It stores two tables: A factory can be registered in any number of protocols. Except for the tables maintained by FactoryTable, there is no other indication in the system of which factories are in which protocol.

Actions

"Action" is a protocol representing actions the user can take. Factories in the Action protocol always have a Configuration that is a subclass of the C++ class ActionConfiguration, and the configurable they generate is an instance of the C++ class Action. The C++ class Action only encapsulates a success message and an error message. The behavior that the user requested is performed by the Factory as part of constructing the Action, and the Factory registers an information message or a failure message or no message at all in the Action it returns.

An ActionConfiguration differs from other Configurations only in that it has a field that the Factory can use to fetch a top-level object representing the entire system. If ac is an instance of the C++ class ActionConfiguration, then ac->getTopLevel() is an instance of the class TopLevel decared in TopLevel/TopLevel.h. The Factory can call methods on a TopLevel to do whatever it needs to do to the system, including getting and then manipulating the SceneGraph which has all of the objects in the scene.

Actually, "Action" is a subprotocol of two other protocols, "KeyboardMouseAction" and "SpaceOrbAction". "KeyboardMouseAction" is a protcol for Actions that only make sense when invoked in response to an event from the keyboard or the mouse, and "SpaceOrbAction" is a protocol for Actions that only make sense when invoked in response to an event from the Space Orb. Factories in the generic "Action" protcol should not pay attention to any input events, so they can work when invoked because of keyboard events, mouse events, Space Orb events, or events from some other device which does not yet have a device driver plugged into Fungimol.

The sample plugin below defines a new Action.

The Sample Plugin: Hi!

In spirit with the venerable convention of providing introductory examples that speak greetings, this plugin spells out the phrase "Hi!" with atoms. It is 132 lines long. Many of the lines are comments, and a good portion of the remaining lines compensate for the lack of any predefined fonts to use when making letters out of atoms. The plugin is included in the source as Plugins/Hi.cpp, and we include the entire source file here. You may find this code easier to read if you first read about the coding conventions used. You may also want to read comments in the header files that are included.
// This file is in the public domain.  Tim Freeman 23 Feb 2000.

#include "ActionConfiguration.h"
#include "TypedFactory.h"
#include "Action.h"
#include "FactoryTable.h"
#include "MemoryUtil.h"
#include "String.h"
#include "SP.h"
#include "Factory.h"
#include "Float.h"
#include "Dynavec.h"
#include "Vec3.h"
#include "TopLevel.h"
#include "SceneGraph.h"
#include "LinkManager.h"
#include "BoringAtom.h"

namespace {
  // This sample plugin defines a command that inserts "Hi!" into the scene,
  // spelling out the letters with the default DesignAtom.
  
  // Dot represents one atom in the figure we're going to add.
  struct Dot {
    // x and y coordinates of the atom, in an arbitrary coordinate system.
    int x, y;
    // The index in dotArray of the atom we want to link this atom to, or
    // -1 if none.  We interpret hiArray from the beginning, so linkTo had
    // better be less than or equal than the index of this dot.
    int linkTo;
  };
  // Here's an ASCII drawing of figure we're going to add:
  //
  //y=4  8   10   14   18
  //y=3  7   9         17
  //y=2  2 3 4    13   16
  //y=1  1   5    12
  //y=0  0   6    11   15
  //
  //   x=0 1 2  3 4  5 6
  //
  // And here it is as a sequence of Dot's:
  const Dot hiArray [] = {
    {0, 0, -1}, // 0
    {0, 1, 0},  // 1
    {0, 2, 1},  // 2
    {1, 2, 2},  // 3
    {2, 2, 3},  // 4
    {2, 1, 4},  // 5
    {2, 0, 5},  // 6
    {0, 3, 2},  // 7
    {0, 4, 7},  // 8
    {2, 3, 4},  // 9
    {2, 4, 9},  // 10
    {4, 0, -1}, // 11
    {4, 1, 11}, // 12
    {4, 2, 12}, // 13
    {4, 4, -1}, // 14
    {6, 0, -1}, // 15
    {6, 2, -1}, // 16
    {6, 3, 16}, // 17
    {6, 4, 17}};// 18
  // Multiply all dimensions by this much.  1.4 Angstroms is a plausible bond
  // length between two Carbon atoms.
  const Float bondLength=1.4;
  class Hi
    : public TypedFactory <ActionConfiguration, Action>
  {
  public:
    // Give the factory (that is, the command) a name so we can look it up
    // later.
    Hi ()
      : TypedFactory <ActionConfiguration, Action> ("Hi")
    {}
    // Specify the default configuration for this command.  Nothing interesting
    // here.
    SP<ActionConfiguration> typedDefaultConfiguration () const {
      return NEW (ActionConfiguration ());
    }
    SP<Action> makeIt (ActionConfiguration *conf) const {
      // Get the TopLevel so we can get the SceneGraph from it.
      SP<TopLevel> const top = conf->getTopLevel ();
      // Get the SceneGraph so we can add atoms to it.
      SP<SceneGraph> const sg = top->getSceneGraph ();
      // Get the LinkManager so we can create links between the atoms. 
      SP<LinkManager> const lm = sg->getLinkManager ();
      // Get the factory that makes DesignAtom's.
      SP<Factory> const factory = FactoryTable::load ("Atom", "DesignAtom");
      // Make one.  We only need one DesignAtom because the position is stored
      // outside of the DesignAtom itself.  DesignAtom has no header file and
      // it is a subclass of BoringAtom which does have a header file, so da is
      // a BoringAtom.
      SP<BoringAtom> const da =
	dynamic_cast <BoringAtom *>
	(&*factory->makeIt (factory->defaultConfiguration ()));
      assert (da);
      // We'll hold the state in the plausibleState vector.
      Dynavec <Float> plausibleState;
      // Put a plausible state into the plausibleState vector.  For
      // BoringAtom's, this state has zero position and zero velocity.
      da->plausibleState (plausibleState);
      // A list of the indices of the atoms we've added, so we can link them
      // together properly.
      Dynavec <int> atomIndices;
      // Vectors for the x and y coordinates 
      const Vec3 dx = Vec3 (bondLength, 0, 0);
      const Vec3 dy = Vec3 (0, bondLength, 0);
      for (int i = 0; i < sizeof (hiArray) / sizeof (Dot); i++) {
	const Dot &d = hiArray [i];
	BoringAtom::setCenter (&*plausibleState, d.x*dx+d.y*dy);
	// Add the new atom, remembering its index.
	const int index = sg->addObject (da, plausibleState);
	// Save the index so we can make links later.
	atomIndices.push (index);
	if (-1 != d.linkTo) {
	  // Add the link.  An individual link is a directed edge, but bonds
	  // between atoms are undirected, so we have to do it both ways.
	  lm->addNewLink (index, atomIndices [d.linkTo]);
	  lm->addNewLink (atomIndices [d.linkTo], index);
	}
      }
      // Generate the return value.
      SP<Action> result = NEW (Action ());
      // Give a success message.
      result->setMessage ("Hi!");
      return result;
    }
  };
  // Register this factory as an Action.
  bool useless = (FactoryTable::store ("Action", NEW (Hi ())), true);
}
To compile the sample plugin, give this command:
g++ Hi.cpp -I/usr/include/fungimol -fpic -shared -o Hi.so
The -fpic -shared is required to create a loadable object file, and the -I/usr/include/fungimol is required for the #include's at the beginning of the plugin.

Before running the new Action you must first load the plugin. The command for loading plugins is not bound to any key, so you must run it with the command for executing any command, which is bound to meta-semicolon. If Hi.so is in Fungimol's current directory, the dialogue in the text window is something like the following, except the number "21" may be different depending on your version of Fungimol:

The configuration has these read/write fields:
Number  Field name          Field type  Field value
1       Command to execute  Action      DoNothing
Give the number of a field to edit, or press 0 to exit: 1
This slot implements the protocol Action.
The currently chosen factory is DoNothing.
Do you want to change the factory and reset to the new factory's
default configuration? y
The available factories are:
Number  Name
...
21      LoadPlugin
...
What is the number of the factory you want? 21
Editing the configuration...
The configuration has these read/write fields:
Number  Field name               Field type  Field value
1       Shared object file name  String      Unspecified.so
Give the number of a field to edit, or press 0 to exit: 1
The old value of the slot is Unspecified.so.
What do you want the new value to be? ./Hi.so
The configuration has these read/write fields:
Number  Field name               Field type  Field value
1       Shared object file name  String      ./Hi.so
Give the number of a field to edit, or press 0 to exit: 0
The configuration has these read/write fields:
Number  Field name          Field type  Field value
1       Command to execute  Action      LoadPlugin
Give the number of a field to edit, or press 0 to exit: 0
Note that the path name had to be specified as ./Hi.so. The dynamic linker did not accept a simple Hi.so, probably because I did not have . on my LD_LIBRARY_PATH.

Next, we will run the plugin. First, delete the default elements in the scene graph, as described in the basic editing section of the tutorial. Then, use meta-semicolon to invoke the new plugin:

The configuration has these read/write fields:
Number  Field name          Field type  Field value
1       Command to execute  Action      LoadPlugin
Give the number of a field to edit, or press 0 to exit: 1
This slot implements the protocol Action.
The currently chosen factory is LoadPlugin.
Do you want to change the factory and reset to the new factory's
default configuration? y
The available factories are:
Number  Name
...
19      Hi
...
What is the number of the factory you want? 19
Editing the configuration...
The configuration has no fields that can be changed.
Nothing to edit, so we're done.
The configuration has these read/write fields:
Number  Field name          Field type  Field value
1       Command to execute  Action      Hi
Give the number of a field to edit, or press 0 to exit: 0
Hi!
Note that the success message Hi was printed immedately after the command was run. If time is stopped, the result looks like this:
If time was started, the figure will immediately rearrange itself to look like this:
To bind the new command to a key, we need to edit the configuration of the keyboard dispatcher. The keyboard command for editing the top-level configuration is "c", and then the following grievously long terminal dialogue can happen to bind the "Hi!" Action to the keyboard key "h":
The configuration has these read/write fields:
Number  Field name           Field type        Field value
1       Scene Generator      SceneLoader       VectorSceneLoader
2       Scene Graph          SceneGraph        SubsetStorageScene
3       Timesteps Per Frame  Positive Integer  5
4       Top Level            TopLevel          XTopLevel
Give the number of a field to edit, or press 0 to exit: 4
This slot implements the protocol TopLevel.
The currently chosen factory is XTopLevel.
Do you want to change the factory and reset to the new factory's
default configuration? n
Editing the configuration...
The configuration has these read/write fields:
Number  Field name                          Field type             Field value
1       Busy device bug threshhold          Positive Integer       100
2       Input Devices                       Vector of InputDevice  Vector of length 2
3       Object Drawer                       ObjectDrawer           XObjectDrawer
4       Sleep between polls (microseconds)  Positive Integer       50000
5       Use MIT Shared Memory Extension     Boolean                true
Give the number of a field to edit, or press 0 to exit: 2
The vector currently has these contents: 
Position  Value
1         Keyboard and Mouse
2         Space Orb Configuration
These options are available:
f,0            Finish editing this vector
e              Edit an element of the vector
<slot number>  Edit the given slot of the vector
p              Print out the vector again
Which of these do you want to do? 1
This slot implements the protocol InputDevice.
The currently chosen factory is Useless (this string should not be seen).
Editing the configuration...
The configuration has these read/write fields:
Number  Field name                     Field type           Field value
1       Key and Button Dispatch Table  KeyboardMouseAction  KeyboardMouseDispatcher
Give the number of a field to edit, or press 0 to exit: 1
This slot implements the protocol KeyboardMouseAction.
The currently chosen factory is KeyboardMouseDispatcher.
Do you want to change the factory and reset to the new factory's
default configuration? n
Editing the configuration...
The configuration has these read/write fields:
Number  Field name                         Field type            Field value
1       Bindings when Control is down      Vector of KeyBinding  Vector of length 9
2       Bindings when Meta or Alt is down  Vector of KeyBinding  Vector of length 1
3       Bindings when Shift is down        Vector of KeyBinding  Vector of length 7
4       Bindings with no shift keys        Vector of KeyBinding  Vector of length 16
Give the number of a field to edit, or press 0 to exit: 4
The default value for new elements has the type
KeyBinding and the value Mouse motion is bound to DoNothing.
The vector currently has these contents: 
Position  Value
1         b is bound to InferBonds
...
16        i is bound to SelectionInvisible
These options are available:
f,0            Finish editing this vector
d              edit the Default value
i              Insert a copy of the default value into the vector
l              deLete an element from the vector
e              Edit an element of the vector
<slot number>  Edit the given slot of the vector
p              Print out the vector again
Which of these do you want to do? i
Note that the vector starts with element 1.  Enter 0 if 
you have changed your mind and do not want to insert 
anything.
Where you want the new default element to appear? 17
The default value for new elements has the type
KeyBinding and the value Mouse motion is bound to DoNothing.
The vector currently has these contents: 
Position  Value
1         b is bound to InferBonds
...
16        i is bound to SelectionInvisible
17        Mouse motion is bound to DoNothing
These options are available:
f,0            Finish editing this vector
d              edit the Default value
i              Insert a copy of the default value into the vector
l              deLete an element from the vector
e              Edit an element of the vector
<slot number>  Edit the given slot of the vector
p              Print out the vector again
Which of these do you want to do? 17
This slot implements the protocol KeyBinding.
The currently chosen factory is KeyBinding.
Editing the configuration...
The configuration has these read/write fields:
Number  Field name         Field type           Field value
1       Key                Button               Mouse motion
2       What it should do  KeyboardMouseAction  DoNothing
Give the number of a field to edit, or press 0 to exit: 1
The old value of the button is Mouse motion.
What do you want the new value to be? h
The configuration has these read/write fields:
Number  Field name         Field type           Field value
1       Key                Button               h
2       What it should do  KeyboardMouseAction  DoNothing
Give the number of a field to edit, or press 0 to exit: 2
This slot implements the protocol KeyboardMouseAction.
The currently chosen factory is DoNothing.
Do you want to change the factory and reset to the new factory's
default configuration? y
The available factories are:
Number  Name
...
24      Hi
...
What is the number of the factory you want? 24
Editing the configuration...
The configuration has no fields that can be changed.
Nothing to edit, so we're done.
The configuration has these read/write fields:
Number  Field name         Field type           Field value
1       Key                Button               h
2       What it should do  KeyboardMouseAction  Hi
Give the number of a field to edit, or press 0 to exit: 0
The default value for new elements has the type
KeyBinding and the value Mouse motion is bound to DoNothing.
The vector currently has these contents: 
Position  Value
...
17        h is bound to Hi
These options are available:
f,0            Finish editing this vector
d              edit the Default value
i              Insert a copy of the default value into the vector
l              deLete an element from the vector
e              Edit an element of the vector
<slot number>  Edit the given slot of the vector
p              Print out the vector again
Which of these do you want to do? 0
The configuration has these read/write fields:
Number  Field name                         Field type            Field value
1       Bindings when Control is down      Vector of KeyBinding  Vector of length 9
2       Bindings when Meta or Alt is down  Vector of KeyBinding  Vector of length 1
3       Bindings when Shift is down        Vector of KeyBinding  Vector of length 7
4       Bindings with no shift keys        Vector of KeyBinding  Vector of length 17
Give the number of a field to edit, or press 0 to exit: 0
The configuration has these read/write fields:
Number  Field name                     Field type           Field value
1       Key and Button Dispatch Table  KeyboardMouseAction  KeyboardMouseDispatcher
Give the number of a field to edit, or press 0 to exit: 0
The vector currently has these contents: 
Position  Value
1         Keyboard and Mouse
2         Space Orb Configuration
These options are available:
f,0            Finish editing this vector
e              Edit an element of the vector
<slot number>  Edit the given slot of the vector
p              Print out the vector again
Which of these do you want to do? 0
The configuration has these read/write fields:
Number  Field name                          Field type             Field value
1       Busy device bug threshhold          Positive Integer       100
2       Input Devices                       Vector of InputDevice  Vector of length 2
3       Object Drawer                       ObjectDrawer           XObjectDrawer
4       Sleep between polls (microseconds)  Positive Integer       50000
5       Use MIT Shared Memory Extension     Boolean                true
Give the number of a field to edit, or press 0 to exit: 0
The configuration has these read/write fields:
Number  Field name           Field type        Field value
1       Scene Generator      SceneLoader       VectorSceneLoader
2       Scene Graph          SceneGraph        SubsetStorageScene
3       Timesteps Per Frame  Positive Integer  5
4       Top Level            TopLevel          XTopLevel
Give the number of a field to edit, or press 0 to exit: 0
After going through all this, you can delete the original "Hi!" from the scene, and then press "h" to get a new one. It ought to be possible to write a plugin that binds keys and can significantly shorten this dialogue. I have not done this since I have been editing my default keybindings directly into the device driver plugin at Input/KeyboardMouse.cpp.
Copyright 2003 Tim Freeman <tim@fungible.com>