About Metaclasses

From The Foundry MODO SDK wiki
Jump to: navigation, search

The MODO 10 SDK has introduced the concept of metaclasses. A metaclass is an object that serves as the factory for a whole set of related objects of the same type. For those familiar with the SDK a Polymorph or Spawner is a type of metaclass, but at a very low level. They require a lot of manual configuration to get useful SDK objects. The metaclasses in MODO 10 are specifically designed to make it easy to create plug-ins by automating a lot of the boilerplate that makes the SDK hard to use.

Base Class

Metaclasses are composed of a base class and a template class. They are specific to the type of server or object being created, and the names of the classes derive from the interface for that object type. For example to create a Package server (ILxPackage interface) we'd use the CLxPackage base class and the CLxMeta_Package template class. For this example let's imagine a hypothetical server type Foo which manages a list of strings. The first part of the metaclass would be the CLxFoo base class:

/*
 * Base class for Foo metaclass. (NOTE: not a real server type!)
 */
class CLxFoo
{
    public:
        /*
         * Override method to refresh the string list. Add strings one by one using add_entry().
         */
        virtual void              get_list () { }
        void                      add_entry (const char *);
 
        class pv_Foo *pv;
};

This is a pure C++ class that contains virtual and non-virtual methods. The virtual methods can be overridden by sub-classes implemented by the plug-in developer. Those methods implement the core functionality of their plug-in. The non-virtual methods are helpers that maintain the state of the object and allow it to implement the COM methods of the interface. In this case the developer would write their own get_list() method to populate the list by calling add_entry() for each one. The metaclass would then implement whatever methods were expected for COM using that cached list. The 'pv' pointer contains the state of the base class and is effectively private.

Template Class

The second part of the metaclass is the template. This takes the developer's subclass and defines a CLxMeta object using that. In reality this is split into two classes -- the core and the template -- but the core is there just to serve as a concrete class for the template to derive from. In practice the meta object is logically one type.

/*
 * Core class for Foo metaclass template. (NOTE: not a real server type!)
 */
class CLxMeta_Foo_Core :
		public CLxMeta
{
    public:
	 CLxMeta_Foo_Core (const char *srvName);
	~CLxMeta_Foo_Core ();
	class pv_Meta_Foo	*pv;
	virtual CLxFoo *	 new_inst () = 0;
	void *			 alloc () LXx_OVERRIDE;
 
	/*
	 * Add a server tag.
	 */
	void		add_tag (const char *tag, const char *val);
 
	/*
	 * By default the list is static. It can be made dynamic in which case it needs
	 * to be invalidated when it changes.
	 */
	void		set_dynamic ();
	void		invalidate ();
};
 
/*
 * Template class for Foo metaclass. (NOTE: not a real server type!)
 */
template <class T>
class CLxMeta_Foo:
		public CLxMeta_Foo_Core
{
    public:
	CLxMeta_Foo (const char *srvName) : CLxMeta_Foo_Core (srvName) { }
 
		CLxFoo *
	new_inst ()
	{
		return new T;
	}
};

It looks like there's a lot to this, but there isn't. The only thing that the developer needs to worry about is the template class, and the commented methods it inherits from the core class. Those methods -- add_tag(), set_dynamic(), and invalidate() in this case -- are used to configure the common attributes of all the objects generated by this metaclass. For example since this is a server add_tag() adds server tags such as user name or licensing info to the server object.

Example Usage

The point of all this is to hide the complexity of implementing servers and other SDK objects from the plug-in developer, and to make their code simpler and easier to maintain. As an example, here's a a simple Foo server that just makes a list of three items. (There is no such server type as Foo -- this is a contrived and too-simple example.)

class MyFoo : public CLxFoo
{
    public:
        void              get_list ()       LXx_OVERRIDE
        {
                add_entry ("One");
                add_entry ("Two");
                add_entry ("Three");
        }
};
 
static CLxMeta_Foo<MyFoo>  foo_meta ("myFooServerName");

The static foo_meta object represents a Foo server with the name "myFooServerName" specialized with the client's MyFoo subclass. The final step is initialization, which requires adding this metaclass as a child of a root. This will typically be done in the plug-in initialize() method.

static CLxMetaRoot  root_meta;
 
        void
initialize ()
{
        foo_meta.add_tag (LXsSRV_USERNAME, "Foo Example");
 
        root_meta.add (&foo_meta);
        root_meta.initialize ();
}

CLxMeta objects are linked into trees, with the shared add() method used to string nodes together. Here the Foo metaclass is made a child of root, and then the root is initialized to install any servers or other objects it contains. The initialize() function is also used to set any state on the metaclasses, such as adding the username tag to the Foo server.

Tree Searches

In more complex examples there may be multiple metaclasses that work together. For example a Package and a Modifier that both share the same Channel definition, or a Package that has extensions for optional interfaces. In these cases each metaclass will search the tree that it's part of in order to find related metaclasses. The search starts by going down, looking for metaclasses that are its children in the tree. If a suitable metaclass isn't found there it will then start to look higher in the tree. By placing related objects close in the tree the developer can control the results of the search. Regular CLxMeta objects can also be created and used for grouping.