Hello World

This “Hello World!” tutorial will cover the absolute minimum steps required to create a plug-in and get feedback inside the application.

Preparation

Build Environment Setup

You need to do three things to be ready to start coding a plug-in.

# Include headers. The lxsdk header directory needs to be on the compiler search path. # Build common lib. The common code library archive needs to be built using your compiler and environment. You will link your plug-in to this library to get base implementations for the shared classes and objects. # Empty .cpp file ready to compile and link.

You should be able to get started with this using the provided IDE projects for VC and XCode.

Decide on Your Server Type

A plug-in can’t do anything unless it provides one of the proscribed server types for nexus to access. Is this a command, an item type, a tool? Perhaps the plug-in will have multiple servers that all work together. This is something for you to decide.

For the purpose of this tutorial we’ll be making an image saver, which is one of the simplest types.

Writing Code

Headers

The first part of your plug-in source file will include the required Headers . There are two types of headers you will typically want to use:

  • lx_<system>.hpp – the user header for any given nexus system will have an L-X-underscore prefix and be of the hpp type. Check the documentation to see which interfaces are defined as part of a given system.

  • lxu_<name>.hpp – utility headers have an L-X-U-underscore prefix, and contain helper classes of various kinds.

1
2
3
 #include <lx_io.hpp>
 #include <lx_image.hpp>
 #include <lx_log.hpp>

The Server Class

The heart of any plug-in is a C++ class that you write to implement a server interface. The interface is the set of standard methods that nexus uses to integrate your server into the application. This is done by inheriting from multiple implementation classes. In this case we’re going to inherit from the Saver implementation:

1
2
3
4
5
 class CHelloWorldSaver : public CLxImpl_Saver
 {
     public:
         ...
 };

The Saver super-class defines two methods: sav_Verify() and ‘’sav_Save()’’. The sav prefix is unique to the Saver implementation class, and allows multiple implementations with the same or similar methods to be inherited by the same server. We’re going to implement the Save() method, adding this line to our class definition above:

1
         LxResult   sav_Save (ILxUnknownID source, const char *filename, ILxUnknownID monitor)  LXx_OVERRIDE;

The override keyword is optional but very useful. It declares to the compiler that you intend for your method to be derived from an identical method in a super-class. That means that if the method in the implementation class changes the compiler will throw an error. If you don’t use the override keyword (or if your compiler doesn’t support it – it’s not standard) then you get no error, but your method will never be called.

Server Methods

To flesh out the save method we’ll declare a code body with the same arguments as above.

1
2
3
4
5
6
7
8
9
         LxResult
 CHelloWorldSaver::sav_Save (
         ILxUnknownID            source,
         const char             *filename,
         ILxUnknownID            monitor)
 {
         ...
         return LXe_OK;
 }

The source is the object to be saved, in this case an image. This is an ILxUnknownID pointer type – a general handle to a COM object – and can be queried for any number of Image Object interfaces. The filename is the full path of the file to be written in platform-specific format. The monitor is another object used for tracking the progress of the save and is discussed later.

The first thing we want to do is query the image object for an Image Interface that will allow us to read the size for the “hello world” message. This is done simply by declaring a localized C++ image user class and initializing it to the source object.

1
         CLxUser_Image           image (source);

‘’CLxUser_’’ classes are wrappers that allow COM objects to be accessed through common C++ syntax, with C++-friendly APIs. Once initialized with a COM object, they can be used like any other C++ object. In our case we’re going to read the size of the image.

1
2
         unsigned                w, h;
         image.Size (&w, &h);

Instead of writing an image file as would be expected for a plug-in of this type we’re going to generate test output just to alert the world that we exist. It’s possible to do this in more complex ways but we’ll stick with the easiest, using the log service.

1
         CLxUser_LogService      log_S;

Services are the inverse of servers – they are interfaces exported by nexus to plug-in, allowing plug-in to access and manipulate the internal state of the nexus system. In this case the state of the log system. Unlike other user classes, they don’t need to be initialized. Instead they are automatically initialized as soon as they are declared.

The simplest form of debug output just writes to the debug log or, inside the VC debugger, the output window. This is the same as debug output from nexus internal systems and respects the debug level specified on the command line.

1
         log_S.DebugOut (LXi_DBLOG_NORMAL, "Hello world: image %s is %d x %d\n", filename, w, h);

Server Tags

