The host interface provides facilities to create server classes, register plug-in modules, and perform lazy loading and activation of registered servers. There is a fairly elaborate name and type mapping scheme which allows a great deal of flexibility in how modules are used, but which still provides a fairly simple interface for those who do not need the full facility.
The server interface provides an easy method to write programs that will operate as plug-ins. Different classes of plug-in services will require different host interfaces, but the loading and initialization part of the server interface is standard and works with the host portion of the system.
A Server Class is a string which determines the type of service which the server can perform. This might be strings like "TEXTURE" or "FileRequester". Many servers can have the same class, and all servers of the same class have the same host interface.
A Server Name is a string which refers to a specific server within a given class. This might be something like "FractalNoise3D" or "Default". The name must be unique among the servers of the same class.
The names for class and server identification should be byte strings containing characters only the the ASCII range 33-127. By convention these string contain no spaces and no characters outside 7-bit ASCII. Case is significant in distinguishing different classes and servers within classes.
There are two main types of plug-in modules: those providing a single server and those providing multiple servers. It is simple to have one server per module, but it can be more efficient and useful to define many servers with a single code file.
All servers require an activation function, and all plug-ins have the option of providing initialization and cleanup functions. The header for server types is `splug.h'.
The Startup function, if present, will be called when the plug-in is first loaded into the host system. The return value is global data for the server which is passed to the Activate and Shutdown entry points as ` serverData'. A zero return value (null pointer) indicates failure, so even a plug-in with no data should return something.
void * Startup (void)If provided, the server's Shutdown function is called just before the server module is unloaded from the host. Any allocated server data should be freed at this point. Note that even though it is an error, this function may be called even when the server is locked, so correct cleanup should be done in this case as well.
void Shutdown ( void *serverData)
long version, GlobalFunc *global, void *local, void *serverDataThe activation function returns an error code if the attempt to call failed because of some clash between the server and the host environment. If the server was able to process the request, even it failed to complete it, it should return AFUNC_OK. If the version number is not a value which the server can explicitly handle it should return AFUNC_BADVERSION. If there is some global data the server cannot get which it requires it should return AFUNC_BADGLOBAL. Severe problems with the contents of the local data, such as some necessary pointer in the local data being null, may be reported by returning AFUNC_BADLOCAL. Any other errors from the server (running out of memory, bad filenames, user aborts, etc.) must be provided for by the specific plug-in protocol.
typedef int ActivateFunc (<Activation function args>); #define AFUNC_OK 0 #define AFUNC_BADVERSION 1 #define AFUNC_BADGLOBAL 2 #define AFUNC_BADLOCAL 3
typedef void * GlobalFunc (const char *, int); . . .When a server calls the global function, it passes a string which identifies the global data required and a code for the way the data will be used. If the data pointer is not available, null is returned. The ID's that will be recognized depends on the host, on the available global plug-ins and perhaps on the server class.
The use code depends on how the result of the call will be used. If the returned pointer will only be used for the course of the activation function itself, the TRANSIENT code should be used. If the data will be used after the activation function returns, such as in a server that requires locking, the ACQUIRE code should be used. In this case there must be a matching RELEASE call made when the data pointer is no longer required. RELEASE calls need only be made for ACQUIRE calls which returned a non-null pointer. The return value from a release mode global data call is undefined.
. . . #define GFUSE_TRANSIENT 0 #define GFUSE_ACQUIRE 1 #define GFUSE_RELEASE 2
When a server calls the global function with an ID string, the host can service the request itself or has the option of pass unrecognized ID's to Global class servers of the same name. For example, if the ID is "Mambo Functions," the host may recognize this itself and return a pointer value. If it does not recognize it, it may attempt to activate a server of class "Global" with name "Mambo Functions." If such a server exists, it may be locked or unlocked, depending on the use type of the global request, and it will be called to get the value of the global pointer for the orignal requester.
The activation function of a Global server is called with a GlobalService structure which will be initialized with the ID string for the request. The server must fill in the data pointer with a value which will be returned to the client, which may be null if the server wishes to deny the request. The string is passed as data so that the same activation function may be used for multiple servers.
typedef struct st_GlobalService { const char *id; void *data; } GlobalService;
XCALL_ is used on the return type, e.g. XCALL_(int) for an external entry point returning an int. XCALL_INIT is used as the first statement of the function. Both must be used for full compatibility, but XCALL_INIT is only non-null for Manx small-code modules.
<XCALL_ and XCALL_INIT system-specific definition>The activation function as well as any function pointers returned from the activation function need the XCALL treatment. Startup and Shutdown do not.
This plug-in must contain a global character string with the name `ServerClass'. This string defines the class of this server and the server will not be loaded if this string does not match the service type string requested by the host.
It must also contain a global character string called `ServerName' which holds the name for this specific server.
The activation function must be called Activate, which takes the arguments as described above.
XCALL_(int) Activate (<Activation function args>)
typedef struct st_ServerRecord { const char *class; const char *name; ActivateFunc *activate; } ServerRecord;The plug-in module may also have Startup and Shutdown entry points, and all the activate functions in the plug-in will get the same serverData as returned from the Startup function. The assumption is that the servers all share a module for some logical reason, so the sharing of global data is not unreasonable.
The fields of the HostDisplayInfo structure vary from system to system. On the Amiga, the screen pointer is provided for custom screens and is null for Workbench applications. The window pointer is the main application window or null if there is none. On X systems, the window session handle is passed, as well as the ID of the main application window, if any. On Win32 systems, the application instance and main window are provided.
typedef struct st_HostDisplayInfo { #ifdef _AMIGA struct Screen *screen; struct Window *window; #endif #ifdef _XGL Display *xsys; Window window; #endif #ifdef _WIN32 HANDLE instance; HWND window; #endif } HostDisplayInfo;This structure is defined in the `hdisp.h' header file.
A Monitor consists of some generic data and three functions: init, step and done. The `init' function is called first with the number of steps in the process to be monitored, which is computed by the server. As the task is processed, the `step' function is called with the number of steps just completed (often one). These step increments should eventually add up to the total number and then the `done' function is called, but `done' may be called early if there was a problem or the process was aborted. The `step' function will return one if the user requested an abort and zero otherwise.
typedef struct st_Monitor { void *data; void (*init) (void *, unsigned int); int (*step) (void *, unsigned int); void (*done) (void *); } Monitor;The server is masked from any errors in the monitor that may occur on the host side of the interface. If there is a problem with putting up a monitor, the functions will still return normally, since the monitor is for user feedback and is not that critical.
There are some macros provided to call a monitor which will do nothing if the monitor pointer is null. MON_INCR is used for step sizes greater than one and MON_STEP is used for step sizes exactly one.
#define MON_INIT(mon,count) if (mon) (*mon->init) (mon->data, count) #define MON_INCR(mon,d) (mon ? (*mon->step) (mon->data, d) : 0) #define MON_STEP(mon) MON_INCR (mon, 1) #define MON_DONE(mon) if (mon) (*mon->done) (mon->data)These structures and macros are described in the `moni.h' header file.
A new server class is completely defined by the semantics of the activation function for the class. The activation function takes a pointer argument from the host, `local' which is a reference to data for the particular service the host needs performed. It also gets a `global' function pointer which will return global data as needed by the server.
The `local' pointer will point to a StringLocal structure which holds the data for the current operation. This is a null-terminated string and the length of the string buffer, plus a temporary scratch buffer and its length. The server will overwrite `buf' with the result, and will set the `overflow' status flag if the buffers were too short.
typedef struct st_StringLocal { char *buf; char *tmpBuf; int len, tmpLen; int overflow; } StringLocal; . . .For the string transform class of server, the global function can return a `progress' function which can be called by the server to give the user feedback about its progress. This is returned using a string ID of "Progress Function."
. . . typedef void StringProgress (void);We'll stick these definitions into the `t_plug.h' header file for test modules to use.
long version, GlobalFunc *global, StringLocal *local, void *serverDataString activation functions may return with an error code if the version number is wrong or if the global progress function is not available.
{ if (version != 1) return AFUNC_BADVERSION; if (local->len < 10) local->overflow = 1; else sprintf (local->buf, "%ld", strlen (local->buf)); return AFUNC_OK; }
{ StringProgress *progress; int len, i; if (version != 1) return AFUNC_BADVERSION; progress = (*global) ("Progress Function", GFUSE_TRANSIENT); if (!progress) return AFUNC_BADGLOBAL; len = strlen (local->buf); if (local->tmpLen <= len) { local->overflow = 1; return AFUNC_OK; } for (i = 0; i < len; i++) { local->tmpBuf[i] = local->buf[len - i - 1]; (*progress) (); } local->tmpBuf[len] = 0; strcpy (local->buf, local->tmpBuf); return AFUNC_OK; }
{ StringProgress *progress; const char *empty; char *c; if (version != 1) return AFUNC_BADVERSION; progress = (*global) ("Progress Function", GFUSE_TRANSIENT); empty = (*global) ("EmptyStringText", GFUSE_TRANSIENT); if (!progress || !empty) return AFUNC_BADGLOBAL; if (local->buf[0]) { for (c = local->buf; *c; c++) { (*progress) (); if (*c >= 'a' && *c <= 'z') *c = *c - 'a' + 'A'; } } else strncpy (local->buf, empty, local->len - 1); return AFUNC_OK; }
{ StringProgress *progress; int len, i; if (version != 1) return AFUNC_BADVERSION; progress = (*global) ("Progress Function", GFUSE_TRANSIENT); if (!progress) return AFUNC_BADGLOBAL; len = strlen (local->buf); if (local->tmpLen < len || local->len - 1 < len * 2) { local->overflow = 1; return AFUNC_OK; } strcpy (local->tmpBuf, local->buf); for (i = 0; i < len; i++) { local->buf[i * 2] = local->tmpBuf[i]; local->buf[i * 2 + 1] = local->tmpBuf[i]; (*progress) (); } local->buf[len * 2] = 0; return AFUNC_OK; }
The C program module itself includes the headers for the test system and server-side plug-ins. The source file is `tp_rev.c'.
#include <splug.h> #include "t_plug.h" #include <string.h> char ServerClass[] = "StringXfrm"; char ServerName[] = "REVERSE"; XCALL_(int) Activate (<String transform arguments>) { XCALL_INIT; <Reverse function body> }
#include <splug.h> #include "t_plug.h" #include <string.h> static XCALL_(int) Capitalize (<String transform arguments>) { XCALL_INIT; <Capitalize function body> } static XCALL_(int) Double (<String transform arguments>) { XCALL_INIT; <Double function body> } const char class[] = "StringXfrm"; ServerRecord ServerDesc[] = { { class, "CAPS", Capitalize }, { class, "DOUBLE", Double }, { NULL } };
static int ActLength (<String transform arguments>) { <Length function body> } . . .
#include <splug.h> #include <string.h> char ServerClass[] = "Global"; char ServerName[] = "EmptyStringText"; char result[] = "** Empty String **"; XCALL_(int) Activate ( long version, GlobalFunc *global, GlobalService *local, void *data) { XCALL_INIT; if (strcmp (local->id, ServerName) != 0) return AFUNC_BADLOCAL; local->data = result; return AFUNC_OK; }
The final section shows how to add your plug-in to the LightWave host.
.o.p: sc link $(CFLAGS) startup=$(SLIB)serv_s.o $*.o\ $(SLIB)server.lib $(OTHER_LIBS) pname=$@ . . .The math options to the compiler must be chosen so that doubles are 64-bit IEEE values. There is currently some difficulty using the "math=68881" option, however, having to do with the startup code in serv_s.a.
. . . .o.p: ln -o $@ $(SLIB)serv_m.o $*.o -lserver_m $(OTHER_LIBS) . . .Manx modules must use 32-bit ints and IEEE format floating point values.
. . . .obj.p: link32 -dll -out:$@ -def:$(SINC)serv.def $*.obj\ $(SLIB)serv_w.obj server.lib $(OTHER_LIBS) . . .Structure alignment may vary among different compilers and can cause problems when trying to communicate between the host and a plug-in DLL. The LightWave host is compiled using Microsoft alignment rules. Here's an excerpt on structure alignment from the MS Visual C documentation:
"Applications should generally align structure members at addresses that are 'natural' for the data type and the processor involved. For example, a 4-byte data member should have an address that is a multiple of four.
"This principle is especially important when you write code for porting to multiple processors. A misaligned 4-byte data member, which is on an address that is not a multiple of four, causes a performance penalty with an 80386 processor and a hardware exception with a MIPSĀ® RISC processor. In the latter case, although the system handles the exception, the performance penalty is significantly greater. The following guidelines ensure proper alignment for processors targeted by Win32:
"Type Alignment ---- --------- char Align on byte boundaries short (16-bit) Align on even byte boundaries int and long (32-bit) Align on 32-bit boundaries float Align on 32-bit boundaries double Align on 64-bit boundaries structures Largest alignment requirement of any member unions Alignment requirement of the first member"The compiler automatically aligns data in accordance with these requirements, inserting padding in structures up to the limit (default pack size) specified by the /Zp option or #pragma pack. For example, /Zp2 permits up to 1 byte of padding, /Zp4 permits up to 3 bytes of padding, and so on. The default pack size for Windows 3.x is 2, whereas the default for Win32 is 8."
The link line should include any other libraries that the plug-in would need as a stand-alone program. Since most libs on the SGI are themselves DSOs, this adds very little to the size of the plug-in and adds no extra runtime overhead.
. . . .o.p: ld -shared -exported_symbol _mod_descrip -L$(SLIB)\ $(SLIB)serv_u.o $*.o -o $@ -lserver $(OTHER_LIBS)Normally the plug-in DSO's are loaded in a way that forces resolution of all symbols. This allows the host program to report undefined symbols to the plug-in developer. If the name of the module includes the substring "__lazy" (lowercase), however, then the lazy evaluation mode is used, allowing the module to contain undefined symbols as long as they are not referenced by any executed code. This is normally not needed unless you are using code from an external vendor which includes undefined but unused symbols, like the HIIP library from Elastic Reality.
For Modeler, for example, the config file on the Amiga is "MOD-config", on the SGI is ".lwmrc" and on Windows is "LWM.CFG". This file can contain any number of lines of the following form:
Plugin <class> <name> <module> <user name> . . .Each line describes a single server given by Class and Name. The module is the plug-in file containing the server and the user name is the string to display on the interface for describing the server's function. Class, name and module are delimited by spaces, and the user name is the rest of the line. Here are some examples (lines wrap for readability -- each statement has to be a single line).
. . . Plugin CommandSequence Demo_AllBGLayers layerset.p Include Background Plugin CommandSequence Demo_NextEmptyLayer layerset.p Next Empty Plugin MeshDataEdit Demo_MakeSpikey z:lw/plugin/spikey.p Spikey Subdivide Plugin ImageLoader PDQ_Targa pdq/targa.lwp Truevision Targa Image