Writing C functions for Isis

If this is your first time writing a C function for Isis, you should read this entire document. The many macros and functions available to help you manipulate Isis values are fairly intuitive, but there are still some important rules to follow.


Arguments and return values

The first step in writing a C function for Isis is to use this prototype for its arguments and return values:
Value *my_func(Script *script, char *proc_name, int call_data, int argc, Value **argv);
Isis passes 5 arguments to C functions when they are called:

Each C function must return a pointer to a Value. Not surprisingly, this is the value that will be returned from the procedure when it is called in Isis. If you do not wish to return a value, then simply return NullValue. This is a special pre-defined Isis value that represents Null in the interpreter. You may NOT return NULL from an Isis C function at any time--use NullValue instead. More details below.


Checking argument types

The very first thing to do in the function is make sure the expected number of arguments was received. After that, the next thing to do is check that each of those arguments is of the expected type. Isis provides several efficient macros for this purpose:
checkCount(count, usage); checkCountLeast(min, usage); checkCountMost(max, usage); checkInt(argnum, argname); checkReal(argnum, argname); checkBool(argnum, argname); checkChar(argnum, argname); checkAddr(argnum, argname); checkProc(argnum, argname); checkList(argnum, argname); checkNumber(argnum, argname); checkIntList(argnum, argname); checkRealList(argnum, argname); checkBoolList(argnum, argname); checkString(argnum, argname); checkAddrList(argnum, argname); checkProcList(argnum, argname); checkListList(argnum, argname); checkNumberList(argnum, argname);
The checkCount() macros check if exactly, at least, or at most the specified number of arguments have been received. If the count is wrong, the macro prints an error message and entire function returns NullValue. The macro will also print a "usage" message if you pass a string for the usage argument. That string will be printed starting on the line after the error message, and it may contain newline characters in order to print on more than one line. If you don't want a usage message to be printed, pass NULL.

The rest of these macros check the types of individual arguments. The first 7 check the primitive types. checkNumber() checks if the argument is either an integer or a real number. The rest check if the argument is a homogeneous list (contains values of only one type). If the type is incorrect, the macro prints an error message and the entire function returns NullValue.

The type-checking macros expect the argument number (starting from 0) and a string name for the argument to be used in error messages. If the argument doesn't have a name, just pass NULL.

For example, if your function expects exactly two arguments, an integer "loop parameter" and a string, then the beginning of your function could look like this:

checkCount(2, "loop parameter and string"); checkInt(0, "loop parameter"); checkString(1, NULL);
Since the above macros automatically print error messages and return NullValue if there is a problem, they are not appropriate for all circumstances. In that case, you can also use the following macros to directly check the type of an Isis value:
int isNull(Value *val); int isInt(Value *val); int isReal(Value *val); int isChar(Value *val); int isBool(Value *val); int isProc(Value *val); int isAddr(Value *val); int isList(Value *val); int isNumber(Value *val); int isNullList(Value *val); int isIntList(Value *val); int isRealList(Value *val); int isString(Value *val); int isBoolList(Value *val); int isProcList(Value *val); int isAddrList(Value *val); int isListList(Value *val); int isNumberList(Value *val);
Each of these macros returns 1 or 0. You can check the type of an argument by passing an item from the argv[] array to these macros. For example:
if(argc != 2 || !isInt(argv[0]) || !isString(argv[1])) { fprintf(stderr, "** %s: Expected an integer and a string.\n", proc_name); return NullValue; }
Another way to check types is to use the typeof() macro:
int typeof(Value *val);
typeof() returns an integer that corresponds to one of the types defined by the following constants:
IsisNull IsisInt IsisReal IsisBool IsisChar IsisAddr IsisProc IsisList
typeof() is best used in an switch statement, like this:
switch( typeof(argv[0]) ) { case IsisInt: printf( "You passed an integer!\n" ); break; case IsisReal: printf( "You passed a real number!\n" ); break; case IsisList: printf( "You passed a list!\n" ); break; default: printf( "You passed something else.\n" ); break; }


