NOTE: the examples in this chapter require the compilation of shared libraries. If you don't own the tutorial directory, copy the files Makefile and demolib[1-5abc].[cC] into a suitable directory. Edit variable DEST_DIR in the Makefile to specify the directory where the created shared libraries shall become placed and include that directory in your NST_LD_LIBRARY_PATH (or LD_LIBRARY_PATH, if the former is not defined) so that Neo can find the libraries.
1. implement the function in a shared library nnnn.soand
add some simple wrapper code to inform the prog_unit about the function call
interface (see below).
2. put the line #import "nnnn" into the header portion of any prog_unit
program that wishes to use the function. Here, nnnn must be the basename
(i.e. without suffix) of the shared library of step 1.
The implementation of step 1 consists of two parts. The first part is the line
char *FUNCTIONS_nnnn[] = { "float scalar_prod(float*,float*)", NULL };
It tells the prog_unit that the library contains a function namedscalar_prod with two float* arguments and a float return value (the name suffix _nnnn is optional but recommended; its presence allows the library also to be statically linked with the executable). The second part is the implementation of the function itself. The prototype must be of the form
float scalar_prod(int *piDim, void **ppvArg) { ... }
The first array piDim will pass the dimensions of all arguments (a value piDim[i]=0 indicates the the i-th argument is a scalar). The second array ppvArg will pass for each argument the address of its first element (scalars are treated like 1-dimensional arrays and can be recognized from the zero dimension passed in piDim), Note that an array parameter can be NULL. This will be passed as the address value NULL in ppvArg[], together with piDim[]=0 for the corresponding argument. In the present case, there are two float* arguments, so that the full implementation of scalar_prod in nnnn.c might look like
float scalar_prod(int *piDim, void **ppvArg) {
float *pfArg0 = (float*) ppvArg[0]; /*
adr of 1st arg */
float *pfArg1 = (float*) ppvArg[1]; /*
adr of 2nd arg */
float fReturn = 0;
if (piDim[0] == piDim[1]) {
int i; /* compute scalar
product of two vectors: */
for (i=0; i<change(*piArg0);
i++) fReturn += (*pfArg0++)*(*pfArg1++);
} /* else error message ... */
return fReturn;
}
The file nnnn.c = demolib1.c contains the full example. The command
gcc -shared -o demolib1.so demolib1.c
will compile it into a shared library. Instead of the above, you can use the accompanying Makefile and just say make demolib1.so. Example#1 illustrates its use from a prog_unit.
char *FUNCTIONS_nnnn[] = {
/* define a prefix: */
"__:",
/* two exported constants: */
"float MyPI",
"int My4711",
/* three exported functions: */
"void foo(int, float*)",
"float bar(void)",
"int bcopy(char*, byte*)",
NULL
};
char *NAMESPACE_nnnn = "mylib";
float __MyPI = 3.14; /* implements float const MyPI */
int __My4711 = 4711; /* implements int const My4711 */
void __foo(int *piDim, void **ppvArg) { ...
}
float __bar(int *piDim, void **ppvArg) { ... }
int __bcopy(int *piDim, void **ppvArg){ ...
}
(a full implementation with some further functions explained below is in
file demolib2.c). Note also that the (int*,void**)
call interface is even used when a function is declared as parameterless (e.g.,
"void myfun(void)" in the FUNCTIONS[] line); in this case,
piDim and ppvArg are passed as two NULL pointers
that are not used. Exceptions are some functions with predefined, special
identifiers (such as _init or special class methods explained
below). Their interface may deviate from the standard (int*,void**)
form and must be inferred from their documentation.
The definition NAMESPACE_nnnn = "mylib"
defines an optional prefix for the prog_unit level (to be distinguished
from the implementation level) and allows the exported functions and symbols
also to be referenced by a "qualified name", e.g.
.mylib.foo(), mylib.MyPi etc. This is useful
in the case of name conflicts when importing from several libraries.
char *FUNCTIONS_nnnn[] = { ...
void foo(char*,float=3.14,int=4711);
...
}
This makes foo callable with a trailing number of the default arguments omitted, e.g., foo("hello"), foo("hello",2.5) and foo("hello",2.5,753) are all legal function calls.
int nst_redimension(void **ppAdr, int iNewDim)
Here, *ppAdr must be the address of the to-be-resized memory block
(and ppAdr the adress where the address is stored, i.e,
to resize the i-th argument parameter, one might call nst_redimension(ppvArg+i,iNewDim))
and iNewDim the new number of elements (not bytes!).
The call will return 1, if the redimension operation on the argument was permitted, and 0 otherwise. In the former case, the old address *ppAdr will have been replaced by (the possibly changed) new address after the resize operation. Using this function call will keep the prog_unit informed that after return the argument will have the new dimension iNewDim.
A redimension operation is not automatically allowed for all arguments. Nst_redimension() will return 1 (and thus have carried out the resizing) only on those arguments that were in the function call preceded by the special token "(&)", which permits the resize(a "reference cast"). An additional effect is that the presence of a "(&)" in front of a function argument will restrict that argument to be a pointer variable (when the pointer variable is specified with an offset, e.g., (p+5), then the redimension operation will ensure that the "tail portion" (i.e, the part behind p+5) will have the specified iNewDim elements, with a corresponding larger dimension for p itself). Therefore, an implementation that invokes nst_redimension() should provide proper action for both outcomes, since some calls may use the reference cast and others may not, e.g, if they wish to pass an array or a string constant (which are not pointer variables and, therefore, cannot be preceded by the reference cast). Note also that inside a function defined in the prog_unit source code, all function parameters passed by address are considered as fixed arrays of the passed dimension and, therefore, will not be accepted after a (&) in an enclosed #import function call:
void function foo(float *p) { myfunction((&) p); } // not accepted : p is fixed array!
An example is implemented in demolib7.c
void foo(int *piDim,
float **ppfArg) {
...
ppfArg[1][0]
= 47.11; // write something into scalar argument #1
...
}
is the implementation of an #import library function foo(float x, float y, float z), the writing of a value into the argument parameter y (argument #1) will not have any effect for a "normal" function call. However, the prefix "(&)" before any scalar argument will make any assignment of a new value, as in the above example, visible for the caller:
y = 3.14;
foo(x, y, z);
// y still 3.14
foo(x,(&) y, z);
// now y=47.11
This allows to implement functions such that they
optionally (i.e., when the caller uses the "(&)") can
pass back result values through their scalar argument parameters.
NOTE: for implementation reasons, the use of the "(&)" before
a scalar function parameter is restricted in two ways: (i),
the parameter must not be an array element (such as a[4] etc.) and
(ii), the parameter must not be the first occurrence (and thus its
implicit definition) of the variable: the variable must already have been
defined before!
char *FUNCTIONS_nnnn[] = {
"_1_:", "fun(int)",
"_2_:", "fun(float)",
"_3_:", "fun(int,float)",
NULL
};
float _1_fun(int *piDim, void **ppvArg) { ... }
float _2_fun(int *piDim, void **ppvArg) { ... }
float _3_fun(int *piDim, void **ppvArg) { ... }
(if no return type is specified for a function in the FUNCTIONS_nnnn line, the default is float). The prog_unit parser will ensure that each function is called precisely with those arguments that are specified in its parameter list in the FUNCTIONS line, i.e. _1_fun will get passed a single int, _2_fun a single float etc. File demolib2.c contains also the above example, and example#2 is a prog_unit that issues some test calls of these and some other functions.
Note: do not define overloaded functions with the same name, but different return types! Currently, this is not checked, but it may cause problems in certain cases.
The choice of the correct function is made according to an analogous set of rules as familiar from C++. One of these rules is that differing return types cannot be used for distinguishing between different prototypes.
char *FUNCTIONS_nnnn[] = {
"variadic1(int, float*, ...)", /*
comma separated ... */
"variadic2(int, float* ...)",
/* no comma before ... */
"variadic3(int, int
...)", /* variadic integer parameters */
NULL };
Here, a comma-separated ellipsis without any
preceding type specifier (as in variadic1)
specifies any number (including zero) of trailing read-only float or
float* arguments (note that the prog_unit allows to supply
also a char* or byte* parameter for a float* argument;
in this case, there will occur an automatic conversion to a float*,
i.e., the called routine need not care and can rely on getting the data as
a float* array in any case). In contrast, a non-comma-separated
ellipsis (as in variadic2) specifies any number (including zero)
of trailing readable or writeable array parameters (i.e., now scalars are
excluded!) of the type to which the ellipsis were appended (here float*).
Finally, the "int ..." specifies a variable
(including 0) number of trailing int parameters (currently, for scalar
data types this is only provided for integers).
Variadic functions require a prototype ident(int,int*,void**) with an additional argument parameter that specifies the actual number of arguments in the call, e.g.:
float variadic1(int iNumArgs, int *piDim, void **ppvArg) { ... }
char *FUNCTIONS_nnnn[] = { ...
"void foo(int, (*)(float,
char*), (*)(void))",
... }
defines foo() as an #import function with an int argument and two further function arguments. The argument parameters of the first function are float and char*, the second function is parameterless. The implementation for foo() takes the form
void foo(int *piDim, void **ppvArg) {
int *piArg0 = (int*) ppvArg[0];
float (*pFunctionArg1)(double,...) = ((float(*)(double,...))
ppvArg[1];
float (*pFunctionArg2)(void)
= ((float (*)(void)) ppvArg[2];
...
pFunctionArg1(2.45, "hello
world!"); // using function arg 1
pFunctionArg2();
// using function arg2
...
}
i.e., just picks up from ppvArg[] the corresponding function pointers and uses them (the gray parts cast to the correct pointer type and are not really needed).
Important note: the passed function pointers must be declared as variadic function prototypes that use double arguments in all positions were a float parameter was declared!
When foo is used at the prog_unit level, the supplied function
parameters may be either #import library functions (including class member functions), or functions
define in the source code of the prog_unit, as well as most of the
fixed builtin functions (such as sin, cos, ...
but not, e.g., exec_opnds() [but you can pass such functions enclosed
in a wrapper routine]). However, NST transports for all array function parameters
also their dimension. Therefore, when a function parameter has arrays (other
than char*,
which are in this context expected only to be filled with null-terminated
strings whose length+1 is then taken as the dimension) among its
arguments, e.g.,
....
"foo2(int, (*)(float,byte*), (*)(float*)",
...
the implementation will get passed pointers to "augmented" function prototypes that have for each array argument an additional int argument (in front of the array argument) that expects the array dimension when the function is called. E.g., the foo2 implementation would look like
void foo2(int *piDim, void **ppvArg) {
int *piArg0 = (int*) ppvArg[0];
float (*pFunctionArg1)(double,...)
= ((float(*)(double,...)) ppvArg[1];
float (*pFunctionArg2)(int,...) = ((float (*)(int,...))
ppvArg[2];
...
pFunctionArg1(2.45, 10, pcArrayOfDim10);
// using function arg 1
pFunctionArg2(5,pfArrayOfDim5);
// using function arg2
pFunctionArg2(0,NULL);
// using function arg2 another time
...
}
Note that these additional dimension parameters do not appear in the implementation of the passed functions themselves (e.g., in their source text in the prog unit); they only appear when a function (such as foo2) references them via its (foo2's) parameter list. Note also that float* arrays are not transformed into double* and that byte* gets mapped to char* and differs from char* only in that it makes no assumptions about null-terminatedness and thus needs the extra int parameter to request a dimension value.
Passing overloaded functions. When in a call, e.g. of foo(),one or more supplied function parameters are overloaded functions that can come in different signatures, the compiler needs a specification which signature shall be chosen from the alternatives. The specification takes the form of a type list appended to each overloaded function parameter that is supplied, e.g., a call of foo() would then have to look like
foo(42, fun1(int,char*), fun2(void));
char *foo_c[] = {
"L_:", //
prefix against name collision
"foo(int)", // class constructor
"~foo()", // class destructor
(mapped to "FREE_foo()")
"float fX", // a float variable
"int iX", // a int variable
"readonly int iY",
// as iX, but forbids write access
"float* pfFloat", // a float array
"void change(int)", // an exported method
NULL};
char **CLASSES_nnnn[] = {foo_c,NULL};
After an #import "nnnn" directive in a prog_unit, instances of the class can then be declared with
class foo a(5), b(10);
and used in statements such as
a.iX = 15; b.fX = a.fX + a.iY + 1;
for (i=0; i <dimof(a.pfFloat); i++) a.pfFloat[i] = i;
b.change(3);
a.iY = 3; // SYNTAX ERROR, since iY was def'd as 'readonly'
Scalar element variables may be defined with the optional type qualifier readonly (e.g., iY in the foo example); this then forbids to make direct changes at the level of the prog_unit source code. However, such variables can still be changed through class methods. This is useful, e.g., when the allowed changes to one or several class parameters must obey additional constraints which can then be enforced by the class methods, but might be violated by direct changes.
A library nnnn may contain the definitions of classes (CLASSES_nnnn[] array) and the definitions of functions (FUNCTIONS_nnnn[] array) simultaneously.
Class arrays. Of any imported class, one can define resizeable arrays:
class foo b[10]; // array of 10
unistantiated elements: different from b(10)!
...
for (i=0; i<10; i++) b[i] = foo(5);
b[4].pfFloat[2] = 4711;
b = resize(2*dimof(b)); // now 20
elements, the last 10 uninstantiated
...
Autoloading of classes: Although the CLASSES_nnnn[] array can specify the definitions of several classes within a single library nnnn, it is recommended to always define only a single class per library and to choose for the library base name the same name as for the class (e.g., "foo" here; the name of the class as used by the prog unit is determined by its constructor function, cf. below). This will then allow the prog_unit to automatically load the required library when a class declaration is encountered without a previous, explicit #import statement (but note that currently only the #import statement allows to select a particular directory to pick the library from; the automatical loading of classes simply uses the first matching library that it can find in $NST_LD_LIBRARY_PATH).
// the constructor:
class foo *L_foo(int *piDim, void **ppvArg) {
int *piArg0 = (int*) (ppvArg[0]);
// create C++ class instance & return
handle:
return new foo(*piArg0);
}
// the destructor:
void L_FREE_foo(class foo *pData) {
if (pData) delete pData; // must permit
NULL argument!
}
// the class method change(int):
void L_change(int *piDim, void **ppvArg, class foo *pData)
{
int *piArg0 = (int*) ppvArg[0];
// call corresponding member function of
instance pData:
pData->change(*piArg0);
}
A full implementation can be found in file demolib3.C.
Note that the destructor is expected to always be implemented under the name
FREE_xxxx (even though declared as "~xxxx()") plus any
additionally specified prefix. Note also that here and everywhere else the
name of the C++ class need not coincide with the class name chosen for the
prog_unit import; however, to avoid too many general identifiers, we
will stick with the same identifier for both. Note also that class member
functions, including the
constructor, can be overloaded, as explained for FUNCTION's (an
example is given below);
float *L_fX(int *piDim, class foo *pData) { return &(pData->fX);
}
int *L_iX(int *piDim, class foo *pData) { return
&(pData->iX); }
float *L_pfFloat(int *piDim, class foo *pData) {
*piDim = pData->iFloatDim; return pData->pfFloat;
}
Note that the actual allocation of array space is expected to take place in the constructor call and that the registration functions must rely on the C++ class handle to gain access to the dimensions of the array variables that are to be exported for the prog_unit (if the class is not designed to store such information, it may be necessary to work with a derived class that contains storage for the additional auxiliary information).
The registration function must be provided even if an element variable was defined as 'readonly'.
void (*L_UPDATE_foo)(void) = NULL;
and any class method call that has redimensioned one of the exported array variables must issue a call (*L_UPDATE_foo)() before it returns (actually, this call must be made after both the redimensioning and the update of any dimension information accessed by the array registration functions (here: L_pfFloat()) has occurred; behind this is the following mechanism: when the prog_unit calls a member function of a class xxxx, it will set its UPDATE_xxxx function pointer to a suitable notifier function that re-registers all array variables). I.e., the implementation of our example class "foo" now becomes augmented as follows:
char *foo_c[] = { ..., "UPDATE_foo()", ... };
...
void (*L_UPDATE_foo)(void) = NULL;
...
void L_change(int *piDim, void **ppvArg, class foo *pData)
{
int *piArg0 = (int*)
ppvArg[0];
// now assumed to change
the dimension of pfFloat array:
pData->change(*piArg0);
(*L_UPDATE_foo)();
// the newly added update call
}
...
Note that resizeable array variables should always be declared with the asterisk (e.g., float *foo), never with the brackets (e.g. float bar[]), since the system will take the [] as the assertion that the variable is not affected by the UPDATE function (cf. the remark above).
How to allow resize operations from the prog unit level: these require two additional things: (i) the corresponding array variable (say, myvect) must be prefixed with the (non-standard) type qualifier "dyn" [experimental: don't use yet!] and (ii) the implementation must provide a different registration function void **L_DYN_myvect(int **,class*) that is described more fully below:
char
*foo_c[] = { "L_:", ... "dyn float *myvect", ... "UPDATE_foo()",
... }
...
void
**L_DYN_myvect(int **ppiDim, class foo *pData)
{ .... }
char *LOAD_xxxx(FILE *fp, class xxxx *pHandle) { ... }
char *SAVE_xxxx(FILE *fp, class xxxx *pHandle) { ... }
The return value of these functions should be NULL in case of proper operation, or a char* pointer to a statically stored error message otherwise. The expected semantics is that "LOAD_xxxx" will read and restore precisely what "SAVE_xxxx" has written about the class instance. It is allowed to have neither, one or both functions present. E.g., if there is only a SAVE_xxxx function implemented, all circuit files will contain saved information about the class instances which is not used during loads, but which may become useful later, when the missing LOAD_xxxx function is added to the implementation.
Note that data of an instance are only saved when the instances has been declared as static class.
If the class shall allow that static class variables can be uninstantiated (e.g., declarations such as static class xxxx p;), the class needs instead of the LOAD_xxxx function the implementation of a somewhat "stronger" RESTORE_xxxx function that can even create instances according to previously saved data in a file. To this end, its second parameter is not the pointer pHandle, but instead the address ppHandle of a pointer pHandle:
char *RESTORE_xxxx(FILE *fp, class xxxx **ppHandle) { ... }
When *ppHandle!=NULL, The required behavior must be identical to what would be expected of SAVE_xxxx(fp,*ppHandle). Otherwise, when *ppHandle==NULL, the RESTORE_xxxx method must create an new instance according to the read data and replace the NULL value in *ppHandle by the address of the created instance.
INP class foo x(10); OUT class foo y(20);
With the above declaration, the class instances would
appear at the Neo level as connectors of the user-defined type type
"special" that can, e.g., be connected with wires, if they hold compatible
instances of the same class (cf. below).
The user-defined "special" connectors are always
opaque, i.e., a Neo wire can only transmit the entire connector contents
and not any element variables selectively.
New: It is also possible, to make
scalar or array members of a class selectively visible as output pins of
corresponding
type. For instance, if foo has element
variables float vec[] and int num, these can be declared as
OUT float y.vec[]; // yields
array pin
OUT int y.num; // yields scalar pin
Alternatively, vec[] could also be made visible as a dynamic output
pin:
OUT float *y.vec; // yields
dynarray pin
(if vec were declared as float *vec in the class interface, only this second
possibility would exist).
Note that the mapping of members is restricted to OUTput pins. Also, mapping
some element variables of a class instance may restrict some operations on
it, such as, e.g., reassignments or re-instantiation (the latter only, if
arrays are mapped to const dim pins).
To create instead non-opaque connectors in which the element variables appear as separate pins of a composite field you can use the declaration
INP class foo #x(10); OUT class foo #y(20);
The difference between opaque and non-opaque connectors is that the former transmit the entire state information of a class instance, while the latter transmit only the part that is captured in the element variables of the class that are visible at the prog unit source level, but with the benefit of making each element variable accessible for the Neo wiring level.
To permit the use of class instances as connectors requires the definition of a further special member function "COPY_xxxx()". It must be implemented as
class xxxx *COPY_xxxx(class xxxx *pTo, class xxxx *pFrom) { ... }
and the required semantics is to return the handle of a copy of instance pFrom (or a make a default instance, if pFrom=NULL, which must be a permitted argument value). Moreover, when pTo != NULL, pTo will reference another class instance whose ownership is passed to the routine so that *pTo can be used to reduce any memory allocations/reallocations to make the copy (in one extreme case, it suffices to adjust some parameters of *pTo and return pTo as the result; in the other extreme, *pTo is of no use at all and must then be deleted [since ownership has been passed to the routine!] while an entirely newly built copy is returned). If a COPY_xxxx function has been defined, existing class instances can also be assigned to each other by statements such as
y = x; // y must already exist as class foo instance!
Example#3 illustrates the above with two prog_units each of which imports the class foo from example library demolib3.so. The left prog_unit instance has a class instance as its output connector. Each call assigns to the class member variables of the class instance new values, including a call of the member function change() to redimension the array variable pfFloat. The right prog_unit accesses this class instance over a wire and prints the newly set values.
Pin types for non-opaque connectors: in this case, all float*, int*, byte* and char* element variables are mapped to of the corresponding array type. Alternatively, it is also possible to map each * element variable to a dynamic pin by providing adifferent registration function. If the name of the variable is "varname" , the required alternative registration function must be of the following form:
void **DYN_varname(int **ppiDim, class xxxx *pData)
Here, DYN_ is a fixed additional name prefix (any further, user-defined prefixes must be prepended to DYN_). The registration function itself is similar to the registration function that we know so far, but with one additional level of indirection: it must return the address where the address of the start of the array is stored, and *ppiDim must be set to the address of an int variable that holds the dimension of the dynamic array. This then allows the implementation to work with the element variable arrays in the usual way (i.e., ownership of the data is always with the implementation).
For variables that use a DYN_ registration function, any resizes can (in fact, should!) be made without a call to an UPDATE function, provided the new variable address and dimension are written into the locations whose address was registered with the DYN_varname function. However, there may be situations when a resize would lead to undesireable effects (e.g., when the class instance is used as an INP variable). To safeguard agains such situation, the implementation should equip each class xxxx that uses DYN_ variables with an indicator variable int RESIZE_xxxx. When such a variable is defined, the prog unit will set it to 1 in all situations when a resize of any of its DYN_ variables is permitted, and to 0 otherwise. The necessary definition looks as follows:
char *xxxx_c[] = { // xxxx denotes the class
name
"myprefix_:",
... the usual definitions ...
"myprefix_RESIZE_xxxx", // the special RESIZE_xxxx
element variable
NULL
};
int myprefix_RESIZE_xxxx; // the implementation
Any resize operation to a DYN_ variable of class xxxx should then be bracketed in the form
if (myprefix_RESIZE_XXXX) { ... the resize op ... } else { ... diagnostic message ... }
Note that the RESIZE_xxxx element variable is invisible at the prog_unit level and - unlike ordinary class variables - it needs no registration function.A class implementation for which this second registration function is missing for one or more of its array element variables can have its instances only declared in the opaque mode.
Example testdm.c illustrates this with a simple example. A more comprehensive example is the implementation of the dm library that implements the NST datamining connector as a prog unit class data type.
Compatibility control for class assignments: Finally, a (optional) further function "COMPATIBLE_xxxx()" can be specified to limit class assignments as the above to instances that are "sufficiently similar" to be semantically sensible. It must be implemented as
int COMPATIBLE_xxxx(class xxxx *pX, class xxxx *pY) { ... }
and should return 1 when x may be assigned to y (after which y is a copy of x), 0 otherwise. A corresponding restriction will be placed on the connectability of two opaque class connectors.
char *DESCRIBE_xxxx(class xxxx *pHandle) { ... }
one can define the contents of the popup-menue that appears when a class connector is selected at the Neo level in info-mode. The return value should be a pointer to a string (ownership is not passed to the caller) containing some useful description of the instance referenced by pHandle. In the absence of a DESCRIBE_xxxx definition, a default description is constructed.
char *drawing_c[] = {
"picture(void)",
"_0_:", "void add(class)",
"_1_:", "void add(int, class line)",
"_2_:", "void add(class point ...)",
NULL};
char *line_c[] = { ... };
char *point_c[] = { ... };
char **CLASSES[] = {drawing_c, line_c, point_c, NULL};
Here, "add(class)" (to be implemented under the name _0_add()) is interpreted as "add(class picture)", while the second function (to be implemented under the name _1_add()) expects an integer followed by a handle to a class line instance. The last function (to be implemented as _2_add()) is variadic and expects zero or more handles to class point instances. Example#4 is a prog_unit that imports the above classes from demolib4.so (make it by issueing make demolib4.so) and issues some calls that print messages about their call parameters (demolib4 provides no actual implementation of the suggested operations; each routine is a dummy that just prints information about its arguments).
Class arguments are passed by reference, i.e., the implementation function will get passed the address of the instance and the parser will have ensured that the address points to an object of the correct type expected at its argument position.
Note that it is legal to specify functions with arguments that are classes defined in other #import packages. However, in this case the class name must include the namespace prefix of the #import package from which the class is to be taken (the namespace prefix is separated with a single dot from the base name (e.g., class mylib.foo); if the package makes no explicitnamespace definition, the base name of its implementation file will be the default).
A class member function can also pass as return
value an instance of a different class. This requires a declaration of the
form
"class classtype
my_class_valued_function(...)"
where classtype must be the qualified name
of the return class type (if the return class type is the same class of which
the function is a member, the classtype identifier may be omitted).
Similar remarks apply to class valued plain (ie., non-member functions).
The implementation of a class-valued
function must return a pointer to a newly created object of the appropriate
type and ownership of the object must be given to the caller.
char *Foo_c[]
= {
"TEMPLATE<T2,T5>", // defines two type parameters T2 and T5
"foo(int n)", // constructor
"fun(int i, T2 x, T5 y)",
"T5 fun2(z),
NULL
};
... implementation of member functions (cf. below)
Class foo offers two member functions that contain variable parameter types T1 and T2 in addition to the fixed parameter type int (for a worked example, see file texample.c and circuit texample.NST).
A prog_unit wishing to create a foo instance must specify for each type variable Ti the actual type that is desired for this variable. This is done in the familiar C++ syntax:
class <hello,world> foo f(5),g(3); // choose T2=hello,
T5=world for f and g
class <world,hello> foo h(7);
// choose T2=world, T5=hello for h
class world B(), I(); class hello F(); // example parameters
// some example calls:
f.fun(3, F, B);
h.fun(4, I, F);
Note the f,g and h are from the same class foo, but
instantiated to be used with different choices for the types T2 and T5.
Currently, allowable type parameters are restricted
to names of #imported non-template classes and to structs defined at the prog
unit level. An asterisk * in place of a type name may be used to
indicate that the particular template type remains unspecified. This will
restrict the callable methods of the instance to those that do not involve
any of the unspecified parametrized types (unless the constructor function
uses any parametrized type parameters among its arguments, it is allowed
to specify all type parameters as * which is equivalent to omitting
the <...> part altogether, or to omit a number of trailing
type parameters).
IMPORTANT: The semantics of passing parametrized type variables (both via argument or as a return value) is always by reference. This differs from the semantics of returning an array or object instance, which is by copy.
At the implementation level, names of parametrized types are currently restricted to T0..T9 (i.e., currently there cannot be more than 10 type parameters in a class). At the C-level, arguments or return values that are a parametrized type are treated as void* pointers. Since the template class shall work for any combination of choices for its parametrized types (even for types not yet known at the time of the implementation), parametrized type variables must be considered as entirely opaque (never try to implement any specific operation on them). The main use of a template class, therefore, is to provide organized structured storage and access to parametrized type instances (e.g., in lists, trees or graphs). To this end, the following three operations on parametrized types are provided:
FREE_T(void *x)
: indicate that the caller has no claims to access object x anymore.
y = BY_REF_COPY_T(y, x)
: replace an existing reference y to a parametrized type object
with the new reference x (all arguments and return values are of type void*).
Also use, if initially y=NULL.
void * COPY_T(void *x):
return ptr to a newly allocated copy of x.
Note that the implementation does not get passed pointers to the actual object instances, but instead to wrappers that contain additional information to allow FREE_T, BY_REF_COPY_T and COPY_T to work in a generic way, including management of ref counts (including references that may exist in the prog_unit code or other template instances) so that the persistence of all objects is ensured as long as there are still references around. To make this work, never make direct assignments, such as y = x, use y = BY_REF_COPY(y,x) instead! For examples, how to implement template classes, inspect demolib8.c (test circuit is ClassArg.NST); larger examples are in the source structures.c of the library structures.so which uses the flexibility of the template mechanism to provide a general List, Dictionary and Graph data type.
To make them more useful, template class functions
may declare among their parameters functions that use the parametrized types
also among their arguments. Example:
...
"void foofunction(int i,
float (*funarg)(T0,char*,T1), float y)",
...
This would define a member function whose middle parameter funarg is a function which expects as its first and last parameter the address of a type T0 and type T1 object, resp. (don't use the syntax T0* or T1* for this!). The main use of this is to permit the implementation of iterator functions that iterate a given function over a set of previously "incorporated" object(-references) of the parametrized type(s).
To return a parametrized type instance x (a void* pointer), simply end the method by return x; (neither do a copy nor set piDim[-1]).
Examples: demolib8.c and circuits TemplateDemo-1.HNST and TemplateDemo-2.HNST
Ensuring type compatibility: for template
classes, type compatibility checks (e.g., for connecting pins) must also
include checking for matching type argument bindings. This cannot
be done at the NST level alone, the implementation has to help a little:
1. provide a char *pcTypeInfo variable in the object struct
(e.g. foo_t), 2. within the constructor function, initialize this
variable with the line
pObj->pcTypeInfo = TEMPLATE_T(); // private ptr, don't free
later!
(function TEMPLATE_T() is a new predefined system function). 3. Define a "minimal" COMPATIBLE function as follows:
int COMPATIBLE_foo(foo_t *pObj1, foo_t *pObj2) {
return pObj1->pcTypeInfo == pObj2->pcTypeInfo;
}
and include it among the exported functions (an entry
"COMPATIBLE_foo()," in the method definition string list).
Steps 1-3 are only necessary for template classes. For
an example and more information, inspect file demolib8.c.
The pcTypeInfo variable can also be used for
a minimal implementatin of the DESCRIBE method:
char *DESCRIBE_foo(foo_t *pObj) { return pObj->pcTypeInfo; }
A final note: when implementing overloaded template classes, avoid overloading a class argument against a template type argument or vice versa:
"TEMPLATE<T0>",
...
"g1_:",
"void bad(class ex.ample x)",
"g2_:",
"void bad(T0 x)", // ambiguity, since T0 might become set
to class type ex.ample!
since a template type may be filled with an object
argument, this would cause an unresolvable ambiguity (there is currently no
automatic diagnosis and warning of such situations!).
char *derived_c[] = {
"EXTENDS
base FROM lib_of_base",
"derived(...)",
/* constructor of derived class */
...
declarations of further new functions/variables here ...
NULL
}
This defines a new class of name derived that extends the existing class base from #import library lib_of_base (specified without suffix). The "FROM ... " part is only needed, when the library name lib_of_base differs from the name base of the base class. The new class will appear at the prog_unit level as a class that has as its functions/variables the union of the functions/variables of the parent class(es) (in the example: class base) and the newly defined functions/variables (but the constructors of the parent(s) will no longer be accessible at the prog_unit level).
In the following, we explain the C-implementation. The data object of the derived class has the form (we assume that the base class uses the data type base_t for its objects):
#include "base.h"
typedef struct derived_ {
base_t base; /* instance
of the base class */
... further elements of class
derived here ...
} derived_t;
I.e., its initial part contains a base instance. Therefore, the derived() constructor(s) first call(s) a base constructor to make a base instance and then resize(s) the base instance to the derived instance to accommodate the remaining data while keeping the base data intact:
derived_t *derived(...arguments...) {
base_t *pBase = base(...arguments...);
/* make base instance first */
derived_t *pDerived;
pDerived = (derived_t*) realloc(pBase,sizeof(derived_t));
... initialization of additional
data in pDerived ...
return pDerived;
}
Likewise, the derive destructor first handles destruction of any new data in pDerived and then passes pDerived to the parent class destructor for the remaining clean up (note that the destruction of the enclosing memory block is always left to the top-level parent in the class hierarchy)
#include "base.h" /* expect FREE_base to be declared */
void FREE_derived(derived_t *pDerived) {
... free any dynamic elements
among the new variables in pDerived ...
FREE_base((base_t*) pDerived);
/* parent destructor does rest */
}
If the derived class introduces no additional dynamic variables (i.e., all new elements in the derived_t struct use only memory within the struct) it need not define an own destructor. In this case, it will automaticall inherit the parent class destructor which then is sufficient to handle the situation.
All other functions/variables are defined in the same manner as before
(each function will get passed a derived_t handle
as last argument, but can always downcast it to a base_t when access to the
base variables is desired).
For function parameters (but not in assignments), a derived class can always represent an instance of its parent class (but not vice versa), e.g., when a function expects a base parameter in the present example, it will also accept a derived parameter. This will never cause a problem, since a derived instance has all base elements as a proper subset and in the same offset locations from its start address.
If the derived class defines a member function that is identical both in name and parameter list with a function in the base class, it will replace this function. If it is identical in name, but differs in the parameter list, it will overload the existing function.
A derived class may only inherit from a single base class (i.e., multiple inheritance is not supported). However, the parent class may itself be a derived class, i.e., one can extend a class in several "layers".
Regarding the LOAD/SAVE methods, the derived class'es LOAD/SAVEfunction needs only to care about its newly defined variables; for the variables of the parent class there will be an automatic invocation of the parent class functions (if the parent class has no LOAD/SAVE functions defined, then, of course, it might be useful to include load/save of parent class variables also in the derived class).
If the derived class resizes any dynamic variables, it must define an UPDATE function pointer, and invoke the UPDATE function after each resize. This will then take proper care of resizes both of new and/or old variables. If the new functions contain no resize operations, no UPDATE function pointer is necessary (any resizes within the base class will be properly handled within the base class implementation and need not be considered in the derived class implementation).
Regarding the COMPATIBLE, COPY and DESCRIBE functions, so far nothing is inherited, i.e., these functions must be entirely newly defined, if needed.
A fully worked out example#5 is contained in the files demolib5a.c, demolib5b.c and demolib5c.c. Demolib5b.c defines a class picture with functions to add points and to print a listing of the added points (no real graphics is involved). Demolib5c.c extends this class by functions to add lines and to translate all objects in a picture. It also replaces the old list function by a new version that can also print the added lines. Demolib5a.c implements the auxiliary classes point and line used by the picture class and its extension. The header files demolib5a.h, demolib5b.h and demolib5c.h define the necessary C-structs and also illustrate how to embed version check info for the prog_unit (explained in the *.c files).
Using a derived class required also to have all definitions for its base classes in memory. The prog unit will try to load them automatically (cf. Autoloading of classes) when a derived class is used; this, however, requires that all base class libraries must be in the dependency list of the library of the derived class. I.e., the derived class should be compiled with a suitable linker option, for gcc this is
-Wl,rpath,BaseClassLib1,...,BaseClassLibK
where BaseClassLib1...BaseClassLibK is a comma-separated list
(including suffix) of all required base class libraries.
Additionally, the BaseClassLibs (with their absolute paths!) must
be specified among the objects that are linked into the library for the derived
class.
Otherwise, the libraries for the required base classes must be explicitly imported with suitable #import statements.
Currently, a #verbose listing of a derived class may during the
first instantiation list only the added elements. Subsequent instantiations
will produce a full listing (this is an artefact of a postponed instantiation
in the prog_unit that first tries to load all #import libraries
before instantiation of derived classes is attempted).
void CTRL_foo(int mode, class foo *pClassObj) { .... }
instances of a class foo can be made to
respond to NST control calls. The string declaration of this method must
be given in the form
...
"CTRL_foo()",
... further methods ...
i.e., type and arguments must not be specified (but are implied), similarly as for the other reserved methods (such as LOAD, SAVE, etc.; also any prefix handling is the same). For calls such as INIT or RESET that also cause execution of prog unit code, the calls (one for each class instance) to CTRL_foo() are issued before prog unit source code processing starts. You should not make any assumptions about the order in which the calls to the class objects are issued.
FUNCTIONS_nnnn[] = { ..., "_init(int)", "_fini(int)", ... }
and implemented as (note that you must not use the (int,void**) call interface here!)
void myprefix_init(int iArg) { ... }
void myprefix_fini(int iArg) { ... }
myprefix_init will be called whenever a prog_unit #imports the library, myprefix_fini will be called whenever the prog_unitinstance is deleted. To keep calls from different prog_unit instances apart, both functions get passed an integer argument iArg that is a unique identifier for the calling prog_unit (this is a new feature in version NST 7.1).
Backward compatibility remark: you also can use the old, parameterless function definitions
FUNCTIONS_nnnn[] = { ..., "_init()", "_fini()", ... }
with the corresponding implementation
void myprefix_init(void) { ... }
void myprefix_fini(void) { ... }
In this case, the prog_unit identifier can be obtained by calling the predefined function int nst_get_prog_unit_id(void) inside both functions (or any other function that is exported for the prog_unit).
The predefined function nst_prog_loadlib(pcName) can be called to ensure that another import library of name pcName (must include suffix) is imported.
prog_throw(char *pcErrorType, char*pcFmt, ... );
to "throw" a NST exception. This will cause an immediate backjump to the prog_unit invocation point of the function and from there a search for a suitable exception handler. The passed string pcErrorType is used to specify a particular exception type (NST exception handlers will inspect this string and decide on the basis of its contents whether to handle an exception or not). The recommended syntax for pcErrorType is
pcErrorType = "ident1:ident2:...identk"
where each substring identi consists of alphanumeric characters (no white space) only. The remaining arguments pcFmt,... are processed like in a printf() call to assemble an error message that is propagated with the exception. In the prog unit itself, exception handlers can be implemented in the form of catch-blocks that decide about acceptance of an exception on the basis of a const string argument, e.g.
// after call point
of #import function (possibly in more outer scope):
catch ("badop:index") {
... handler code
for all exceptions thrown with pcErrorType ="badop:index:*"
}
catch ("badres") {
... handler code
for all exceptions thrown with pcErrorType = "badres:*"
}
The advantage of this type of error handling is that the exception can progagate even more upwards into the NST execution chain where more global exception handlers can be positioned in the form of suitable NST units (e.g., other prog_units). If no handler is found, the exception will end with a printout of the supplied error text plus some additional information abouot the location where the error had occured. For more details, see chapter Runtime Error Handling and the prog_unit manual page.
static char *pcLibraryPath = ....;
static void *pLibraryHandle = NULL;
void _init(void) {
if (!pLibraryHandle) pLibraryHandle =
dlopen(pcLibraryPath,RTLD_GLOBAL|RTLD_NOW);
/* now functions in library pcLibraryPath
are visible */
...
}
void _fini(void) {
...
if (pLibraryHandle) dlclose(pLibraryHandle);
}
...
FUNCTIONS[]/CLASSES[] definitions
& wrappers for library functions
...
Instead of the _init() and _fini() functions, also their NST counterparts defined in the FUNCTIONS[] array can be used.
nst_prog_def_builtin_lib("nnnn");
so that the prog_unit can know that #import "nnnn" should not trigger a search for an external shared library, but should use the library handle of the main program instead. Newly added statically linked import libraries should be prefereably placed in the file nst_prog_libs.c, with a corresponding nst_prog_def_builtin_lib(...) call inserted in the initialization function init_builtin_libraries() in nst_prog_aux.c.
If the builtin library has the name of one of the compiled folders nst/neo_nnnn.c of Neo, its definitions can also be placed in the nst_nnnn.c file. Neo then will automatically issue the necessary registration call, so nothing needs to be added to routine init_builtin_libraries() in this case (Neo automatically issues a registration call nst_prog_def_builtin_lib("nnnn") for each compiled folder "nnnn" during startup).
The purpose of builtin import libraries is to make some prog_unit functions relyably available, without needing to care about the presence of shared objects (this, e.g., simplifies the writing of network portable NST programs for the nstplugin).
2. Rule 1 does not apply to the special class member functions (such as
FREE_nnnn, SAVE_nnnn, COPY_nnnn etc). These are always defined with an empty
bracket, but have non-void, prespecified parameter lists, as explained
above.
Rule 1 also does not apply to the oldstyle definition "foo<0>"
(not explained in this tutorial, but in the prog_unit man page, now deprecated
and not commented any further).
3. Data of a class instance are not saved, although a SAVE_nnnn method is defined: probably the keyword static is missing in front of the class declaration.
4. When the return types of the implementation of #import functions do not coincide with the specified return type in the FUNCTIONS[] or CLASSES[] array, strange errors may result in apparently very unrelated places. For the same reason, it is important that function pointers passed via the ppvArg array are accessed with the correct function type.
For backward compatibility, float*, byte* and char* (but not int*) variables are interchangeable as function arguments. This is very different from C and involves an invisible copy-conversion operation, when one in the pair is a float*. For new work, the same effect (without the copying overhead) can be achieved with overloading.
For additional information, see the prog_unit manual page. It also describes an (older)
shorthand notation (deprecated!) for the function interface specifications,
with some additional possibilities (deprecated!).