SPL C-API Documentation ======================= SPL has a pretty simple C interface which allows easy embedding of the SPL virtual machine and the SPL compiler in other applications and extending SPL using functions and modules written in C. A very complete example program (using almost the entire API) is "splrun.c". It is the tool which is usually used to run SPL programms on the command line. A rather short example for executing SPL code is "examples/c-api-test4.c". Beware of the other C API demo programms in "examples/": They are test cases for fiddling with SPL byte code and SPL assembler code. Usually one does not want to use SPL on this level. A nice example for writing SPL modules in C is "spl_modules/mod_termio.c". All SPL data structures and public functions are defined in the "spl.h" header file. Running SPL Scripts ------------------- Running an SPL Script is easy: The SPL compiler compiles the SPL Script to SPL Assembler commands. But this assembler commands are not genereted as text ouput, they are passed to an SPL Assembler instance. Optionally it is possible to run the SPL optimizer over the generated assembler code: char *spl_source = "debug 'Hello World!';"; struct spl_asm *as = spl_asm_create(); if (spl_compiler(as, spl_source, "spl_script", spl_malloc_file, 1)) error(); spl_asm_add(as, SPL_OP_HALT, 0); spl_optimizer(as); The first two argument to spl_compiler() are the SPL Assembler instance and the SPL Source to be compiled. The third parameter is the name of the script file (this is primarily used for error messages). The fourth parameter is a function which can be used by the compiler for loading external files. Loading external files is not allowed if this is a NULL pointer. The last parameter is a bool value which specifies if the compiler should generate debug symbols. The SPL Assembler instance can then be used to dump SPL bytecode. This bytecode is then passed to a newly generated task of an SPL Virtual Machine: struct spl_vm *vm = spl_vm_create(); struct spl_task *task = spl_task_create(vm, "main"); spl_task_setcode(task, spl_asm_dump(as)); task->code->id = strdup("spl_script"); spl_asm_destroy(as); Before the script is executed it is required to configure the SPL Virtual Machine. Usually one wants to activate the standard builtin functions and set a search path for loading additional SPL Modules: spl_builtin_register_all(vm); asprintf(&vm->path, ".:./spl_modules:%s", spl_system_modules_dir()); When it should be possible to execute SPL callbacks from C functions then one also needs to define a runloop function which should be used for that purpose. Usually the pre-defined 'spl_simple_runloop' function is used for that: vm->runloop = spl_simple_runloop; Now it is possible to execute the script by calling spl_exec() until the HALT opcode is reached. This opcode removes the code page from the current task because there is nothing left to be executed on the page: while ( task->code ) { spl_gc_maybe(vm); task = spl_schedule(task); if ( spl_exec(task) < 0 ) break; } The spl_gc_maybe() function runs the garbage collector when it is time to do so. The spl_schedule() function does the scheduling between the tasks. For a single-threaded script like in this example it is a NO-OP. Finally cleaning up all the SPL data structures is easy. Simply destroying the SPL Virtual Machine also destroys everything which is connected to it (including stuff such as unloading modules loaded by the script): spl_vm_destroy(vm); Have a look at "splrun.c" in the SPL source tree for a very complete example of an SPL runtime implementation. An application which is embedding SPL should be compiled and linked with the options printed by "spl-config --cflags", "spl-config --ldflags" and "spl-config --ldlibs". Writing SPL functions in C -------------------------- We will start with an example. In this example we create two new SPL functions named 'myadd' and 'mysub' which implement integer addition and substraction. First we need to create a C function which implements myadd() and mysub(): struct spl_node *spl_builtin_myaddsub(struct spl_task *task, void *data) { int a = spl_clib_get_int(task); int b = spl_clib_get_int(task); if (!strcmp(data, "add")) return SPL_NEW_INT(a + b); if (!strcmp(data, "sub")) return SPL_NEW_INT(a - b); return 0; } We also could have written two small C functions for the two SPL functions, but then we would have had no example for the second parameter to SPL C functions. All SPL C functions have the same prototype: They return an SPL node pointer and expect an SPL task struct as first and a void pointer as second argument. An SPL node pointer is the abstract representation of a "value" in SPL. The task struct contains all information about the currently running SPL task. Functions such as 'spl_clib_get_int(task)' can be used to pop the arguments from the VM stack. Arguments are pushed from right to left on the machine stack so the first argument must be popped first, then the second, and so on. Scalar values can be popped using the three functions: int spl_clib_get_int(struct spl_task *task); double spl_clib_get_float(struct spl_task *task); char *spl_clib_get_string(struct spl_task *task); You must not free the pointer returned by spl_clib_get_string() it is automatically freed as soon as control is passed back to the SPL virtual machine. You also must not modify the string. In addition to that the three functions int spl_clib_get_argc(struct spl_task *task); struct spl_node *spl_clib_get_hargs(struct spl_task *task); struct spl_node *spl_clib_get_node(struct spl_task *task); can be used to get the number of remaining arguments on the VM stack, get the hash with the named arguments or pop an argument as SPL node from the stack respectively. The spl_clib_get_node() function is only needed when complex (non-scalar) data structures are passed as parameters. The return values of spl_clib_get_hargs() and spl_clib_get_node() must be freed using spl_put() (see the seperate section about SPL data structures below). New SPL nodes for scalar values can be created using the helper functions struct spl_node *SPL_NEW_INT(int v); struct spl_node *SPL_NEW_FLOAT(double v); struct spl_node *SPL_NEW_STRING(char *v); struct spl_node *SPL_NEW_STRING_DUP(const char *v); struct spl_node *SPL_NEW_SPL_STRING(struct spl_string *v); The SPL_NEW_STRING() function expects the parameter to be already malloced. The SPL_NEW_STRING_DUP() function is internally using strdup() to create a seperate malloced copy of the string. SPL is internally using binary trees with reference counters for representing strings. That improves the performance of string concatenations massively. If you have already created an SPL string you can easily create a value for it using the SPL_NEW_SPL_STRING() function. It is also possible to return a NULL pointer in a SPL C function. This is automatically converted to an 'undef' value in SPL. Finally the new functions need to be registered with the virtual machine. This is done using the spl_clib_reg() function: spl_clib_reg(vm, "myadd", spl_builtin_myaddsub, "add"); spl_clib_reg(vm, "mysub", spl_builtin_myaddsub, "sub"); The first parameter is the SPL VM, the second the name of the function and the third a pointer to the C function implementing the SPL function. The last parameter (a void pointer) is simply passed as second parameter to the C function implementing the SPL function whenever it is called. This way our spl_builtin_myaddsub() function can distinguish if it has been called as myadd() or mysub(). A good place for the spl_clib_reg() calls is right after calling spl_builtin_register_all() for the virtual machine. Writing SPL Modules in C ------------------------ Extending SPL as described above is only possible when embedding SPL in own projects. But usually one is using an already existing SPL runtime environment and simply wants to add a few functions. This can be done by writing SPL modules. Here is the sourcecode of a simple example module, mod_hello.c: #include #include static struct spl_node *handler_hello(struct spl_task *task, void *data) { printf("Hello World!\n"); return 0; } void SPL_ABI(spl_mod_hello_init)(struct spl_vm *vm, struct spl_module *mod, int restore) { spl_clib_reg(vm, "hello", handler_hello, 0); } void SPL_ABI(spl_mod_hello_done)(struct spl_vm *vm, struct spl_module *mod) { return; } The SPL_ABI() is a macro which add a little prefix with the SPL ABI version to the identifier passed as argument. This ensures that a SPL runtime never loads a module which has been compiled for another SPL ABI. Every module must export the functions 'spl_mod__init' and 'spl_mod__done'. The _init function is called when a virtual machine loads the module and the _done function is called when a virtual machine which has loaded the module is going to be destroyed. The third argument to the _init function is set to 1 when the module is loaded while restoring a dumped session. When a module is e.g. creating SPL variables when loaded they get dumped and restored automatically and so do not need to be recreated when the module is loaded while restoring the session. Compiling and loading the module is straight forward: $ gcc -shared mod_hello.c -o mod_hello.so $ splrun -q 'load "hello"; hello();' Hello World! Note that the module *.so file must be named mod_.so and that the in the filename and the _init and _done functions must be identical. SPL Data Structures ------------------- All SPL values are stored in SPL nodes (struct spl_node). Basically the following data structure is associated with an spl_node: - scalar values (string, integer and floating point) - a hash with ordering information with references to other nodes - a context and a class pointer (also references to other nodes) - the context type information (function context, object, etc.) - a code pointer (reference to a code page and an address in it) - a flags field (additional type information and various other stuff) - data for hosted nodes (see seperate section below) - some internal data (e.g. for garbage collection) Most of this information is not accessed directly but using helper functions. We have seen already the helper functions for creating new SPL nodes with scalar values. The helper functions for reading the scalar value from an existing node are: int spl_get_int(struct spl_node *node); double spl_get_float(struct spl_node *node); char *spl_get_string(struct spl_node *node); struct spl_string *spl_get_spl_string(struct spl_node *node); char *spl_get_value(struct spl_node *node); int spl_get_type(struct spl_node *node); The first three functions are self explainatory. While spl_get_string() returns a single continous string (you must not free or modify this string), spl_get_spl_string() returns the spl_string structure used internally in SPL to store strings. The spl_get_value() function does the same as the spl_get_string() function but returns a NULL pointer when the value is undefined (spl_get_string() return "" in this case). The spl_get_type() is used by the dynamically typed operators to decide which typed operator should be used and returns one of SPL_TYPE_NONE, SPL_TYPE_INT, SPL_TYPE_FLOAT or SPL_TYPE_OBJ. Usually SPL_TYPE_NONE is interpreted as if SPL_TYPE_INT were returned. SPL is using a hybrid garbage collector which also performs reference counting. Thus one needs to update the reference counters whenever creating new or removing old references for an SPL node. This can be done using the following two functions: struct spl_node *spl_get(struct spl_node *node); void spl_put(struct spl_vm *vm, struct spl_node *node); The spl_get() function creates a new node when called with a NULL pointer as argument. The argument 'vm' to spl_put() is required for the garbage collector. For this and similar reasons most functions for working with SPL nodes require either a pointer to the SPL virtual machine or a pointer to the currently running SPL task as argument. SPL nodes generated with one of the SPL_NEW_*() functions described above have their reference counter already set to one. So it is wrong to make an additional call to spl_get() for these nodes unless you are creating more than one link to the new node. Each SPL node may have child nodes. Such child nodes can be created, looked up and removed using the following three functions: struct spl_node *spl_create(struct spl_task *task, struct spl_node *node, const char *key, struct spl_node *newnode, int flags); struct spl_node *spl_lookup(struct spl_task *task, struct spl_node *node, const char *key, int flags); void spl_delete(struct spl_task *task, struct spl_node *node, const char *key); The 'flags' argument to spl_create() and spl_lookup() are bitmap. The following values are supported by both functions: SPL_LOOKUP_TEST Do not trigger a runtime error when the entry looked for can not be found. This is example given used by the SPL 'declared' statement. SPL_LOOKUP_NOCTX SPL_LOOKUP_NOSTATIC These are used internally in recursive spl_lookup() calls, example given when looking something up in the class derivation path. In addition to the SPL_LOOKUP_TEST flag the following flags are supported by the spl_create() function: SPL_CREATE_LOCAL Create it directly here. Do not follow the context pointers and skip the local blocks. Do not trigger a runtime error when the variable does not exist yet. In most cases one needs to pass this flag. SPL_CREATE_BEGIN When creating a new key, create it at the begin of the key list. Without this flag new keys are added to the end of the key list. SPL_CREATE_NOSTATIC Used internally. SPL_CREATE_FUNCLOCAL Create function local unless already defined in a local command block. This is used internally for storing regular expression results. The following two functions from the "time" module are used to convert a UNIX 'tm' struct to an SPL data structure and vice versa. I think this is an excellent example for using spl_lookup() and spl_create(): static void convert_node_to_tm(struct spl_task *task, struct spl_node *node, struct tm *ttm) { memset(ttm, 0, sizeof(struct tm)); ttm->tm_sec = spl_get_int(spl_lookup(task, node, "sec", SPL_LOOKUP_TEST)); ttm->tm_min = spl_get_int(spl_lookup(task, node, "min", SPL_LOOKUP_TEST)); ttm->tm_hour = spl_get_int(spl_lookup(task, node, "hour", SPL_LOOKUP_TEST)); ttm->tm_mday = spl_get_int(spl_lookup(task, node, "mday", SPL_LOOKUP_TEST)); ttm->tm_mon = spl_get_int(spl_lookup(task, node, "mon", SPL_LOOKUP_TEST)); ttm->tm_year = spl_get_int(spl_lookup(task, node, "year", SPL_LOOKUP_TEST)); ttm->tm_wday = spl_get_int(spl_lookup(task, node, "wday", SPL_LOOKUP_TEST)); ttm->tm_yday = spl_get_int(spl_lookup(task, node, "yday", SPL_LOOKUP_TEST)); ttm->tm_isdst = spl_get_int(spl_lookup(task, node, "isdst", SPL_LOOKUP_TEST)); spl_put(task->vm, node); } static struct spl_node *convert_tm_to_node(struct spl_task *task, struct tm *ttm) { struct spl_node *result = spl_get(0); spl_create(task, result, "sec", SPL_NEW_INT(ttm->tm_sec), SPL_CREATE_LOCAL); spl_create(task, result, "min", SPL_NEW_INT(ttm->tm_min), SPL_CREATE_LOCAL); spl_create(task, result, "hour", SPL_NEW_INT(ttm->tm_hour), SPL_CREATE_LOCAL); spl_create(task, result, "mday", SPL_NEW_INT(ttm->tm_mday), SPL_CREATE_LOCAL); spl_create(task, result, "mon", SPL_NEW_INT(ttm->tm_mon), SPL_CREATE_LOCAL); spl_create(task, result, "year", SPL_NEW_INT(ttm->tm_year), SPL_CREATE_LOCAL); spl_create(task, result, "wday", SPL_NEW_INT(ttm->tm_wday), SPL_CREATE_LOCAL); spl_create(task, result, "yday", SPL_NEW_INT(ttm->tm_yday), SPL_CREATE_LOCAL); spl_create(task, result, "isdst", SPL_NEW_INT(ttm->tm_isdst), SPL_CREATE_LOCAL); return result; } The 'keys' used in spl_create() and spl_lookup() are simple strings in which the dot character can be used as seperator for recursive lookups. For example the following spl_lookup() call looks up the variable 'A', then inside of 'A' the variable 'B' and finally inside of that 'B' the variable 'C': spl_lookup(task, node, "A.B.C", 0); In addition to that there are some special keys starting with the '!' prefix (for example '!CLS' looks up the parent class of a class or object) or the '#' prefix (special variables generated by the compiler). The following two functions can be used to encode everything which is not a number, 7-bit ascii letter or underscore and decode such an encoded again: char *spl_hash_encode(const char *source); char *spl_hash_decode(const char *source); The algorithm of spl_hash_encode() is also used when hash elements are addressed using square brackets in SPL. The return value of this function is a malloced string which must be freed by the caller. SPL Strings ----------- SPL stores strings in spl_string structs. Such a struct has a reference counter and (optionally) a left child, a right child and a text value. The string represented by such an spl_string struct is the concatenation of the string represented by the left child, the text value and the string by the right child. The following function can be used to create a new string struct: struct spl_string *spl_string_new(int flags, struct spl_string *left, struct spl_string *right, char *text, struct spl_code *code); The 'flags' argument is a bitmap for which the following values are supported: SPL_STRING_STATIC Usually the 'text' argument is a malloced pointer which is automatically freed when the spl_string struct is destroyed. This flag must be set when the SPL string subsystem should not free the string. SPL_STRING_DOTCAT Automatically insert a dot character between the left child and the text value. This is used to speed up the creation of variable lookup paths (see spl_lookup() above). SPL_STRING_UTF8 Indicates that the string contains UTF8 characters. This flag is automatically maintained by the strings library and does not need to be passed to spl_string_new(). SPL_STRING_NOAUTOGET Do not increment the reference counters of the children. The 'left' and 'right' arguments do specify the left and right children and must be NULL when no left or right child exists. The 'code' argument is used when the 'text' pointer points to the data section of a code page and the reference counter of the code page should be decremented when this spl_string struct gets destoyed. This argument is usually simply set to NULL. Often the 'text' argument is dynamically created. There is a printf-like helper function for automatically allocating the text argument and filling it with the string sprintf() would generate: struct spl_string *spl_string_printf(int flags, struct spl_string *left, struct spl_string *right, const char *fmt, ...); Sometime one needs to manually increment or decrement the reference counter of an SPL string. This can be done using the following two functions: struct spl_string *spl_string_get(struct spl_string *s); void spl_string_put(struct spl_string *s); Finally there is a function for generating the flat string representation of an SPL string: char *spl_string(struct spl_string *s); This function stores the flat representation of the string as the new text value and dereferences the children. That also means that the caller must not modify or free the return value of spl_string(). Hosted SPL Nodes ---------------- Lets have a look at the following SPL/SDL (SDL is the "Simple DirectMedia Library") example program: load "sdl"; sdl_init(640, 480, fullscreen: 0); sdl_title("Sprites Demo"); var sprite_background = sdl_sprite_create(); var sprite_ball = sdl_sprite_create(); sprite_background.image = sdl_image_load("background.png"); sprite_ball.image = sdl_image_load("ball.png"); while (1) { sprite_ball.x = (sprite_ball.x + 5) % 640; sprite_ball.y = (sprite_ball.y + 1) % 480; sdl_sprite_update(); sdl_delay(25); } You can easily see that the SPL nodes pointed to by the variable names 'sprite_background' and 'sprite_ball' are somewhat special. Changing the values of the member variables 'x' and 'y' have the side effect of changing the position of the sprite on the screen. If you play with it a bit you will also find some other unusal effects. For example it is not possible to create new member variables of this structures and the "x" and "y" members seem to only accept integer values. The sdl_sprite_create() function creates a so-called "Hosted Node", an SPL node that is not a freestanding general purpose data structure but has a close connection to a set of SPL functions or an SPL module. Here is the C sourcecode of the sdl_sprite_create() function (with the parts which are specific to the SDL module and not interesting here removed): static struct spl_node *handler_sdl_sprite_create(struct spl_task *task, void *data) { struct sdl_sprite_hnode_data *hnd = calloc(1, sizeof(struct sdl_sprite_hnode_data)); /* initialize hnd */ struct spl_node *n = SPL_NEW_STRING_DUP("SDL Sprite"); n->hnode_name = strdup("sdl_sprite"); n->hnode_data = hnd; return n; } So a hosted node is an ordinary SPL node with the attribute "hnode_name" set to an identifier specifying the node type, in this case "sdl_sprite". The same naming scheme which applies to global variable and function names does also apply for hnode identifiers. The spl_node attribute "hnode_data" is a void pointer which can be freely used by the hnode implementation to store local data. The SDL module must also register a handler function for "sdl_sprite" nodes. This is done using the following function call in the modules _init function: spl_hnode_reg(vm, "sdl_sprite", handler_sdl_sprite_node, 0); The function prototype of handler_sdl_sprite_node() reads: void handler_sdl_sprite_node(struct spl_task *task, struct spl_vm *vm, struct spl_node *node, struct spl_hnode_args *args, void *data); The last argument to spl_hnode_reg() is simply passed through to the handler_sdl_sprite_node() function (the 'data' argument). The handler function is called whenever an action is performed on an "sdl_sprite" hnode. The action itself is passed using a spl_hnode_args struct and return values are also passed back using that struct: struct spl_hnode_args { int action; const char *key; int flags; struct spl_node *value; }; The action field specifies the kind of action that should be performed. The meaning of the other fields depends on the action. Following actions are defined: SPL_HNODE_ACTION_LOOKUP Lookup a member variable of the hnode. The "key" field contains the name of the member variable and the "flags" field the value of the flags argument to spl_lookup(). The value of the member should be passed back using the "value" field. SPL_HNODE_ACTION_CREATE Like SPL_HNODE_ACTION_LOOKUP but the "value" field contains the value to be set for the member. SPL_HNODE_ACTION_DELETE Deletion of a member has been requested. The "key" field contains the name of the member that should be deleted. SPL_HNODE_ACTION_NEXT SPL_HNODE_ACTION_PREV For listing the member variables. The "key" field contains the name of a member. The "value" field should be set to a spl node as returned by SPL_NEW_STRING() which contains the name of the next or preview member respectively. When the "key" field is a null pointer the first member should be returned for SPL_HNODE_ACTION_NEXT and the last member for SPL_HNODE_ACTION_PREV. SPL_HNODE_ACTION_COPY Someone tries to copy the hosted node using the ':=' operator. Create a full copy of this node and return the new hosted node in the "value" field. SPL_HNODE_ACTION_PUT This node is going to be destroyed. Free all resources which are connected to this node. SPL_HNODE_ACTION_DUMP The virtual machine state is going to be dumped. Serialize the state of this node and set the spl_node attribute 'hnode_dump' to a string representing the serialized node. Be prepared that this handler can later be called with 'hnode_data' beeing a NULL pointer. Then the node status must be restored from the string in 'hnode_dump'. Hosted nodes can be created so they look pretty similar to normal SPL data structures (as the SDL sprites described above) or very different (e.g. XML document handlers can be used like hashes with XPath queries as key values). Most hosted nodes do not implement all of the actions described above. E.g. the NEXT and PREV actions are rarely implemented. Sometimes it is too difficult or even impossible to implement dump and restore. In these cases it is also possible to not implement SPL_HNODE_ACTION_DUMP which then triggers a runtime error when one tries to dump a virtual machine with active hosted nodes without a valid 'hnode_dump' attribute. Error Handling -------------- There are two ways of error reporting in SPL: Creating VM runtime errors, warnings or debug output and throwing exceptions. In SPL scripts the runtime messages can be produced using the 'panic', 'warning' and 'debug' statements and exceptions can be thrown using the 'throw' statement. VM runtime errors, warnings and debug output can be created using the spl_report() function. Example given: spl_report(SPL_REPORT_RUNTIME, task, "This is an error!\n"); spl_report(SPL_REPORT_RUNTIME|SPL_REPORT_WARNING, task, "This is a warning message!\n"); spl_report(SPL_REPORT_RUNTIME|SPL_REPORT_DEBUG, task, "Did you know that %d + %d is %d?\n", 23, 42, 23+42); The 1st spl_report() call in the example creates a runtime error message. The printed error message will contain a stack backtrace and the virtual machine stops execution. The 2nd spl_report() call in the example creates a runtime warning message. The printed message will contain a stack backtrace. But the virtual machine will continue execution as if nothing special happened. The 3rd spl_report() call in the example creates a debug message. The printed message will not contain a stack backtrace and the virtual machine will continue execution. Note that spl_report() supports the well-known printf format strings. Exceptions can be thrown using the spl_clib_exception() function. The following small example module (mod_exdemo.c) implements the function exdemo() which can throw an exception. #include static struct spl_node *handler_exdemo(struct spl_task *task, void *data) { int argc = spl_clib_get_argc(task); if (argc != 4) spl_clib_exception(task, "ExdemoEx", "description", SPL_NEW_SPL_STRING(spl_string_printf(0, 0, 0, "You passed %d arguments to exdemo().\n" "This function must be called with 4 arguments.", argc)), NULL); return 0; } void SPL_ABI(spl_mod_exdemo_init)(struct spl_vm *vm, struct spl_module *mod, int restore) { if (!restore) spl_eval(vm, 0, strdup(mod->name), "object ExdemoEx { }"); spl_clib_reg(vm, "exdemo", handler_exdemo, 0); } SPL exceptions are objects and thus we first need to create a class for our example exceptions. This is simply done using spl_eval() in the modules _init function. It is important that this spl_eval() is not executed when a dumped vm machine state is restored. Note that the ExdemoEx class has no properties at all. This is not unusual for exception classes. The handler_exdemo() function does throw exceptions when it is not called with 4 arguments. The exceptions are thrown using the spl_clib_exception() helper function. This function is called with the current task pointer and the name of the exception class followed by a variable number of arguments. This additional arguments are pairs of a string pointer holding a property name and an SPL node pointer with the value for this property. The list is terminated with a single NULL pointer. In our example we only set the "description" property. This property is automatically included in the 'uncaught exception' error messages: $ gcc -shared mod_exdemo.c -o mod_exdemo.so $ splrun -q 'load "exdemo"; exdemo(1, 2, 3);' SPL Runtime Error: Thrown uncaught exception: [ ExdemoEx ] >> You passed 3 arguments to exdemo(). >> This function must be called with 4 arguments. in: ROOT (byte 28 in code block 'command line') The spl_clib_exception() helper is not 100% equal to the SPL 'throw' instruction. The most important difference is that it does not run the constructor for the new exception object. Callbacks from C to SPL ----------------------- Sometimes one needs to pass an SPL callback function to a C function and then call this callback function from the C code. This is tricky, but possible: #include static struct spl_node *handler_callback(struct spl_task *task, void *data) { struct spl_node *callback = spl_clib_get_node(task); struct spl_node *arg1 = spl_clib_get_node(task); struct spl_node *arg2 = spl_clib_get_node(task); struct spl_task *cb_task = spl_clib_callback_task(task->vm); struct spl_asm *as = spl_asm_create(); spl_asm_add(as, SPL_OP_PUSHC, "retval"); spl_asm_add(as, SPL_OP_ZERO, 0); spl_asm_add(as, SPL_OP_PUSHA, "arg2"); spl_asm_add(as, SPL_OP_PUSHA, "arg1"); spl_asm_add(as, SPL_OP_DCALL, "callback"); spl_asm_add(as, SPL_OP_POPL, 0); spl_asm_add(as, SPL_OP_HALT, 0); spl_task_setcode(cb_task, spl_asm_dump(as)); spl_asm_destroy(as); spl_create(cb_task, cb_task->ctx, "callback", callback, SPL_CREATE_LOCAL); spl_create(cb_task, cb_task->ctx, "arg1", arg1, SPL_CREATE_LOCAL); spl_create(cb_task, cb_task->ctx, "arg2", arg2, SPL_CREATE_LOCAL); spl_clib_callback_run(cb_task); struct spl_node *retval = spl_get(spl_lookup(cb_task, cb_task->ctx, "retval", 0)); spl_task_destroy(cb_task->vm, cb_task); return retval; } void SPL_ABI(spl_mod_callback_init)(struct spl_vm *vm, struct spl_module *mod, int restore) { spl_clib_reg(vm, "callback", handler_callback, 0); } This example module (mod_callback.c) implements a function which should be called with three arguments. The first one is a callback function and the other two are arguments which are passed thru to the callback function. The return value of the callback function is also the return value of the example function. Example given: $ gcc -shared mod_callback.c -o mod_callback.so $ splrun -q 'load "callback"; debug callback(function(a,b) { return a+b; }, 23, 42);' SPL Debug: 65 As can be seen, it is not possible to call the callback function directly. Instead one needs to create a seperate task (using spl_clib_callback_task()) and generate a little helper codepage which then calls the callback function. This codepage can then be executed using the spl_clib_callback_run() function. In the example program I have used temporary SPL variables in the local context of the task created by spl_clib_callback_task() to pass the callback, the arguments and the return value to and from the callback codepage. A task context is not very different from any other SPL node. So the variables can be easily created with spl_create() and looked up with spl_lookup(). The SPL assembler code used in the example above is pretty generic and is sufficient for the most cases where an SPL callback should be called from C code. Simply add as many SPL_OP_PUSHA records as you have arguments. The command 'splrun -AN' can be used to create SPL assembler code from SPL scripts. So that command can be used if the above example does not cover your requirements. For example the code used in the above example can be generated like this: $ splrun -AN -q 'var retval=callback(arg1, arg2);' # SPL Assembler Dump :16 PUSHC "retval" # (1 byte arg) 10 :18 ZERO :19 PUSHA "arg2" # (1 byte arg) 0 :21 PUSHA "arg1" # (1 byte arg) 5 :23 DCALL "callback" # (1 byte arg) 17 :25 POPL :26 HALT # Total size: 37 bytes (11 text, 26 data). You could also call the SPL compiler directly in the C function to generate the SPL assembler code. But the SPL compiler is rather slow and calling it for such a small code snippet is an unnecessary overhead. SPL and Multithreading ---------------------- SPL can be used in multithreaded environments using POSIX Threads. Be sure that pthread support is enabled when running GNU make and that your program links to the pthread library with -lpthread. Using the output of spl-config --ldflags and spl-config --ldlibs should be sufficient if SPL itself has been built with pthread support. Only one SPL VM is allowed per thread. Multiple threads in one SPL script is not possible. However, you can use the task module to allow for parallel activities within one SPL VM. In this case each SPL function call will be atomic and the SPL scheduler will switch between SPL tasks.This form of parallel SPL tasks will look like a single thread or process to the embedding C program or operating system. Multiple truly threaded SPL VMs can be used by creating one SPL VM per thread. In this case thread scheduling will be independent to SPL function calls. This allows for increased responsiveness of the individual SPL VMs in the case of heavy duty function calls in the SPL code. Communication between threaded SPL VMs can be established with synchronized registered C functions that perform some kind of communication, for example event handling. If the communication path can be kept simple, such a solution can be very efficient and still provide the benefits of a threaded environment. In addition, utilizing pthread conditions and (timed) condition wait calls provided by registered C functions can avoid excessive polling of the communication path. See the man page for pthread_cond_timedwait(3) for details.