Server objects also support a tags interface. Server Tags are string/string pairs that define the attributes of servers. In the case of savers this is the type of object that’s saved, the file extension of the format, and the user name. These can be added easily by defining a descInfo array in the server class:

1
         static LXtTagInfoDesc   descInfo[];

And declaring the contents of the array statically in the module. The array is terminated with a null name:

1
2
3
4
5
6
 LXtTagInfoDesc  CHelloWorldSaver::descInfo[] = {
         { LXsSAV_OUTCLASS,     LXa_IMAGE       },
         { LXsSAV_DOSTYPE,      "HEL"           },
         { LXsSRV_USERNAME,     "Hello World"   },
         { 0 }
 };

Initialization

Plug-ins need to declare the servers they export, and the interfaces those servers support. This is done with the initialize() function which is called from the common lib code as the plug-in is loaded. This is largely boilerplate, but with the classes and interfaces varying depending on the plug-in. The first interface defined is also the type of the server. Note that the name of the server is an internal name string, so it should start with lowercase and cannot contain spaces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
         void
 initialize ()
 {
         CLxGenericPolymorph     *srv;

         srv = new CLxPolymorph<CHelloWorldSaver>;
         srv->AddInterface (new CLxIfc_Saver     <CHelloWorldSaver>);
         srv->AddInterface (new CLxIfc_StaticDesc<CHelloWorldSaver>);
         lx::AddServer ("helloWorld", srv);
 }

Results

Final Code

The code above is the absolutely minimal – and minimalistic – plug-in code that’s required. There are a number of common enhancements that make things easier to manage as your project grows larger.

  • Persistent service objects. There is a small but non-zero overhead associated with looking up a global service. It’s better when possible to move service objects to be class members so they can persist and be reused.

  • Class initialize method. Rather than placing the initialization of a server into the global initialize() function, it’s better to create a static method on the class itself and call that from the global function. That way the interface initialization is embodied in the class that it implements.

  • Namespaces. Namespaces are a good way to organize code, and allow reuse of common code without a lot of text substitution.

  • In-line function bodies. When possible, writing the function declaration once reduces the chance of typos or other errors in repeated declarations.

Here is the final code, putting together the required elements and adding the optional enhancements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 #include <lx_io.hpp>
 #include <lx_image.hpp>
 #include <lx_log.hpp>

         namespace HelloWorld {

 class CSaver : public CLxImpl_Saver
 {
     public:
         static LXtTagInfoDesc   descInfo[];
         CLxUser_LogService      log_S;

                 static void
         initialize ()
         {
                 CLxGenericPolymorph     *srv;

                 srv = new CLxPolymorph<CSaver>;
                 srv->AddInterface (new CLxIfc_Saver     <CSaver>);
                 srv->AddInterface (new CLxIfc_StaticDesc<CSaver>);
                 lx::AddServer ("helloWorld", srv);
         }

                 LxResult
         sav_Save (
                 ILxUnknownID            source,
                 const char             *filename,
                 ILxUnknownID            monitor)     LXx_OVERRIDE
         {
                 CLxUser_Image           image (source);
                 unsigned                w, h;

                 image.Size (&w, &h);
                 log_S.DebugOut (LXi_DBLOG_NORMAL, "Hello world: image %s is %d x %d\n", filename, w, h);
                 return LXe_OK;
         }
 };

 LXtTagInfoDesc  CSaver::descInfo[] = {
         { LXsSAV_OUTCLASS,     LXa_IMAGE       },
         { LXsSAV_DOSTYPE,      "HEL"           },
         { LXsSRV_USERNAME,     "Hello World"   },
         { 0 }
 };

         }; // end namespace


         void
 initialize ()
 {
         HelloWorld::CSaver::initialize ();
 }

Testing

To test your new plug-in you should first go to System > Add plug-in and select the *.lx file created by the linker. You may or may not have to restart – depends on the plug-in type. For savers you should be OK. Render, click ‘’Save Image’’. In the format choices pick “Hello World” and OK. Your plug-in should load and you should see its output in the debug log. Note that in release builds all debug output is normally suppressed. You have to add “-debug:normal” to the command line in order to see your debug print.

NOTE: In the release 601 the output doesn’t appear in the debugger. Instead it goes to stdout or the dblog you specify on the command line. That will be fixed for SP1. See Writing to the Event Log for how to use the event log viewport.