Extracting values

Once you have established that the arguments are of the correct type, you are ready to actually use them to do something! There are several macros and functions that extract the actual values from the Value structure used by the arguments. Always use the macro that corresponds to the type of value being extracted, and only after that type has been verified using the macros shown above.
int getInt(Value *val); real getReal(Value *val); char getChar(Value *val); int getBool(Value *val); Procedure *getProc(Value *val); void *getAddr(Value *val); Value **getList(Value *val);
These first seven macros extract values of the seven basic Isis types. Take notice of the return types: Integers and characters are returned as their standard C counterparts. Real numbers are returned as type real which is currently typedef'd as double. Booleans are returned simply as integers (0 or 1). Procedures are returned as a pointer to a Procedure structure which you are not meant to access directly--it will be used in calls to other helper functions. Addresses are returned, appropriately, as generic pointers (void *). Lists are returned as an array of pointers to Value structures (which should not be modified).
real getRealfromNumber(Value *val); int getIntfromNumber(Value *val);
These 2 functions are used to extract an integer or a real number from a value that could be either (determined using the isNumber() macro).
int Listsize(Value *val); Value *Listitem(Value *val, int index);
These macros only operate on lists. The first returns the number of items in a list, and the other returns the specified item in the list.
int getIntList(Value *val, int *ptr, int maxlen); int getRealList(Value *val, real *ptr, int maxlen); int getBoolList(Value *val, int *ptr, int maxlen); int getString(Value *val, char *ptr, int maxlen); int getAddrList(Value *val, void **ptr, int maxlen); int getProcList(Value *val, Procedure **ptr, int maxlen); int getIntListfromNumberList(Value *val, int *ptr, int maxlen); int getRealListfromNumberList(Value *val, real *ptr, int maxlen);
These functions work on values previously determined to be homogeneous lists. Each automatically extracts the values and puts them into an array that you have allocated. maxlen specifies the array size. If the number of items in the list is larger than the size of the array, the overflow is not extracted. getString() also adds a null terminating character '\0' at the end of the extracted string. All of these macros return the total number of items extracted.

Here is a small example:

len = Listsize(argv[1]) + 1; /* add 1 for final '\0' character */ str = malloc(len); getString(argv[1], str, len); for( i = 0; i < getInt(argv[0]) ; i++) printf("%s\n" , str); free(str);


Creating new values

The following functions create new Isis values, suitable for returning from your function:
Value *newInt(int val); Value *newReal(real val); Value *newBool(int val); Value *newChar(char val); Value *newAddr(void *val); Value *newProc(Procedure *val);
These above six functions simply create new basic Isis values.
Value *newIntList(int len, int *vals); Value *newRealList(int len, real *vals); Value *newBoolList(int len, int *vals); Value *newCharList(int len, char *vals); Value *newString(int len, char *vals); /* same as newCharList */ Value *newAddrList(int len, void **vals); Value *newProcList(int len, Procedure **vals); Value *newIntListva(int len, ...); Value *newRealListva(int len, ...); Value *newBoolListva(int len, ...); Value *newCharListva(int len, ...); Value *newAddrListva(int len, ...); Value *newProcListva(int len, ...);
These functions simply create homogeneous lists of basic Isis values. Each function has 2 versions--one accepts an array and the other accepts a variable number of arguments. Use whichever is most convenient. In any case, be sure to pass the list's length as the first argument.
Value *newBasicListva(int len, ...);
This function allows you to create a heterogeneous list of only the six basic Isis values (not lists). It accepts a variable number of arguments. Pass the length of the list first. Each item in the list is specified as a pair of arguments---the item's type and the item's value. For specifying the types, use the same type identifiers from before: IsisInt IsisReal IsisBool IsisChar IsisAddr IsisProc. Here is an example:
newBasicListva(3, IsisInt, 42, IsisReal, 101.01, IsisChar, 'x');
In Isis this would appear as:
[ 42 101.01 'x' ]
These final 2 functions create a list from Isis values that already exists:
Value *newList(int len, Value **vals); Value *newListva(int len, ...);
One takes an array of the values, and the other accepts a variable number of arguments each of which is a value. You are responsible for properly disposing of these values after calling the function (because the functions will create their own references to the values). Please read the important rules and examples below for more information...


