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.
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 LXtGUID (index).
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’’’).
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.
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.
Objects in the SDK are mostly accessed through two kinds of wrappers.
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 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);
|
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;
|
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 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.
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);
|
Some exported plug-in objects are created on demand, not as servers but as instances. In that case the best approach is to Using 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);
}
|
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>);
}
|