Extending the ICI Interpreter The base ICI language is supplemented by a number of intrinsic functions. These functions typically provide access to operating system or application functions or are speed critical. Intrinsic functions are linked with the interpreter and are typically, although not neccesarially, written in C. A table maps the ICI-level name to the C function. A pointer to this table is added to the interpreter's configuration table. You can make use of the ICI interpreter in specialised applications by writing your own set of intrinsic functions. Writing these functions is straightforward. There are well defined and easy to use mechanisms for accessing function parameters and returning values from the function. Getting Arguments When an extension function is called it is passed no arguments. For the function to do its work it needs to get the arguments passed to the function in the ICI program. These values are part of the ICI program state and are stored as ICI objects. These need to be gotten at, as C data types, in some manner. The typecheck() function The function typecheck() is used to access the arguments passed to the ICI function. typecheck() stores the arguments in C data types, i.e. an ICI integer is stored in a C long, an ICI floating point number in a C double, an ICI string in a C string, and so on. typecheck() function takes a number of parameters. The first is a format string that describes the types of arguments expected to have been passed to the ICI function. The second and subsequent parameters are pointers to places where the values of these arguments may be stored. Just like scanf() but with different a format string syntax. The format string consists of a number of type specifiers. Each specifier is a single character. The corresponding argument in the call to typecheck() must be a pointer to an appropriate C type for the ICI type. The following table defines this correspondence, Format Character ICI type C type ------------------------------------------------------ i integer long f float double n float/integer double o any object object_t * p any pointer ptr_t * d any structure struct_t * a array array_t * s string char * u file file_t * Any format character may be capitalised to indicate that an ICI pointer is required rather than the actual ICI type. E.g., 'I' is a pointer to an integer rather than a integer. The correspoding C type has one extra level of indirection added to it. The format character "-" skips over a parameter. The parameter must still be supplied but it is ignored when storing values. The format character "*" specifies that all subsequent parameters are to be ignored. typecheck() returns an integer, zero if it succeeded or non-zero if an error occurs. An error is either a mismatch in the types of the actual parameters to the ICI function or a wrong number of parameters. In either case the ICI exception mechanism is "primed" to supply the correct failure if you take advantage of this. Examples From ICI's fopen()... char *name; char *mode; if (typecheck("ss", &name, &mode)) return 1; The "return 1;" is an ICI convention. Intrinsic functions return 1 to indicate an error and an error is raised (equivilent to the ICI fail() function). The string pointed to by the global character pointer, error, is used as the exception code. When typcheck() fails is sets error to an appropriate message. For more details see the files cfunc.c and clib.c. These contains most of ICI's intrinsic functions and can be used as models when implementing your own. Don't worry too much, its a lot easier than it seems! The header file func.h defines a number of macros for accessing the parameters passed to the extension function. These macros return actual ICI objects so you have to understand ICI type structures to use these. The function typecheck() provides a higher level interface. Returning Values Values are returned from C functions by returning ICI objects. The function obj_ret() defined in cfunc.c is used to return an object from the intrinsic function. It takes a pointer to an object and makes it the result (to the ICI caller) of the function. The object must not be "loose". To return "loose" objects the loose_ret() function is used instead. Several functions are provided for common return types... int_ret integer long str_ret string char * Errors If an extension function returns 1 it is considered to have failed. The global variable, error (a character pointer), points to a string that describes the error. This string is used in ICI's exception handling. E.g., part of a hypothetical square root routine, static int f_sqrt() { ... get args if (value < 0.0) { error = "domain error"; return 1; } ... do square root and return value } Within an ICI program the caller can do this... auto x, y; ... get y value try x = sqrt(y); onerror { switch (error) { case "domain error": printf("y is negative\n"); x = 0.0; break; default: fail(error); } } printf("square root of %f is %f\n", y, x); Here we trap the case of taking the square root of a negative number but pass any other exceptions through. I've used a switch instead of a simple if to allow for simple expansion of the number of error cases we can deal with and to show a switch using strings as the keys. There are several utilty functions for handling bad parameters. The function argerror() takes an integer and produces the exception, argument of incorrectly supplied as The is replaced with the value of the parameter. is the name of the current function, e.g. "fopen" and is the type of the actual parameter, e.g., argument 1 of fopen incorrectly supplied as int argerror() returns 1 so it can be used in the return statement of an extension function, e.g. from f_close(), if (typecheck("u",&file) return argerror(1); Which generates exceptions like, "argument 1 of close incorrectly supplied as string" The function argcount() generates an exception for functions that take a fixed number of parameters. argcount() takes a single parameter, the required number of parameters, and generates the exception string arguments given to , but it takes is the number of actual parameters, the number of formal parameters and the name of the current function. E.g, from f_copy(), if (NARGS() != 1) return argcount(1); generates the exception, 200 arguments passed to copy, but it takes 1 Adding your functions. To make your extension functions known to the ICI interpreter you must define a function table (an array of cfunc_t structures.) This table maps a name of a function to the C function that implements that function. The CF_OBJ macro defined in func.h generates the object header for use with cfunc_t objects. To define the function table you use this function along with the function name and pointer to the C function, e.g., if we have the functions, int f_say(); int f_translate(); We define a table as follows, cfunc_t speech_functions[] = { {CF_OBJ, "say", f_say}, {CF_OBJ, "translate", f_translate}, {CF_OBJ} }; The final entry terminates the array so the interpereter can know how many functions are defined. The convention within the ICI intepreter is to prefix the names of the C implementations of functions with a "f_" so the function f_fopen() implements the ICI fopen() etc... The cfunc_t type has a couple of extra fields used by the ICI math functions. The math functions are implemented via a single C entry point, f_math(), that takes two arguments, a pointer to a function that implements the appropriate operation and a format string defining what parameters that function accepts. These are stored in the C function table to simplify the implementation of f_math(). Once you have a function table you need to tell the interpreter about it. The file conf.c contains a table of pointers to function tables that define ICI's intrinsic functions. Just add an entry for your table. If your package requires special initialisation then it is probably best to check in your C functions and perform it when required rather than adding code to ICI's main function. This example is an ICI interface to a hypothetical graphics package... /* * gfx.c */ #include "fwd.h" #include "func.h" static double current_x; static double current_y; static int need_initialisation = 1; static void init() { ... do initialisation need_initialisation = 0; } STATIC int f_moveto() { double x, y; /* Get and check parameters */ if (typecheck("nn", &x, &y)) return 1; /* Set current position */ current_x = x; current_y = y; /* Nothing useful to return so return NULL */ return loose_ret(objof(&o_null)); } STATIC int f_lineto() { double x, y; /* Get and check parameters */ if (typecheck("nn", &x, &y)) return 1; if (need_initialisation) init(); /* Draw the line */ GfxMoveTo(current_x, current_y); GfxLineTo(x, y); /* Remember where we ended up */ current_x = x; current_y = y; /* Return NULL */ return loose_ret(objof(&o_null)); } STATIC int f_readpixel() { long x, y, rc; /* Get and check parameters */ if (typecheck("ii", &x, &y)) return 1; if (need_initialisation) init(); /* Return the result of the GfxReadPixel() call */ return int_ret((long)GfxReadPixel(x, y)); } cfunc_t gfx_funcs[] = { {CF_OBJ, "moveto", f_moveto}, {CF_OBJ, "lineto", f_lineto}, {CF_OBJ, "readpixel", f_readpixel}, {CF_OBJ} }; In conf.c we add the following declaration, extern cfunc_t gfx_funcs[]; And add the address of our table, &gfx_funcs, into the "funcs" array. It is common practice to define a macro that can be defined to NOT include the functions. So in the middle of the initialisation of the funcs array we add the following, #ifndef NOGFXFUNCS &gfx_funcs, #endif That's all to it. Then we can simply remake the interpreter and call our functions. E.g., /* * Stupid ICI program using graphics functions */ auto i; for (i = 0; i < 10000; ++i) { moveto(rand(), rand()); lineto(rand(), rand()); } auto n, x, y; for (n = x = 0; x < 1280; ++x) for (y = 0; y < 1024; ++y) n += readpixel(x, y); printf("%d white pixels, %d black\n", n, 1280 * 1024 - n); Real extension packages do much more useful things of course. Adding new object types.