Important rules:

Isis employs an efficient reference count system for handling values and controlling deallocation. In order to maintain the integrity of this system, you must follow these simple rules at all times:


Freeing values

void freeValue(Value *val);

To free a value is to say that you are deleting one reference to it. Isis keeps an internal reference count and releases the memory used by a value only when there are no more references to it.

If you are not returning or storing a value that you created inside your function, or if you delete any other global reference to a value, call freeValue() on it. You will very rarely need to do this since usually you will only create values if you intend to return them. Never call freeValue() on arguments to your function. Here is an example using the newListva() function discussed earlier.

Value *val1, *val2, *retval; val1 = newIntListva(3, 10, 20, 30); val2 = newRealListva(3, 15.0, 25.0, 35.0); retval = newListva(2, val1, val2); freeValue(val1); /* Must free because these values are not retained */ freeValue(val2); return(retval);
The value returned in the above example will show up like this:
[ [ 10 20 30 ] [ 15.000000 25.000000 35.000000 ] ]


Referring to previously-created values

void referValue(Value *val);
If you are returning or storing a value that was not created inside your function, or any other time you create a new reference to a value, call referValue() on it. This means if you want to return or retain a value that was passed as an argument, you must call referValue() on it before the function completes. When you create new values inside your function, they are automatically initialized with one reference, so unless you create additional references, you do not need to call referValue() on them at any time.

referValue() returns the same value passed to it.


Returning values

Every Isis C function must return a pointer to a Value. Values created with any of the creation primitives from before are suitable for returning directly from your function. However, as discussed before, if you want to return a value that was passed to the function as an argument, you must call referValue() on it since you are creating a new reference to it.
return argv[0]; /****** NEVER! ******/ return referValue(argv[0]); /* Correct */

You can also return NullValue, which is a special pre-defined Isis value that represents Null in the interpreter. NullValue functions just like any other Isis value, except that you never have to worry about creating or freeing it. Please note that you should NEVER return C's null pointer, NULL, from any Isis function. This will result in annoying error messages. Use NullValue instead:

return NULL; /****** NEVER! ******/ return NullValue; /* Correct */


A Full example

The following is an example of an Isis C function that accepts an integer and a string as arguments, prints the string the specified number of times, and returns the original string. Usually an operation as simple as this would be written directly in Isis, but the individual pieces of this example may be useful to you as you write your own C functions. Value *loop_string(Script *script, char *proc_name, int call_data, int argc, Value **argv) { /* This function takes an integer and a string, and prints that string the number of times specified by the integer. */ int len; char *str; /* First check that we actually received an integer and a string: */ checkCount(2, "loop parameter and string"); checkInt(0, "loop parameter"); checkString(1, NULL); /* Extract the string and print it repeatedly: */ len = Listsize(argv[1]) + 1; /* add 1 for final '\0' character */ str = malloc(len); getString(argv[1], str, len); for(i = 0; i < getInt(argv[0]); i++) printf("%s\n" , str); free(str); /* Return the original string that came in. Must use referValue because we are creating a new reference to an argument: */ return referValue(argv[1]); }


Other useful routines for Isis C functions:

int equalValue(Value *val1, Value *val2);
This function check if two values are equal. Returns 1 or 0.
int trueValue(Value *val);
Checks if the value is a true value (as defined in the Isis reference manual).
void printValue(FILE *fd, Value *value);
Prints a text representation of the value to the given file. All values except for procedures are printed in a form that could be re-read and evaluated by the interpreter at a later time.
int homogeneousList(Value *val, int type);
If you pass one of the specifiers IsisNull IsisInt IsisReal IsisBool IsisChar IsisAddr IsisProc IsisList for the type, this function checks if all the elements are of the given type. Result is nonzero if so, 0 otherwise. If you pass 0 for the type, the function simply check if all the elements in the list are of the same type (not any particular type). If so, that type is returned, and 0 otherwise.

