SDK Basics

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

COM

The nexus SDK is implemented in COM, but as a C++ developer you mostly don't need to know any of that. Whew! That's a good thing because while COM is relatively simple, dealing with raw COM objects directly can be complicated. Fortunately the C++ wrapper classes hide most of the complexity. There are, however, a few concepts that are important to understand at a high level.

Interfaces

COM distinguishes between objects and interfaces. An object is a specific thing, like an image or a item. An interface is a set of methods for accessing or operating on that thing. COM objects are polymorphic, which is to say that a single object can have more than one interface. Such an object is said to present those interfaces and clients who want to use them query for the interface, which is given by a unique ID called a GUID.

The name of an interface comes from the name of a vTable struct starting with the ILx prefix. Some are very specific, like the Item Interface (ILxItem), and some are more generic and are shared by multiple object types. Examples of the latter would be the Attributes Interface (ILxAttributes), or the Value Interface (ILxValue).

ILxUnknownID

The methods that you implement may get arguments of the ILxUnknownID datatype. This is the type for a general COM object before the interfaces are known, but it can be transformed into specific interfaces performing a query. It's a little like dynamic casting in C++, except that the types are only known at runtime. The query is done automatically by initializing a localization wrapper class.

Likewise some of the service methods you will be calling also take ILxUnknownID arguments. Fortunately the wrapper classes can cast to that type implicitly so there is very little that needs to be done on your side to support this. Just know that you can pass a wrapper object and it will be converted to the unknown type if necessary.

Reference Counting

COM objects manage their existence by reference counting. Again, this is mostly handled for you by the wrapper classes. When you initialize a wrapper with an ILxUnknownID object a reference is added, and when the wrapper changes or goes out of scope the reference is released.

User Wrappers

Objects in the SDK are mostly accessed through two kinds of wrappers.

Services

Services are interfaces provided by nexus to access internal state. The wrappers are exceptionally easy to use; you just declare them and they are ready to go. The constructor does all the work of hooking the wrapper object to the real interface.

        CLxUser_SelectionService  sel_srv;

        now = sel_srv.GetTime ();

Localized Objects

Localized objects come into the plug-in from nexus, often as ILxUnknownID argument pointers. These need to be localized by initializing a class wrapper. For example, this method is passed an unknown object that has a StringTag Interface, which is then localized using the interface wrapper.

method (
        ILxUnknownID             thing)
{
        CLxUser_StringTag        tags (thing);

        n = tags.Count ();
        ...
}

The wrapper can also be initialized using the set() method, and its return value or the test() method can be used to determine if the initialization succeeded. (Lowercase methods operate on the wrapper, uppercase on the actual object.)

        if (!tags.set (thing))
                return LXe_NOTFOUND;

        ...

        if (tags.test ())
                tags.Get (tag_id, &value);

Using set() increments a reference to the object which is decremented when the wrapper releases the object, such as when it goes out of scope. There are cases when it's useful to "steal" the reference that's already been incremented by a previous call. For example, using raw allocation methods -- the ones that take an LXtObjectID as a ppvObj indirect argument -- add a reference to the returned object. The take() method transfers ownership of that reference to the wrapper.

        LXtObjectID               obj;
        LxResult                  rc;

        rc = source.Allocate (&obj);
        if (LXx_FAIL (rc))
                return false;

        return wrap.take (obj);

Export Wrappers

It's also necessary to export objects from the plug-in to nexus. In this case the plug-in creates objects using C++ and wraps them in COM interfaces. When nexus (or sometimes other plug-ins) use the COM methods those call through to the C++ methods for the base class. The C++ class starts by inheriting from the CLxImpl_ classes for the interfaces that it wants to present, and it then creates bodies for some subset of the inherited methods. Anything not implemented will have a default code body, if possible.

For example, a tool -- which needs to present Tool and ToolModel Interfaces -- could start like this:

class CMyClass :
        public CLxImpl_Tool,
        public CLxImpl_ToolModel
{
    public:
        void      tool_Evaluate (ILxUnknownID vts)                               LXx_OVERRIDE;
        void      tmod_Draw (ILxUnknownID vts, ILxUnknownID stroke, int flags)   LXx_OVERRIDE;
};

Each CLxImpl class has a unique prefix for all its methods, assuring that there is never a conflict as result of multiple inheritance.

Once the class implementation is created it's necessary to create the actual wrapper. This is called a polymorph -- because COM object are polymorphic -- and it manages the actual COM incarnations of your class instances. The polymorph must exist as long as there are COM instances. The code to create the polymorph is as follows:

        CLxGenericPolymorph     *srv;

        srv = new CLxPolymorph<CMyClass>;
        srv->AddInterface (new CLxIfc_Tool     <CMyClass>);
        srv->AddInterface (new CLxIfc_ToolModel<CMyClass>);

First we create a polymorph object using a template, which allows it to find the methods of your class. To that polymorph we add interfaces, which are the actual COM interfaces that will be presented by your COM object. Each CLxIfc_ template object serves as translator from the general COM API to the specifics of your class implementation. This polymorph object, once created, is capable of spawning many instances of your class. There are a few ways to do that.

Servers

In the case of the tool example, the class implements a server. This means that it gets installed into the module during initialization as one of the named servers that the module can provide. The class of the object is given by the first interface, in this case Tool, which is what we want. The server should be given a name globally unique for all servers of this class by calling this function in initialize().

        lx::AddServer ("serverName", srv);

Instances

Some exported plug-in objects are created on demand, not as servers but as instances. In that case the best approach is to use a spawner. This example uses the CLxSpawnerCreate class to make a self-contained static method, but at the small cost of a name lookup for each spawn.

class CMySurfaceBin :
        public CLxImpl_SurfaceBin,
        public CLxImpl_TableauSurface,
        public CLxImpl_StringTags
{
    public:
                static CMySurfaceBin *
        Spawn (
                void              *ppvObj)
        {
                CLxSpawnerCreate<CMySurfaceBin> sp ("mySurfBin");

                if (sp.created) {
                        sp.AddInterface (new CLxIfc_SurfaceBin    <CMySurfaceBin>);
                        sp.AddInterface (new CLxIfc_TableauSurface<CMySurfaceBin>);
                        sp.AddInterface (new CLxIfc_StringTags    <CMySurfaceBin>);
                }

                return sp.spawn->Alloc (ppvObj);
        }

        ...
};

Singletons

Sometimes it would be overkill to create a spawner because you only ever need one instance of a given object, like a global listener for example. In that case it's best to inherit from CLxSingletonPolymorph. This encapsulates the polymorphic COM management into the single instance of the object. You just have to remember to add the special method macro and to add your interfaces in the constructor.

class CMySelectionTracker :
        public CLxImpl_SelectionListener,
        public CLxSingletonPolymorph
{
    public:
       LXxSINGLETON_METHOD

       CMySelectionTracker ()
       {
               AddInterface (new CLxIfc_SelectionListener<CMySelectionTracker>);
       }

       ...
};

Metaclasses

Metaclasses are new in MODO 10, and are intended to make exporting servers and objects much easier. These are custom classes and templates designed to hide much of the boilerplate that's otherwise required to export objects. Because these are hand-crafted for each object type only a few server types and interfaces are currently supported. More will be added in future versions, but if there is a metaclass for the type of server you want to write, it will be the easiest way to approach the task.