14.2 How fsc2's variables work and how to use them
All functions in a module that are going to be invoked from EDL
scripts get their input parameters in the form of a special type of
variable and fsc2
also expects that each function returns a
value in this form.
Let's start with a look at the way fsc2
internally stores these
variables. Here is the (actually somewhat simplified) typedef
of the structure for such variables:
typedef struct Var { Var_Type_T type; /* type of the variable */ union { long lval; /* value of integer values */ double dval; /* value of float values */ long * lpnt; /* pointer to integer arrays */ double * dpnt; /* pointer to floating point arrays */ char * sptr; /* for string constants */ struct Var ** vptr; /* for array references, used for multi-dimensional arrays */ } val; int dim; /* dimension of array */ ssize_t len; /* length of array */ struct Var * next; /* next variable on stack */ } Var_T; |
There are only six types of variables (defined by an enumeration with
type Var_Type_T
) you have to know about:
INT_VAR | a variable for integer values |
FLOAT_VAR | a variable for floating point values |
INT_ARR | a one-dimensional array of integer values |
FLOAT_ARR | a one-dimensional array of floating point values |
INT_REF | a 2- or more-dimensional array of integer values |
FLOAT_REF | a 2- or more-dimensional array of floating point values |
Actually, there are a few more, but those are only used by fsc2
internally. If you test for a variables type in an switch
you
may get compiler warnings because Var_Type_T
is an enum
.
To avoid the warning simply add a default
for all the other
types, you should never receive a variable of such a type and if you
should do you should throw an exception (see below) - for that to
a serious bug in fsc2
must have been triggered.
To give you a better idea what these variables are good for let's assume that you want to write a function that returns the curve between the two cursor bars of your new LeCronix digitizer. For that you may want to write a function that has the two positions of the cursor bars as input arguments and which returns the data of the curve between the cursor bars. Let's call this function
digitizer_get_curve_between_cursors( cursor_1, cursor_2 ) |
A typical C declaration for this function is
Var_T * digitizer_get_curve_between_cursors( Var_T * var ); |
Perhaps surprisingly, there seems to be only a single argument! And how to return an array of data?
Actually, it's not very complicated. The pointer to the variable
structure var
points to the variable with the first of the two
parameters. And if you look back at the typedef
for
fsc2
's variables above, it contains a next
pointer. This
is the key to accessing further function arguments - var->next
points to next of the input parameters. If the function expects
further arguments, var->next->next
etc. let's you get at
them. I.e. the input variables are organized as a linked list:
var pointer passed to function | | V | --------------- V | | next | first input parameter --------------- | | | V | --------------- V | | next | second input parameter --------------- | | | V V NULL no more parameters... |
This method allows to pass the function an arbitrary number of
arguments and you can check how many you've got by simply counting
while following the pointers until the next
pointer of a
variable is NULL
.
When you later tell fsc2
about the function (by adding it to
the `Functions' file, see below) you can explicitely state if the
functions allows a variable number of arguments or only a certain
fixed number. A function that only accepts e.g. 3 arguments will
always get 3 - when the EDL
function is called with less
arguments an error message gets printed and executing the EDL
script is stopped. And if it is called with too many arguments the
superfluous ones are discarded and an error message is printed before
your function gets invoked with the correct number of arguments.
But in case you defined the function to accept a variable number of arguments you probably better check in your function that there aren't too many and if necessary print out a warning.
One word of warning: Never ever change the variables you get
passed to your functions in any way, especially the
next
-pointers! Even though the variables get thrown away
automatically when you return from the function changing something
within the variables may break the mechanism for clearing up the
variables and lead to all kinds of weird errors.
What fsc2
can't check is if the arguments it passes to your
function have the types you expect them to have. Let's assume that you
expect two integer values. What you should do first is to check if the
parameters you got are really integers. There is a function that can
do this for you, vars_check()
.
All you have to do is to call vars_check()
with the pointer to
the variable and the type you expect it to have, e.g.
vars_check( var, INT_VAR ); vars_check( var->next, FLOAT_VAR ); |
If vars_check()
finds that everything is ok it simply returns,
otherwise an error message will be printed (telling the user that a
variable of an unexpected type was detected in the function call) and
the program stops, so you don't have to take care of error handling.
If you're prepared to accept integers as well as floating point
numbers, call vars_check()
instead with
vars_check( var, INT_VAR | FLOAT_VAR ); |
i.e. just logically or
the types of variables you're prepared
to accept in your function.
You can also check if the argument is a string by testing a type
of STR_VAR
, i.e.
vars_check( var, STR_VAR ); |
And the same holds, of course, for variables of type INT_ARR
,
FLOAT_ARR
, INT_REF
and FLOAT_REF
.
vars_check()
not only checks that the variables has the correct
type but also does some internal consistency checks to make sure that
the variable actually exists and has been assigned a value. Failure
of these tests also lead to the function not returning and instead
aborting the execution of the EDL
script.
A function that expects just integer arguments could start like this, just running through the linked list of parameters:
Var_T * my_function( Var_T * var ) { Var_T * cur; for ( cur = var; cur != NULL; cur = cur->next ) vars_check( cur, INT_VAR ); .... } |
The next question is how to access the value of the variable. As you
can see from the typedef
for variables above the value is
stored in a union
called val
. If the variable has
integer type, you can access it as
var->val.lval
or the macrovar->INT
and what you get is a value of type long int
- fsc2
is using long integers internally. On the other hand, if the type of the
variable is FLOAT_VAR
you get at the data with
var->val.dval
or the macrovar->FLOAT
in which case you get a value of type double
. Finally you may use
var->val.sptr
orvar->STRING
to get the address of the start of a string variable.
• Utility functions to determine variables values | ||
• Getting at the data of one-dimensional arrays | ||
• More-dimensional arrays | ||
• Returning data from EDL functions |
14.2.1 Utility functions to determine variables values
There are some utility functions that make it even easier to evaluate
the parameters your function receives (and, if you use those, you
don't need to check them with the var_check()
function). The
first one is for the case that you expect an integer variable but
would also be prepared to deal with a floating point number after it
has been rounded to the nearest integer. This is the function
get_long()
, declared as
long get_long( Var_T * var, const char * snippet ); |
The first argument is a pointer to the variable you want to evaluate.
The second parameter is used to create a warning message when the
variable isn't an integer variable but a float value. This message
always starts with the name of the currently interpreted EDL
file, followed by the line number in the EDL
script your
function was invoked from and the device name. The second parameter is
a string that gets embedded into the message. For example, if the
currently interpreted EDL
file is named `foo.edl', the
line where your function is called is 17 and the device name is
LECRONIX
, your function (that expects an integer but got a
floating point number) is named abc()
and the string you pass
to the function get_long()
as the second argument is
"channel number"
, then on calling
get_long( var, "channel number" ); |
with a FLOAT_VAR
instead of an INT_VAR
the following
warning message will be printed:
foo.edl:17: LECRONIX: abc(): Floating point number used as channel number. |
If, on the other hand, you expect a floating point number but are also prepared to accept an integer, you can use the function
long get_double( Var_T * var, const char * snippet ); |
The arguments of this function are the same you would pass to the
previous function and the only difference is that it will return a
double
and print a warning message if the variable is an
integer variable instead of the expected floating point variable.
If your function can accept an integer variable only there's a third function:
long get_strict_long( Var_T * var, const char * snippet ); |
This function has the same arguments as the two other functions but it
will throw an exception (see below what this means) when getting
passed a floating point number, stopping the interpretation of the
EDL
script. That's actually only true for the test run, during
the real experiment only a warning message is printed and the floating
point number is converted to the nearest integer, which then is
returned, thus avoiding the termination of a running experiment. But
usually the wrong parameter should already have been found during the
test run, forcing the user to correct the script.
There's no function for requirung a double variable only because
basically all functions should accept an integer in place of a double
and the get_double()
function always returns a double, if
necessary after converting the integer value.
Then there is a function for the case where you want a boolean variable, i.e. a variable that can be either true or false. This function is declared as
bool get_boolean( Var_T * var ); |
This function will return true (i.e. a value of 1
) when the
variable passed to it is either an integer variable with a non-zero
value or a string variable with the string "ON"
(it is
case-insensitive, so "on"
, "On"
or even "oN"
will
also do). False (i.e. 0
is returned when it receives an
integer variable with a value of 0
or a string with the text
"OFF"
(again this is checked in a case-insensitive manner).
If the variable passed to the function is a floating point variable normally an error message like
foo.edl:17: LECRONIX: abc(): Floating point number found where boolean value was expected. |
is printed and an exception is thrown. Should fsc2
already be
running the experiment (instead of just doing the test run) a warning
message is printed and instead of terminating the experiment the
floating point value is converted to the nearest integer value and the
truth value of this number (i.e. if it's non-zero) is returned to
avoid stopping the experiment.
Finally, if the get_boolean()
function receives a string variable
that is neither "ON"
nor "OFF"
(including variations of
the case of the characters) an error message is printed:
foo.edl:17: LECRONIX: abc(): Invalid boolean argument ("bla"). |
(assuming that the string passed to the function was "bla"
) and
an exception is thrown in all cases, even during the experiment since
there is no obvious way how to define the truth value of an arbitrary
string.
14.2.2 Getting at the data of one-dimensional arrays
When a complete one-dimensional array gets passed to your function the
type of the variable is either INT_ARR
or FLOAT_ARR
. As
for single value variables you can check these variables by calling
vars_check()
.
You can find the length of the array by checking the len
part of
the variable structure. Dynamically sized arrays can have a still
undefined length, in which case the len
field has a value of
0
, make sure you check for this possibility in your code.
The actual data of the array can be accessed via the lpnt
or
the dpnt
elements of the union
named val
in the
variables structure. When you have to deal with an array of integer
values (i.e. a variable named var
of type INT_ARR
) the
values (of type long int
) are in
var->val.lpnt[0]
to var->val.lpnt[var->len - 1]
.
For an array of floating point numbers the values (of type
double
) are stored in val->var.dpnt[0]
to
var->val.dpnt[var->len - 1]
.
14.2.3 More-dimensional arrays
For arrays of 2 or more dimensions (i.e. variables of type
INT_REF
and FLOAT_REF
) the rank of the "matrix" is
stored in the dim
field of the variables structure. fsc2
doesn't have real more-dimensional arrays but fakes them in a similar
way like e.g. Perl by using arrays of pointers. For the data of such
arrays the vptr
field in the val
union of the variable
structure is relevant. vptr
points to an array of variable
pointers, each pointing to the next lower-dimension sub-arrays. How
many of such sub-arrays exist can be determined from the len
field of the variable structure. As already for the one-dimensional
arrays care has to be taken to check that len
isn't 0
in
case none of the sub-arrays have been defined yet for variable sized
arrays.
If the rank of a variable is 2 all the sub-arrays are one-dimensional
arrays (i.e. have a type of INT_ARR
or FLOAT_ARR
and
one can the values of these sub-arrays as described in the previous
section.
For arrays of higher rank the pointers in the val.vptr
array
point to variables that again have a type of INT_REF
or
FLOAT_REF
and a rank of one less.
Thus to find out the element [3][2][5]
of variable var
pointing a three-dimensional floating point array one would have to use
var->val.vptr[ 3 ]->val.vptr[ 2 ]->val.dpnt[ 5 ] |
Of course, before one tries to access the element one always should
check that var->len
is at least 4
,
var->val.vptr[3]->len
is at least 3
and
var->val.vptr[3]->val.vptr[2]->len
is at least 6
.
But there's also a function that does all the required checks
automatically and returns an element. It is called get_element()
and is declared as
Var_T * get_element( Var_T * v, int len, ... ); |
It takes a variable number of arguments (but requires at least three). The first one is a pointer to the array or matrix from which you want an element and the second is the number of arguments to follow. This number may not be larger than the rank of the array or matrix. All the remaining arguments are indices into the array or matrix.
The function returns on success a variable that is either an integer or float value or a pointer to an array or matrix. On failure (e.g. because the accessed element does not exist) an exception is thrown.
If you have a 1-dimensional array of integers named i_arr_1d
and you want to determine its fourth element you can call this
function like this
Var_T elem = get_element( i_arr_1d, 1, 3 ); |
and the returned pointer to a new fsc2
variable named
elem
will be of type INT_VAR
, having the value of the
fourth element of the array (please remember that array indices start
at 0
, you're now writting C
not EDL
, so the
fourth element has the index 3
)
If, in contrast, you want the element [3][2][5]
of the variable
f_arr_3d
, pointing to a three-dimensional floating point matrix,
you would call the function as
elem = get_element( f_arr_3d, 3, 3, 2, 5 ); |
You can also call the function with less indices than the rank of a matrix. In this case the function returns a variable pointing to the indexed array or sub-matrix.
14.2.4 Returning data from EDL functions
If your function just wants to return an integer or a floating point
value, things are very simple: just call the function
vars_push()
with the type of the value to be returned as the
first and the value itself as the second argument, e.g.
return vars_push( INT_VAR, i_value ); |
or
return vars_push( FLOAT_VAR, f_value ); |
where i_value
is a long int
and f_value
must be a
value of type double
(take good care not to get this wrong!).
Of course, you don't have to use vars_push()
in return
statements only, it simply returns a pointer to a new variable holding
the value.
For arrays vars_push()
the first argument is either
INT_ARRAY
or FLOAT_ARRAY
, The second argument is a
pointer to the array (i.e. its first argument). When creating
an INT_ARR
this array must be an array of long int
values, for creating a FLOAT_ARR
it has to be an array
of double
values. (mak sure that this is the case since
the vars_push()
function can't test for this an neither
can the compiler!_
For creation of an array variable also third argument is needed, the
length of the array (which must be a long
integer). If you want
to return an EDL
variable that is an array of two integer you
would use for example
long int data[ ] = { 1, 2 }; return vars_push( INT_ARRAY, data, 2L ); |
Keep in mind that vars_push()
always makes a copy of the array
you pass it - if what you pass it is a pointer to allocated memory
it's your responsibility to T_free()
it.
As a complete example here is a rather simple but working function named
square()
that returns the square of the value it got passed:
Var_T * square( Var_T * var ) { vars_check( var, INT_VAR | FLOAT_VAR ); /* is it a number ? */ Var_T * ret_var; if ( var->type == INT_VAR ) { long int_square = var->INT * var->INT; ret_var = vars_push( INT_VAR, int_square ); } else { double float_square = var->FLOAT * var->FLOAT; ret_var = vars_push( FLOAT_VAR, float_square ); } return ret_var; } |
As you see it's checked first that the variable passed to the function
has the correct type - both integer and floating point values are ok
(otherwise the interpretation of the EDL
script would stop).
Next we distinguish between the possibilities that the value is an
integer or a floating point number by testing the type
field of
the variable. Then we create either a new integer variable by calling
vars_push()
with the square of the integer value or a new
floating point variable. Finally, we return the variable pointer
vars_push()
had delivered.
Of course, we could also have written the function in a more compact way:
Var_T * square( Var_T * var ) { vars_check( var, INT_VAR | FLOAT_VAR ); if ( var->type == INT_VAR ) return vars_push( INT_VAR, var->INT * var->INT ); else return vars_push( FLOAT_VAR, var->FLOAT * var->FLOAT ); } |
If your function does not have to return a value at all just return
the integer value 1
, which can be interpreted as success.
What if you want to write to function that returns more than one value?
Again we use a function for a digitizer that has to return a curve
stored in an array as an example. Let's assume the data you got from the
digitizer are stored in an array of integers called data
which
has len
elements (where len
is a long
). Now all
you've got to do is call the function vars_push()
as
Var_T * ret_var; ... ret = vars_push( INT_ARR, data, len ); ... return ret_var; |
Actually, at some point of your function you may have allocated memory
for storing the data. It is your responsibility to free this memory
before you return from your function, fsc2
just uses a copy of
the data you pass to it using vars_push()
. As you probably already
guessed, if you want to return a float array, you will have to use
FLOAT_ARR
instead of INT_ARR
in the call to
vars_push()
.
The same method may be used if your function has to return two different values and both have the same type. Again an array can be returned
VARIABLES: V1; V2; // results of call to my_function() Dummy[ * ]; // variable sized array for values returned by my_function() ... // lots of stuff left out Dummy = my_function( ); // automagically sets the dimension // of array Dummy to 2 V1 = Dummy[ 1 ]; V2 = Dummy[ 2 ]; |
and the C code for function my_function()
would look like
Var_T * my_function( Var_T * var ) { long v[ 2 ]; v[ 0 ] = ...; /* just fill in all the stuff you */ v[ 1 ] = ...; /* need to calculate both data */ return vars_push( INT_ARR, v, 2 ); } |
An alternative (e.g. if the type of the variables you need to return
differs) is to write two functions where the first one does the
calculations needed and stores the second value in a global
variable. All the second function has to do is just to return the value
of the global variable. This way, the EDL
file might look like
V1 = my_function_1( ); v2 = my_function_2( ); |
while the C code would define both functions as
static double v2; /* global variable used by my_function_1() and my_function_2() */ Var_T * my_function_1( Var_T * v ) { long V1; V1 = ...; /* just fill in all the stuff you */ v2 = ...; /* need to calculate both data */ return vars_push( INT_VAR, V1 ); } Var_T * my_function_2( Var_T * v ) { return vars_push( FLOAT_VAR, v2 ); } |
Alternatively, you also could write the function in a way that it counts the number of times it has been called and returns values accordingly, e.g.
V1 = my_function( ); v2 = my_function( ); |
with the corresponding C code
Var_T * my_function( Var_T * v ) { long V1; static double v2; static int call_count = 0; if ( call_count > 0 ) /* on second call return second value */ { call_count = 0; /* don't forget to reset the call counter! */ return vars_push( FLOAT_VAR, v2 ); } V1 = ... /* just fill in all the stuff you */ v2 = ... /* need to calculate both data */ return vars_push( INT_VAR, V1 ); } |
Of course, in both cases one has to be careful to call the function(s) in the correct sequence, so it's not completely foolproof.
This document was generated by Jens Thoms Toerring on September 6, 2017 using texi2html 1.82.