1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
|
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 <spl.h>
#include <stdio.h>
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_<module-name>_init' and 'spl_mod_<module-name>_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_<module-name>.so and that the
<module-name> 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 <spl.h>
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 <spl.h>
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.
|