Compatibility

LightWave

Common Elements Classes Table of Contents

Compatibility

This page discusses four different varieties of plug-in compatibility.

  • Backward compatibility is the ability to use the same code, including the latest SDK headers and source, with both current and older versions of LightWave.
  • Forward compatibility deals with writing code that won't break in future versions of LightWave.
  • Compatibility across platforms allows you to use one code base to support more than one operating system or CPU type.
  • Product compatibility means being able to use the same code in products that may be derived from LightWave or share some of its plug-in API (application programming interface).

Of these, backward compatibility is likely to be the greatest concern. Forward compatibility and compatibility across platforms are largely automatic for the plug-in author, as long as he or she writes to the specification in this documentation and uses the facilities provided by the SDK rather than platform-specific code. The requirements for product compatibility are difficult to predict at the moment, since no concrete examples of the need for it exist yet, but globals for determining the product and version number are available, and we'll introduce them here.

This discussion necessarily delves into some grubby details, so it'll be easier to follow if you're already familiar with the SDK. But if you're not, links to the information you need are provided.

SDK Versions

The plug-in SDK continues to evolve. Changes to it will appear with each release of LightWave. The SDK itself isn't versioned, however. Each class and global has its own version number. Your plug-in remains both forward and backward compatible with SDK changes by using, for classes, the version number passed as the first argument to your activation function, and for globals, increments embedded in the global's service name string.