Procedure *newCfunc(Value *(*func)(), char *name, int call_data);
This creates a new Isis procedure that refers to a C function. You pass the actual C function, a "name" for the procedure that will be passed to it as proc_name and an integer that will be passed to it as call_data. A pointer to the new procedure is returned. You could then use the newProc() macro to create a new procedure value.

Procedure *referProcedure(Procedure *proc); void *freeProcedure(Procedure *proc);
Any time you create or destroy and external reference to an Isis Procedure, use these routines in a similar manner to their counterparts for Isis values. Note that procedures (Procedure *) and procedure values (Value *) are two different things. You should only to use these routines in the extrememly rare case that you want to return or retain a Procedure instead of a procedure Value.

Value *callProcedure(Script *script, Procedure *proc, int argc, ...); Value *applyProcedure(Script *script, Procedure *proc, int argc, Value **argv);
Both of these functions apply an Isis procedure (possibly extracted using the getProc() macro) to a set of values. In both, you must specify the number of arguments you are passing as argc. The first accepts the a variable number of arguments each of which is a value, and the second accepts an array of the values instead. Use whichever one is most convenient. You are responsible for properly disposing of the argument values after the procedure exits. The return value is the result of the call. For example, here is the implementation of the Isis apply primitive:
Value *prim_apply(Script *script, char *proc_name, int call_data, int argc, Value **argv) { checkCount(2, "procedure and list of arguments"); checkProc(0, NULL); checkList(1, NULL); return applyProcedure(script, getProc(argv[0]), Listsize(argv[1]), getList(argv[1])); }


Binding values into the top-level environment

Binding *bindInt(Script *script, char *name, int val); Binding *bindReal(Script *script, char *name, real val); Binding *bindBool(Script *script, char *name, int val); Binding *bindChar(Script *script, char *name, char val); Binding *bindAddr(Script *script, char *name, void *val); Binding *bindProc(Script *script, char *name, Procedure *val); Binding *bindList(Script *script, char *name, int len, Value **vals); Binding *bindIntList(Script *script, char *name, int len, int *vals); Binding *bindRealList(Script *script, char *name, int len, real *vals); Binding *bindBoolList(Script *script, char *name, int len, int *vals); Binding *bindCharList(Script *script, char *name, int len, char *vals); Binding *bindString(Script *script, char *name, int len, char *vals); /* same as bindCharList */ Binding *bindAddrList(Script *script, char *name, int len, void **vals); Binding *bindProcList(Script *script, char *name, int len, Procedure **vals); Binding *bindListva(Script *script, char *name, int len, ...); Binding *bindIntListva(Script *script, char *name, int len, ...); Binding *bindRealListva(Script *script, char *name, int len, ...); Binding *bindBoolListva(Script *script, char *name, int len, ...); Binding *bindCharListva(Script *script, char *name, int len, ...); Binding *bindAddrListva(Script *script, char *name, int len, ...); Binding *bindProcListva(Script *script, char *name, int len, ...); Binding *bindBasicListva(Script *script, char *name, int len, ...); Binding *bindValue(Script *script, char *name, Value *val); Binding *bindCfunc(Script *script, char *name, Value *(*func)(), int call_data);
All of these functions bind Isis values into the top level environment of the interpreter. They work similarly to the new*() functions described earlier, except that you must pass a string name for the variable in Isis that will be bound to the value of interest. bindValue() binds a value of any type, and as always you should properly dispose of the value after calling this function. bindCfunc() is simply a short-cut to calling newCfunc() and bindProc() in sequence. These functions will be heavily used when you are ready to add your functions into the environment of your personal Isis interpreter. Details are in the section on
binding C into Isis.