By utilizing the shared library mechanism, LScript allows you to write shared libraries can be used to implement user-defined Object Agents. What this means is that new Object Agents (technically, these are referred to as Class Definitions) can be written within shared libraries, and instances of these Object Agents (Class Definitions) can be created in an LScript by a programmer utilizing the following declaration:
use "<filename>.dll" as class <ClassName>;
The protocol for creating your own Object Agents is much more complex than
that of simply creating a LScript-callable shared-library function. Because we
have taken pains to emulate C++ class design, Object Agent shared libraries
will contain "methods" (functions) that will only be accessible by the LScript
engine (such as destructors), along with the normal "public" items that an
Object Agent makes available to LScripts, such as data members and exported
methods.To better illustrate the requirements of building a user-defined Object Agent, we will construct a questionably-useful Object Agent called BobObj. BobObj will have only one "public" method, stradd(), which will concatenate two character strings together, returning the new string as its only return value.
As with DLL functions, topics covered in this section are of a programmatic content and are non-trivial in nature. They are intended for more advanced developers.
The location where these global values reside varies across LightWave 3D platforms. Users of LScript v1.3 or higher can utilize the globalstore() and globalrecall() functions to place values into the LScript-global areas. If, however, you are using versions of LScript prior to this, you must enter values manually.
HKEY_CURRENT_USER\Software\NewTek\LScript
Under this key are other keys that are related directly to the specific
LightWave plug-in architectures that LScript supports--LScript/IA, LScript/DM,
LScript (Modeler), etc. It is within these individual architecture keys that
the store() and recall()
script functions place/retreive their values.
To make the 'LibraryPath' setting available to all LScript architectures (recommended),
you would create a new string value in the key mentioned above. This new
string value will have the identifier LibraryPath, and its value will
be the path to the directory wherein ObjectAgent libraries and script insert files
can be found.
If you wish or need to store such files in more than one location, you can place up to 10 directory paths into the string value for LScript to search through. Each path must be separated from the others by a semi-colon (much the same as the MS-DOS PATH value).
Name Data
(Default) (value not set)
LibraryPath "f:\lw\dev\lscript\dll;f:\oa;c:\temp"
LScript on the DEC Alpha creates configuration files for each individual architecture. These configuration files are simple ASCII text, and can be edited by any text editor. Typically, these configuration files reside in the LightWave program directory (NewTek\Programs). The format of these configuration files is identical to the Windows INI file format. When setting the 'LibraryPath' value for an architecture, you will want to place it into the [LScript] section of the configuration file.
[LScript]
ScriptPath=c:\bob\lscript\examples
LibraryPath=c:\bob\lscript\ObjectAgent
A new structure is defined that houses an instance of the LSFunc structure discussed in Appendix A on DLL functions, along with several other data pointers:
typedef struct _lsdll
{
const char *ClassName;
const char *InstanceName;
LS_VAR *instance[MAXINSTANCE];
LSFunc *func;
} LSDLL;
The 'ClassName' member points to a character string that contains the Object
Agent 'class' name, designated in the LScript.'InstanceName' points to a character string that contains the variable name that contains the current instance of the Object Agent.
The 'instance' array is a series of MAXINSTANCE arrays of Object Agent instance data. The current instance can always access its own instance data, if there is any, through instance[0]. Although they are currently unused, the remaining elements are reserved for future instance data that may be available as a result of object inheritance. Instance data is discussed later in this section.
A new prototype has also been is defined for use in creating your "public" Object Agent methods. This prototype will be used by the LScript engine to invoke your methods:
typedef LS_VAR * (*ObjectAgentMethod)(int *,LS_VAR *,LSDLL *);
The four required methods are:
void constructor(count,variables,classinfo)
'count' is an integer value representing the number of
parameters that were passed to this constructor
'variables' is a pointer to an LS_VAR array containing
the values in the parameters that were passed to the
constructor. it will be NULL (and 'count' will be 0)
if no parameters were passed
'classinfo' is a pointer to an instance of an LSDLL structure
(discussed earlier) that contains information about the
class and instance names, pointers to internal LScript
functions, and pointers to instance data
void destructor(count,variables,classinfo)
parameters are identical to those of the constructor, however
because destructors cannot be invoked directly, 'count' will
always be 0 and 'variables' will always be NULL
boolean ownmethod(methodname)
this interface method is used by LScript to see if an Object
Agent instance owns the specified method. 'methodname' is a
character string that names the method in question.
ownmethod() should use this character string to compare to
its known "public" methods, returning 'true' if one of its
methods matches this name, or 'false' if the specified method
is not available
boolean owndata(dataname)
this private method is similar to ownmethod(), but is intended
to be applied to the "public" data members that the Object
Agent makes available. 'dataname' is a character string that
contains the name of the data member in question
In addition, three more methods can exist in the interface. Two are used to manage
access to "public" data members of the Object Agent Class, while the third locks
the Object Agent into a specific revision of the Object Agent Interface (defined
by the OAMAJOR and OAMINOR declarators in the lscript.h
header file):
LS_VAR *returndata(dataname,classinfo)
returndata() returns the value contained by the Object Agent
data member named by 'dataname->extra', a void * that
should be cast to a char *. returndata() will never
be called for a data member unless owndata() has previously
indicated that that data member is owned by this Object Agent
the 'dataname->data.number' member should be cast to an
integer value if the referenced data member is an array.
'dataname->data.number' is the index value used within the
script, and should be converted to a zero-based offset.
PLEASE NOTE that this value should only be cast if it is
needed as an index value. Casting this value when it has
not been explicitly set by the scripting engine can cause
certain operating systems (notably, the DEC Alpha) to
generate a Floating Point exception error.
assigndata(dataname,data,classinfo)
LScript will invoke this Object Agent method when a script
needs to assign new data to a data member. LScript will
handle, internally, any of the possible means of assigning
new values to existing data members. this method will be
passed the result, and be expected to update the appropriate
data member.
as with returndata(), 'dataname' is a pointer to an LS_VAR
structure that houses both the data member identifier
in the 'extra' area, and, if applicable, the requested
index offset in the 'data.number' area if the data member
can be indexed.
'data' is another LS_VAR pointer that contains the new value
'classinfo' is an instance of LSDLL.
version(major,minor)
'major' and 'minor' are integer pointers that will receive
the required major and minor revision values that indicate
the level of the Interface that this Object Agent requires to
function. If the returned values do not match the revision
of the current Object Agent Interface specification supported
by the plug-in, then the library will not be loaded and
unresolved function references will likely result.
A special behavior of the interface involves the existence of "public" data
members in your Object Agent class. If your particular Object Agent makes
available no "public" data members (i.e., owndata() unconditionally
returns a 'false' value), then the returndata() and
assigndata() methods become optional, and need not be present in
your interface. The reason for this is that these two methods will never
be invoked by the LScript engine unless owndata() returns
'true'.We will see examples of the usage of each of these methods later in this section when we write the BobObj Object Agent.
"Instance data" is essentially nothing more than a copy--or instance--of specific data values that an object must have in order to function. A good example of this need of instance data would be in a file-handling Object Agent. In our fictitious file-handling Object Agent, there will be methods defined that perform some type of specific functioning on the Object Agent data. Without the ability to handle instance data, you would have to maintain (and declare) a different Object Agent DLL for each FILE you wanted to handle as an object. Why? Because each DLL would have to have its own variables (declared at compile time) that it used to maintain items such as file name, FILE pointer, etc., but would only have one location where this information could be stored. If you needed to handle a new file using a single DLL, you would have to re-initialize an existing Object Agent with the new file information, or create a new instance of a declared Object Agent--both actions would have the net effect of destroying any previous data that was being maintained for that instance because it would have to be overwritten (remember, there is only ONE copy of the DLL loaded into memory, not one copy for each instance you create).
Another solution might be to have the individual Object Agent shared library provide its own mechanism for maintaining separate instance data. However, this means that Object Agent shared library writers would have to devise their own storage mechanisms instead of using the same established standard, and additional information might have to be provided to the shared library function so that they could distinguish among their potentially-many instances.
LScript employs the instdata() function for allowing an Object Agent shared library to create a new copy of the instance data that the Object Agent will need to function properly. The instance data created by instdata() is not required to be populated at the same time that it is created, because instdata(), if it exists, is always called before the Object Agent's constructor (which is the most appropriate place to initialize instance data values). If an Object Agent shared library does not contain an instdata() method, then LScript assumes that the Object Agent will have no instance data, and that particular value in the LSDLL structure (instance[0]) will be set to NULL.
If an Object Agent needs to maintain instance data, then the instdata() function should be present and designed to allocate the necessary storage locations. Each element of instance data is stored in a LS_VAR data wrapper, and passed back to LScript as a LS_VAR pointer. If you have more than one element, then you would need to allocate a linear array of LS_VAR data wrappers, and return a pointer to the first element to LScript after completing your processing. This same pointer returned by instdata() will be placed into the first element of the LSDLL structures instance array (instance[0]), which is passed to other Object Agent methods.
In the sample Object Agent we will create, we have no real need for maintaining instance data, but we do so for the sake of providing an example.
void version(int *maj,int *min)
{
*maj = 1; // need revision 1.1
*min = 1; // (this is the interface version, NOT the plug-in version)
}
Because we will maintain instance data--albeit, dummy data--in the BobObj
Object Agent, we will begin with that method. Keep in mind that this method
is optional.In BobObj, we will maintain three instance values, named "tom," "dick" and "harry." These instance values are all considered "public", and all will be accessible by the host LScript. We will not actually have to name them thus internally, but rather we need to be aware that this will be how the script may reference them. Because we do not initialize the instance data in the instdata() method, it becomes quite simple:
LS_VAR * instdata(LSFunc *func)
{
LS_VAR *mydata;
// we have three data members, all "public"
mydata = (LS_VAR *)(*func->malloc)(3 * sizeof(LS_VAR));
return(mydata);
}
Here are some important things to be aware of regarding this method:
1. Do not use "static" variables to house your instance data. Static
variables are single values that are shared between instances. While
this may be your intention, more often than not you will want each
instance to have its own unique set of data values from which to
operate. For this reason, they must be allocated from the run-time
heap.
2. You can structure instance data any way you wish, but no count is
maintained by LScript of the number of entries in your instance data.
It is assumed that the contents of the instance array are all known to
the Object Agent, so access to the data in the array should NEVER
exceed its boundaries.
3. Be sure to use the LScript-provided resource-management functions so
that you don't introduce memory leaks to LightWave 3D. LScript tracks
all allocations/releases of internal memory, and releases any
remaining memory allocations when it terminates. If you use the
standard library malloc(), LScript won't know about it and leaks could
occur if you forget to explicitly free() it.
If present, this will be the first method called by the LScript engine when a
new instance is created in the script.The next method we will complete is the Object Agent's constructor. The constructor is a required method, and it will be invoked whenever the script creates a new instance of the Object Agent. The constructor will always be called immediately following the invocation of instdata(), or it will be the first method invoked if instdata() does not exist.
In the BobObj constructor, we will initialize the instance data members that were created in instdata(). Constructors can be passed parameters by the script, and you will typically want to initialize your instance data using values that are derived from these parameters. We do not use constructor parameters in the BobObj Object Agent.
void constructor(int count,LS_VAR *parameters,LSDLL *lsdll)
{
LS_VAR *inst;
if(count != 0) // error! we don't accept parameters
{
// (In C++: no class method matching this signature)
(*lsdll->func->error)(...);
return;
}
if((inst = lsdll->instance[0]) != NULL) // just be sure...
{
inst[0].data.number = 145.567;
inst[1].data.string = "Dick"; // constant data member
inst[2].data.vector[0] = 10.0;
inst[2].data.vector[1] = 20.0;
inst[2].data.vector[2] = 30.0;
}
}
As mentioned earlier, an Object Agent's constructor is invoked at the time
the instance is created in an LScript. Instances are created by the script
programmer when the class name of the Object Agent is used as a function
call. For instance, when we create instances of the BobObj Object Agent,
we invoke the class constructor like so:
use "bobobj.dll" as class BobObj;
...
main
{
...
bob1 = BobObj(); // create a BobObj instance
...
Had we been required to pass arguments to the Object Agent constructor, we
would have placed them within the parentheses:
bob1 = BobObj(1,"Hello"); // create an instance using parameters
Some important things to note about an Object Agent's constructor method:
1. In the constructor, you should establish the appropriate data values
in your instance data for this instance of the class. These values
will usually be derived in some way from the parameters that are
passed to this method. For example, were this some type of file-
handling class, we would expect one of the parameters to indicate
a unique filename the Object Agent would be expected to manage.
2. You can simulate method overloading in any Object Agent method simply
by being flexible about the count and type of arguments provided to
the method. You can then look for the patterns that match what those
overloaded methods your Object Agent supports. Anything else would
be a run-time error.
3. The 'InstanceName' attribute of the LSDLL structure has not yet been
established when the Object Agent's constructor is invoked. The reason
for this is that the instance being created has not yet been assigned
to a variable at the point in time when the constructor is called.
DO NOT USE IT!
All well-written class definitions should include a destructor method. The
destructor method is used to "clean up" the instance that is going away, or
"out of scope." The bulk of this clean up is usually the graceful shutdown
of any instance data members that require it, and freeing of the instance's
resources (memory, files, etc.). It is within the destructor that we also
release any resources allocated by the instdata() method.Because the BobObj Object Agent we are creating manages no complex instance data, it's destructor is quite simple:
void destructor(int count,LS_VAR *parameters,LSDLL *lsdll)
{
if(lsdll->instance[0]) // free instdata()-allocated resources
(*lsdll->func->free)(lsdll->instance[0]);
}
Of course, had we allocated additional resources within each instance data
element, we would traverse each element in the destructor, invoking the
appropriate LScript function to release it (free(), fclose(), etc.) before
releasing the instance data array itself.The next two required Object Agent methods are ownmethod() and owndata(). These two methods are quite straight-forward, in that they simply return a logical 'true' or 'false' to indicate the Object Agent's ability to service specific methods or data members. Our BobObj Object Agent sports one public method, "stradd()", and three public data members, "tom," "dick" and "harry."
int ownmethod(const char *method) // do we own a specific method?
{
if((method[0] == 's') &&
(strcmp(method,"stradd") == 0)) return(TRUE);
return(FALSE); // we don't own this method...
}
int owndata(const char *data) // do we own a specific data member?
{
if((data[0] == 't') &&
(strcmp(data,"tom") == 0)) return(TRUE);
else if((data[0] == 'd') &&
(strcmp(data,"dick") == 0)) return(TRUE);
else if((data[0] == 'h') &&
(strcmp(data,"harry") == 0)) return(TRUE);
return(FALSE); // we don't own this data member...
}
Notice in these two methods the lack of any error handling. When a method
or data member is not found, neither method generates an error message of
any kind. This is quite deliberate in design. We are allowing the LScript
engine to decide how to handle this situation.It is a possibility that in future revisions of this interface, the LScript engine will be able to support Object Agent inheritance, where one Object Agent can inherit the methods and data members of another, creating a new Object Agent all within the operation of the LScript. The reason we do not generate an error of some kind when we cannot locate a method or object method is that a negative return value from either of these methods could cause the LScript engine to query our parent(s) (or superclasses). In the current revision, however, the LScript engine simply generates an error condition on the Object Agent's behalf.
Probably the two methods with the greatest potential for size are the final required methods, returndata() and assigndata(). These methods are responsible for handling access to the Object Agent's public data members.
Returndata() is responsible for returning the value contained in a requested Object Agent data member. This method will not be called by the LScript engine unless the owndata() method has first been used to ensure that the data member to be accessed belongs to the Object Agent. Nevertheless, returndata() should always be prepared to handle situations where the provided data member identifier is not appropriate for the Object Agent. A return value of NULL indicates this error condition.
Here is the BobObj-version of returndata():
LS_VAR * returndata(LS_VAR *datamember,LSDLL *lsdll)
{
static LS_VAR lsvar; // assigned a different value on each call
static LS_VAR *inst;
static char *identifier;
static int index;
if(!lsdll->instance[0]) // no instance data, that's a problem
return(NULL);
inst = lsdll->instance[0]; // use some short-hand for easier reading
identifier = (char *)datamember->extra;
index = (int)datamember->data.number;
if((identifier[0] == 't') && (strcmp(identifier,"tom") == 0))
{
lsvar.type = LSNUMBER;
lsvar.data.number = inst[0].data.number;
}
else if((identifier[0] == 'd') && (strcmp(identifier,"dick") == 0))
{
lsvar.type = LSSTRING;
lsvar.data.string = inst[1].data.string;
}
else if((identifier[0] == 'h') && (strcmp(identifier,"harry") == 0))
{
lsvar.type = LSVECTOR;
lsvar.data.vector[0] = inst[2].data.vector[0];
lsvar.data.vector[1] = inst[2].data.vector[1];
lsvar.data.vector[2] = inst[2].data.vector[2];
}
else
{
// owndata() said we owned it, but we really don't (this should
// never happen, but...)
sprintf(errormsg,"bad data member name %s::%s.%s",
lsdll->ClassName, lsdll->InstanceName, identifier);
(*lsdll->func->error)(errormsg); // halt the script
return(NULL);
}
return(&lsvar);
}
Assigndata() performs a function very similar to returndata(), however it
updates data member values instead of returning them. It also performs
a number of sanity checks on the value being used for the update to ensure
data member integrity:
void assigndata(LS_VAR *datamember,LS_VAR *value,LSDLL *lsdll)
{
static LS_VAR *inst;
static int i1,i2;
static char *identifier;
static int index;
if((inst = lsdll->instance[0]) == NULL) // shorthand
return;
identifier = (char *)datamember->extra;
index = (int)datamember->data.number;
if((identifier[0] == 't') && (strcmp(identifier,"tom") == 0))
{
if(value->type != LSINTEGER && value->type != LSNUMBER)
{
sprintf(errormsg,
"invalid data type in data member assignment %s::%s.%s",
lsdll->ClassName, lsdll->InstanceName, identifier);
(*lsdll->func->error)(errormsg);
return;
}
inst[0].data.number = value->data.number;
}
else if((identifier[0] == 'd') && (strcmp(identifier,"dick") == 0))
{
// since we consider this to be a "constant" data member, any
// assignment type would be illegal
sprintf(errormsg,
"illegal assignment to constant data member %s::%s.%s",
lsdll->ClassName, lsdll->InstanceName, identifier);
(*lsdll->func->error)(errormsg);
}
else if((identifier[0] == 'h') && (strcmp(identifier,"harry") == 0))
{
if(value->type != LSVECTOR &&
value->type != LSINTEGER &&
value->type != LSNUMBER)
{
sprintf(errormsg,
"invalid data type in data member assignment %s::%s.%s",
lsdll->ClassName, lsdll->InstanceName, identifier);
(*lsdll->func->error)(errormsg);
return;
}
// we allow integer/number values to be used, as well as
// vectors
if(value->type == LSVECTOR)
{
inst[2].data.vector[0] = value->data.vector[0];
inst[2].data.vector[1] = value->data.vector[1];
inst[2].data.vector[2] = value->data.vector[2];
}
else
inst[2].data.vector[0] =
inst[2].data.vector[1] =
inst[2].data.vector[2] = value->data.number;
}
else
{
sprintf(errormsg,"bad data member name %s::%s.%s",
lsdll->ClassName, lsdll->InstanceName, identifier);
(*lsdll->func->error)(errormsg);
}
}
Finally, to complete our BobObj Object Agent implementation, we need to
create our single public method, stradd(). Notice that the public method
stradd() adheres to the function prototype set forth in the 'lscript.h' header
file for Object Agent "public" methods:
LS_VAR * stradd(int *count,LS_VAR *vars,LSDLL *lsdll)
{
static LS_VAR var;
static char buf[512];
static char num[100];
static LS_VAR *inst;
if(*count != 2) // check our "signature"
return(NULL);
*count = 0; // prepare to tell the engine how many return values
if(vars[0].type == LSSTRING && vars[1].type == LSSTRING)
{
inst = lsdll->instance[0];
sprintf(num," (( <%g,%g,%g> ))", // this value will be appended
inst[2].data.vector[0], // to each string as a visual
inst[2].data.vector[1], // authentication of the instance
inst[2].data.vector[2]);
var.type = LSSTRING;
if(strlen(vars[0].data.string) >= 512)
strncpy(buf,vars[0].data.string,500);
else
strcpy(buf,vars[0].data.string);
if((strlen(buf) + strlen(vars[1].data.string)) < 500)
strcat(buf,vars[1].data.string);
strcat(buf,num);
var.data.string = buf;
*count = 1; // tell engine that there is one element returned
return(&var);
}
else
return(NULL);
}
Public Object Agent methods have some operational points that need to be
mentioned:
1. Notice that the first parameter to stradd(), used to indicate the
number of script parameters that are contained in the 'vars' list,
is a pointer instead of a value. The reason for this is that this
location is used to indicate to the LScript engine the number of
elements that are being returned from the method. In the case of
stradd(), only one element is returned, so this value is set to 1
before returning (and initialized to 0 in case of error).
2. LScript is not aware of the arrangements you may have made within your
code for the "containers" used to return values. You may have
elected to use static values, or you may have allocated them from
the heap. Because of this, the LScript engine cannot make assumptions
concerning the disposition of your containers when it has finished
processing their values (i.e., should it free them from the heap or
simply ignore them?). For this reason, the method (or Object Agent)
is solely responsible for allocating these items, and releasing them
when they are no longer needed. Suggested practices for creating
these resources might be to use a static array of LS_VARs, or to
allocate the additional values needed in the Object Agent's instance
data (instdata()). However, the use of static storage is the
recommended method of returning values to the engine; allocating
memory consumes critical resources and CPU time.
If using static storage is not a viable option within your method, and
you return the same number of items each time, a somewhat more elegant
solution would be to allocate a static pointer in your method, and
then allocate the needed elements using the provided LScript memory
allocation function malloc() the first time the method is invoked. In
this way, the memory would not be allocated until it is really needed
(the method may not even be invoked throughout the entire script), and
the Object Agent will not have to be concerned about not being able to
release the memory because LScript will catch these unreleased
resources when it terminates the script.
stradd(...)
{
static LS_VAR *myvars = NULL;
...
if(myvars == NULL)
myvars = (LS_VAR *)(*lsdll->func->malloc)
(sizeof(LS_VAR) * 5);
Once you have compiled your new Object Agent DLL, it must be placed into one of the locations in which the LScript engine will look when trying to resolve references. These locations were outlined in the previous section.
use "bobobj.dll" as class BobObj;
bob0 = BobObj(); // a global instance
main
{
bob1 = BobObj(); // two instances local to main()
bob2 = BobObj();
info(bob1); // Object Agent instances can be passed
info(bob2); // as parameters
info(bob1.stradd("Hello","There"));
info(bob2.stradd("I'm","Bob"));
info(bob1.tom);
info(bob1.dick);
info(bob2.harry);
info("calling test!");
test();
bob1.tom += 15;
info(bob1.tom);
bob2.harry /= 10;
info(bob2.harry);
test();
info(bob0.dick);
info(bob0.tom++); // post-increment
info(bob0.tom);
// bob1 and bob2 destructor invoked upon exit from main()
} // bob0 destructor invoked upon termination of script
test
{
bob3 = BobObj(); // instance local to test()
info(bob3.harry);
// bob3 destructor is invoked upon exit from test()
}
The output from this LScript shows that the many instances of our BobObj
Object Agent are all alive and well and functioning properly:
INFO: (ObjectAgent)BobObj::bob1
INFO: (ObjectAgent)BobObj::bob2
INFO: HelloThere (( <10,20,30> ))
INFO: I'mBob (( <10,20,30> ))
INFO: 145.567
INFO: Dick
INFO: <10,20,30>
INFO: calling test!
INFO: <10,20,30>
INFO: 160.567
INFO: <1,2,3>
INFO: <10,20,30>
INFO: Dick
INFO: 145.567
INFO: 146.567