The SDK defines symbolic names for the versions of each class. The version number for shaders, for example, is LWSHADER_VERSION, which as of this writing is defined as 4. Your shader's activation function will usually compare this to the version number passed as the first argument to the function and return AFUNC_BADVERSION if the two numbers don't match.

   XCALL_( int )
   MyActivate( long version, GlobalFunc *global, LWShaderHandler *local,
      void *serverData );
   {
      if ( version != LWSHADER_VERSION )
         return AFUNC_BADVERSION;

This ensures that the LWShaderHandler being passed to you in the local argument is the same as the LWShaderHandler in your copy of the lwshader.h SDK header file. The lwshader.h header contains the line

   #define LWSHADER_VERSION 4

as well as the definition of the structures used by shaders, including LWShaderHandler and LWShaderAccess. When you compile your plug-in using this header, the compiler renders the version checking code in your activation function as

   if ( version != 4 ) ...

You test for version 4 because that's the version of the shader API defined in your copy of the header, and the version of the shader-related structures compiled into your plug-in.

LightWave will call your activation function with every version of LWShaderHandler it supports, until it runs out of versions or one of the calls succeeds. Forward compatibility is therefore automatic, as long as LightWave continues to support version 4 of LWShaderHandler.

But what happens when you update your copy of the SDK headers? LWSHADER_VERSION may have been incremented, yet you want to continue to support LightWave 6.x, which itself supports shaders no later than version 4.

First, your activation function must accept a range of versions. LightWave starts by calling your activation function with the highest version it supports, then with successively lower versions. (The exception is the interface activation for handlers, which for historical reasons starts at 1 and counts up.) Your plug-in is therefore activated with the highest version supported by both the plug-in and the LightWave it's running in.

   XCALL_( int )
   MyActivate( long version, GlobalFunc *global, LWShaderHandler *local,
      void *serverData );
   {
      if ( version > LWSHADER_VERSION || version < 4 )
         return AFUNC_BADVERSION;

Then you have to decide how to handle the different versions of LWShaderHandler and other shader-related structures in the rest of your code. In many cases, changes to the API of a class are incremental. Existing structure members are retained, and new members are appended, making it possible to use the most recent versions of the data structures with previous versions of the API. You just need to remember not to use new members when you've been activated with an older version number.

The documentation includes a history of the changes made to the headers with each revision of LightWave. Look for this in both the changes lists and in sections labeled "History" on the pages for each class. Using this information, you can see how the current data structures differ from those in previous versions.

Globals work in a similar way. The SDK headers define symbolic names for the strings you pass to the global function. LWMESSAGEFUNCS_GLOBAL, for example, is the symbolic name of the messages global.

   LWMessageFuncs *msgf;
   msgf = global( LWMESSAGEFUNCS_GLOBAL, GFUSE_TRANSIENT );
   if ( msgf ) { ...

By using the symbolic name, you ensure that the LWMessageFuncs structure returned by the global function is the same as the LWMessageFuncs defined in your copy of the lwhost.h SDK header.

The name strings underlying the symbols often contain trailing numbers or other incrementing characters. As of this writing, the string for the messages global, for example, is "Info Messages 2". If the LWMessageFuncs structure changes in the future, the new string will most likely be "Info Messages 3", but LightWave will continue to support "Info Messages 2". Again, your plug-in's forward compatibility with future versions of LightWave is automatic.

For backwards compatibility, you can request earlier versions of globals when the most recent version, or the version defined in your copy of the headers, isn't available. As with class-related structures, the data structures for globals in many cases evolve in backward-compatible ways. The LWMessageFuncs structure for the original "Info Messages" global looks like this.

   typedef struct st_LWMessageFuncs {
      void (*info)     (const char *, const char *);
      void (*error)    (const char *, const char *);
      void (*warning)  (const char *, const char *);
   } LWMessageFuncs;

The "Info Messages 2" global adds several functions to the end of the structure.

   typedef struct st_LWMessageFuncs {
      void (*info)     (const char *, const char *);
      void (*error)    (const char *, const char *);
      void (*warning)  (const char *, const char *);
      int  (*okCancel) (const char *ttl, const char *, const char *);
      int  (*yesNo)    (const char *ttl, const char *, const char *);
      int  (*yesNoCan) (const char *ttl, const char *, const char *);
      int  (*yesNoAll) (const char *ttl, const char *, const char *);
   } LWMessageFuncs;

The version 2 definition is backward-compatible with the pointer returned from a request for the original "Info Messages" global, as long as you remember not to use the fields added for "Info Messages 2".

Before LightWave 6.0

The revision of the plug-in API for LightWave 6.0 was the most substantial since the plug-in architecture was first introduced in 1995. Prior to 6.0, API revisions were incremental and had very little effect on existing source code. New globals were made available, and new members were appended to existing class structures. Plug-in authors could take advantage of the new features without changing much of their code and without sacrificing backward compatibility with older versions of LightWave.

This is also true within versions from 6.0 onward. But there is a great divide at 6.0, and the most difficult backward compatibility challenge involves bridging this divide. (This is a problem only for new source code. The situation for existing binaries is quite a bit simpler. With a few exceptions, plug-in binaries built with the pre-6.0 SDK will run in LightWave 6.0 but won't have access to any of the new features. Binaries built with the current SDK will not run in versions of LightWave prior to 6.0.)

With 6.0, the API doubled in size, and in keeping with the complete overhaul of LightWave itself, many familiar API structures were renamed, rearranged or removed. It's no longer possible to write plug-ins to both the current and 5.x APIs using the same code base and a single set of SDK headers. In some cases it simply won't make sense to continue to work within the limitations of the 5.x SDK, particularly as time passes and older versions of LightWave fade from view. But with that caveat in mind, it's still possible to write a single plug-in that uses new SDK features yet runs in LightWave 5.x.

One way to do this is to segregate any code that requires a particular API version. Your activation functions might look like the following.

   XCALL_( static int )
   Activate( long version, GlobalFunc *global, void *local,
      void *serverData )
   {
      unsigned long prodinfo, major;

      prodinfo = ( unsigned long ) global( LWPRODUCTINFO_GLOBAL,
         GFUSE_TRANSIENT );
      major = LWINF_GETMAJOR( prodinfo );
      if ( major < 6 )
         return Activate5( version, global, local, serverData );
      else
         return Activate6( version, global, local, serverData );
   }

The Product Info global is used here to distinguish between major versions of LightWave. Activate5 is the activation function you would have written, had this been an exclusively 5.x plug-in, and Activate6 is the 6.0 version. Activate covers both functions and is the activation function that should be listed in the ServerRecord. Activate5 and Activate6 reside in two different .c files, each of which includes the appropriate version of the headers and contains the version-sensitive callbacks. The code that doesn't depend on the API version can be called from both of these files.

In the 6 SDK, the ServerRecord structure was extended to include a tagInfo member, and the value of the sysVersion member of the ModuleDescriptor structure was incremented. (These structures are defined in lwserver.h and lwmodule.h in the 6.0 SDK, and in splug.h and serv_w.c in the 5.x SDK.) The change in sysVersion prevents plug-ins linked with the 6 SDK library from being loaded by earlier versions of LightWave that don't know about tagInfo. Your 5.x/6 hybrid plug-in must therefore be linked with the 5.x SDK library, and it can't include tagInfo in its ServerRecords.

Product Info and System ID

The Product Info global returns the identity of the host (e.g. LightWave), its major and minor version numbers, and the build number. The System ID global tells you which specific program you're running in (e.g., Layout, Modeler or Screamernet). If your plug-in uses features that only appear in certain versions of LightWave, or in LightWave but not in other programs that may share the LightWave SDK, or in Layout but not Modeler, you can use these two globals so that you can either bracket the affected code or fail gracefully.

Note that this is only necessary when the class version number or the global service name aren't sufficient by themselves to ensure compatibility, as in the 5.x example above. The need also arises in cases where the LightWave programmers, proving they are only human, forget to increment a version number when they make a change to a header.

Returning to our shader example, LightWave 7.0 added four fields to the LWShaderAccess structure without a corresponding change to LWSHADER_VERSION. As the History section of the shader page points out, you'll need to use the Product Info global to ensure that you're running in at least LightWave 7.0 before you try to read or write those new LWShaderAccess fields.

The least convenient but most reliable version indicator is the build number. You may occasionally need this in order to identify minor patches that retain the same major and minor version numbers. Older versions of a given LightWave component will always have smaller build numbers, so that you can reliably use inequalities to test whether the current program is at least as old or new as a specific build. The build numbers for LightWave Layout and Modeler are displayed in their About boxes.

Platforms

LightWave is available on more than one operating system. You can build a version of your plug-in for each of these operating systems and platforms without the use of any platform-specific source code. LightWave supports this by providing services for file I/O and user interface construction (panels, xpanels, requesters, messages, file dialogs, color dialogs, previews, presets and monitors, for example) that hide details specific to each platform. You just need to recompile.

The SDK requires you to define certain preprocessor symbols to distinguish between platforms. The lwdisplay.h header uses these, for example, to selectively compile different versions of the structure returned by the Host Display Info global. Under Windows, your plug-in receives an HWND for the LightWave component's main window, while on the Mac, it receives a WindowPtr. Which one you get will depend on whether you define _WIN32 or _MACOS when you compile.

Although platform independence is usually a good thing, the SDK by no means requires it. Plug-ins aren't exotic objects on any platform. They're shared libraries on the Mac and DLLs in Windows, and they can do everything that other dynamically linked code can do on those platforms.

Under Windows, your plug-in can include an entry point function, usually called DllMain. The entry point is a function Windows calls when LightWave loads your plug-in (or when any process or thread links to a DLL). In order to gain access to resources (dialog box templates, bitmaps, icons, version data) you've linked into your .p file, you need to know your module handle, which you receive as the first argument to your entry point function.

   #ifdef _WIN32
   static HINSTANCE hdll;

   BOOL WINAPI DllMain( HINSTANCE hInstance, ULONG reason,
      LPVOID reserved )
   {
      if ( reason == DLL_PROCESS_ATTACH )
         hdll = hInstance;
      return TRUE;
   }
   #endif /* _WIN32 */

Later, you can pass hdll as the first argument to Win32 functions like LoadIcon and DialogBox.

Parting Tips

  • Always check the activation version (the first argument to the activation function).
  • Always check the value returned by the global function. It may be NULL if the global you've requested doesn't exist in the LightWave that's running you.
  • You can use the lookup function to see whether a command is available.
  • Write to the spec. Don't make assumptions about undocumented LightWave internals. Don't try to dereference opaque pointers or read past the ends of buffers, and don't rely on buffers being contiguous or persistent, or in the same form internally as they are in the SDK.
  • When possible, follow LightWave conventions.
  • Use the Product Info and System ID globals to find out what program is calling you.
  • Use platform-independent services for file I/O and user interfaces.