In the remainder of this document, we will concentrate our discussions and efforts in describing shared libraries in terms of Microsoft Windows Dynamic Link Libraries (DLL). Creating shared libraries under UNIX versions of LScript can vary by platform, so we will concentrate upon the most popular LightWave 3D platform. Most of the techniques discussed can be applied equally to the UNIX method of generating a LScript-linkable shared library.
Under Microsoft Windows, there are several steps required to generate a DLL library capable of linking with your LScript scripts. First, you must become familiar with the LScript Interface Mechanism. This mechanism, although sparse, allows you to interact directly with an executing LScript. It is defined in the 'lscript.h' header file, which must be included in each Windows DLL source file that you compile.
Several items are defined in the 'lscript.h' header file for your use in writing your library. First, the data types that can be passed between an LScript and a DLL function are provided as manifest constants. Although some of the more complex LScript data types are not currently available to a DLL function, quite a lot can be done with the data types that are.
#define LSINTEGER 1
#define LSNUMBER 2
#define LSSTRING 3
#define LSVECTOR 4
#define LSFILE 5
#define LSNIL 99
The next item defined is a structure used to contain the data that is passed
between an LScript and the DLL function:
typedef struct _ls_var
{
int type;
union {
double number;
const char *string;
double vector[3];
} data;
} LS_VAR;
The 'type' member holds one of the data types defined at the head of the file
(LSINTEGER, LSNUMBER, etc.). Based upon the 'type', you can extract data
from one of the 'data' union members 'number', 'string' or 'vector'. The
'number' member of the union is used to contain both REAL and INTEGER values.
For example, if the 'type' of the data is LSINTEGER, then a simple cast of
the 'number' member will extract the integer value:
x = (int)...data.member;
The next entry in the header is the function prototype to which all DLL
functions wishing to interact with an LScript must adhere. Your DLL
functions will not use this typedef directly, rather it is include as a guide
while creating your functions. The LScript core engine will invoke your DLL
function using this prototype:
typedef LS_VAR * (*DLLfunc)(int *count,LS_VAR *,LSFunc *);
Because LScript is itself a shared library, it utilizes the resources of the
host process that has loaded it (i.e., LightWave 3D). When LScript allocates
memory or opens files, it does so by using LightWave 3D's available resources.
For this reason, LScript attempts to be a courteous guest in LightWave's house
by cleaning up after itself, thus avoiding the introduction of memory leaks
into Layout. To accomplish this, LScript tracks all memory allocations
that take place during the execution of a script so that,
when it terminates, it can free any resources that were not properly released
during the script's execution.LScript expects a guest in its house to be no less courteous. To that end, LScript extends to its guests access to the internal functions that it uses to manage these critical allocations. You are encouraged to take advantage of these functions when your shared library function needs to allocate resources (either memory or files). This is not, however, a license to be sloppy in your handling of these resources. You should maintain a discipline of explicitly allocating and freeing the resources you use, but you should do so using these exported internal LScript functions instead of utilities provided by your standard libraries. In this way, should you accidentally overlook an explicit resource release, LScript can catch it upon termination.
The next entries in the lscript.h header file is a structure definition that contains pointers to, among other things, each of these internal resource-management functions exported by LScript:
typedef struct _lsfunc
{
char * (*strdup)(const char *);
void * (*malloc)(int);
void (*free)(void *);
void (*info)(const char *);
void (*error)(const char *);
} LSFunc;
If you refer back to the function prototype defined earlier, you will notice
that the last parameter to each shared library function is a pointer to a
structure of this type, LSFunc. Using this structure pointer, you can access
the function pointers contained within to perform your resource management:
LS_VAR *dllfunc(int count,LS_VAR *parameters,LSFunc *func)
{
...
// allocate an array of 5 LS_VARs
vars = (LS_VAR *)(*func->malloc)(sizeof(LS_VAR) * 5);
...
// release our allocations...
(*func->free)(vars);
...
}
These functions are invoked in the same fashion as other function pointers listed in the structure, but they will produce a visible result to the user:
...
(*func->error)("severe error: bad parameter");
...
There are two important facts regarding these functions that you need to be
aware of. First, the message that you pass to LScript is not the final message
that will be displayed. LScript places information at the beginning of your
message (and potentially at the end) before it displays the message to the
user. For this reason, you should probably follow the guideline that your
message should not begin with a capitalized word, and it should never assume
that it is all the information that the user will see. For instance, the
output from the previous example might appear to the user as:
Line #45, severe error: bad parameter
Second, any invocation of the error() function will set an internal flag that
will cause LScript to halt its execution of the current script when the shared
library function terminates. Therefore, error() is intended to be used only
when a severe error is encountered within a shared library function that will
cause subsequent errors to arise later in the script.
char *getVolumeSerialNumber(void)
{
char tmpPath[MAX_PATH + 1];
char tmpFile[MAX_PATH + 1];
static char volumeSN[50];
BY_HANDLE_FILE_INFORMATION fileInfo;
HANDLE hFile;
GetTempPath(MAX_PATH,tmpPath);
GetTempFileName(tmpPath,"LScript",0,tmpFile);
hFile = CreateFile(tmpFile,GENERIC_READ | GENERIC_WRITE,
0,NULL,OPEN_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY,NULL);
GetFileInformationByHandle(hFile,&fileInfo);
sprintf(volumeSN,"%.4X-%.4X",HIWORD(fileInfo.dwVolumeSerialNumber),
LOWORD(fileInfo.dwVolumeSerialNumber));
CloseHandle(hFile);
DeleteFile(tmpFile);
return(&volumeSN);
}
Now, in its current state, we cannot use this function directly in an
LScript. We must modify it to satisfy the LScript linking conditions. The
modifications, however, will be comparatively minor.First, we must change the header of the function to exactly match the typedef set forth in the 'lscript.h' header. Thus,
char *getVolumeSerialNumber(void)
becomes
LS_VAR * getVolumeSerialNumber(int *count,LS_VAR *vars,LSFunc *func)
Next, we must have a value to return to the LScript. In the original
function, we simply returned a character pointer to a static area. This is
not sufficient for interfacing with LScript. We must return one or more
LS_VAR structures, each holding a single item of LScript-readable data. In
the case of this particular function, we will be returning the same character
string wrapped in a LS_VAR structure.We will declare our own private LS_VAR variable in the function. We must also make sure that the variable itself will exist after the function has returned, so we will declare it as "static":
...
static LS_VAR var;
...
Likewise, the character string we are returning must also continue to exist
after the function returns (otherwise, we will be flirting with disaster when
LScript tries to access "released" memory). Fortunately, we already have a
"permanent" area, defined in the original function, where the character string
can reside without fear of violating memory:
...
static char volumeSN[50];
...
Now, because we have altered the parameters that the function accepts, we are
no longer able to rely on the compiler to detect an incorrect parameter count
at compile time. We must therefore check this parameter count at runtime. It
is up to the shared library function to ensure that the count of parameters
passed into it by LScript is correct:
if(*count != 0) // we take no arguments
{
(*func->error)("invalid parameter count to DLL function ... ");
return(NULL);
}
In the case of the getVolumeSerialNumber() function, it takes no arguments
(i.e., "void"). If any are passed to it, this should be an error.The main body of the function remains largely unchanged. Once we have ascertained the volume serial number, we then must package it into a format that LScript can digest. In the original function, we simply returned a character pointer to our static string area:
return(&volumeSN);
However, for LScript to properly handle the data, it must be wrapped by our
LS_VAR structure, and we must indicate to the LScript engine not only their
types, but how many data items we are returning. The new exit code of the
function looks like this:
var.type = LSSTRING;
var.data.string = volumeSN;
*count = 1; // how many data items to expect
return(&var);
}
Here, then, is the complete LScript shared library version of the
getVolumeSerialNumber() function:
#include <stdio.h>
#include <windows.h>
#include "lscript.h"
LS_VAR * getVolumeSerialNumber(int *count,LS_VAR *vars)
{
char tmpPath[MAX_PATH + 1];
char tmpFile[MAX_PATH + 1];
static char volumeSN[50];
static LS_VAR var;
BY_HANDLE_FILE_INFORMATION fileInfo;
HANDLE hFile;
if(*count != 0) // we take no arguments
return(NULL);
GetTempPath(MAX_PATH,tmpPath);
GetTempFileName(tmpPath,"LScript",0,tmpFile);
hFile = CreateFile(tmpFile,GENERIC_READ | GENERIC_WRITE,
0,NULL,OPEN_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY,NULL);
GetFileInformationByHandle(hFile,&fileInfo);
sprintf(volumeSN,"%.4X-%.4X",HIWORD(fileInfo.dwVolumeSerialNumber),
LOWORD(fileInfo.dwVolumeSerialNumber));
CloseHandle(hFile);
DeleteFile(tmpFile);
var.type = LSSTRING;
var.data.string = volumeSN;
*count = 1;
return(&var);
}
A sample Definition file for the DLL function getVolumeSerialNumber() might look like:
LIBRARY test
EXPORTS
getVolumeSerialNumber
You can save this definition file under any name you wish, as long as you use
that name when you compile the DLL library. In the case of this example, we
have named it 'dll.def.' If and as you add functions to your DLL library, you
should add their names under the "EXPORTS" section of the Definition file
before you re-compile.
CFMACH = -D_X86_=1
CFLAGS = -c -W3 $(CFMACH) -DWIN32 -D_WIN32 /O2 /I . $(OPT)
LFLAGS = -dll -def:.\dll.def
OBJS = test.obj
LLIBS = libc.lib kernel32.lib
.cpp.dll:
cl $(CFLAGS) $*.cpp
.c.dll:
cl $(CFLAGS) $*.c
all : test.dll
test.dll: $(OBJS)
link $(LFLAGS) -out:test.dll $(OBJS) $(LLIBS)
test.obj : test.c
If you are indeed using Microsoft Visual C++ as your development system, a
simple invocation of 'nmake' will generate for you a shiny new DLL that you
can now link directly into your LScript. Other development systems will
likely require modification of the provide makefile, and possibly other
files.
library "test.dll";
main
{
info("Your volume serial number is ",getVolumeSerialNumber());
}
If a path is not specified in the 'library' command, then the LScript engine
will scan the following locations in the following order to locate your DLL
file:
1. The current directory (which may or may not be the same
location where the script resides)
2. The Windows system directory (usually Windows\System or
WinNT\System32)
3. The LibraryPath entry in the LScript Configuration for this
plug-in type (ls-if, ls-or, etc.)