JX9 Scripting Engine

An Embeddable Scripting Engine



Foreign Function Implementation Guide.

The following quick guide is what you do to start experimenting with the JX9 foreign function mechanism without having to do a lot of tedious reading.


Foreign functions are used to add JX9 functions or to redefine the behavior of existing JX9 functions from the outside environment (see below) to the underlying virtual machine. This mechanism is know as “In-process extending”. After successful call to jx9_create_function(), the installed function is available immediately and can be called from the target JX9 code.


A foreign function is simply a host application defined function typically implemented in C which is responsible of performing the desired computation. There are many reasons to implement foreign functions, one of them is to perform system calls and gain access to low level resources that is impossible to do using standard JX9 functions.

Another reason to implement foreign functions is performance. That is, a foreign function implemented in C will typically run 5 times faster than if it was implemented in JX9, this is why most of the built-in JX9 functions (Over 303 as of this release) such as json_encode(), crc32(), array_merge(), chdir(), fread(), fopen() and many more are implemented in C rather than JX9.


The signature of the foreign function is as follows:

int (*xFunc)(jx9_context *pCtx,int argc,jx9_value **argv)


As you can see, the implemented function must accept three parameters. The first parameter is a pointer to a jx9_context structure. This is the context in which the foreign function executes. In other words, this parameter is the intermediate between the foreign function and the underlying virtual machine.

The application-defined foreign function implementation will pass this pointer through into calls to dozens of interfaces, these includes:

jx9_context_output(), jx9_context_throw_error(), jx9_context_new_scalar(), jx9_context_user_data(), jx9_context_alloc_chunk() and many more.


The second parameter is the total number of arguments passed to the foreign function. If the total number of arguments is not the expected one, the foreign functions can throw an error via jx9_context_throw_error() as follow: jx9_context_throw_error(pCtx,JX9_CTX_WARNING,"Unexpected number of arguments");


The last parameter is an array of pointers to jx9_value which represents function arguments. The implementation of the foreign functions can extract their contents via one of these interfaces:

jx9_value_to_int()

jx9_value_to_bool()

jx9_value_to_int64()

jx9_value_to_double()

jx9_value_to_string()

jx9_value_to_resource()

jx9_value_compare()

The computation result (i.e. the return value) of the foreign function can be set via one of these interfaces:

jx9_result_int()

jx9_result_int64()

jx9_result_bool()

jx9_result_double()

jx9_result_null()

jx9_result_string()

jx9_result_value()

jx9_result_resource()

jx9_result_string_format()

If no call is made to one of these interfaces, then a NULL return value is assumed.


The implementation of the foreign function must return JX9_OK on success, but if the callbacks wishes to abort processing (to stop program execution) and thus to emulate the exit or die JX9 constructs, it must return JX9_ABORT instead.

Typical Implementation Of A Foreign Function

A typical implementation of a foreign function would perform the following operations in order:

  1. Check if the given arguments are of the expected numbers and expected types. If the foreign accepts arguments, this is the first operation it must perform before doing any serious work. For that, a set of interfaces are available:

    jx9_value_is_int

    jx9_value_is_float

    jx9_value_is_bool

    jx9_value_is_string

    jx9_value_is_null

    jx9_value_is_numeric

    jx9_value_is_callable

    jx9_value_is_json_array

    jx9_value_is_scalar

    jx9_value_is_json_object

    jx9_value_is_resource

    jx9_value_is_empty

    To check if the given arguments are of the expected number, simply compare the expected number with the argc parameter (Second parameter the foreign function takes).

  2. If the foreign function accepts arguments, it may need to extract their contents via the following set of interfaces:

    jx9_value_to_int()

    jx9_value_to_bool()

    jx9_value_to_int64()

    jx9_value_to_double()

    jx9_value_to_string()

    jx9_value_to_resource()

    jx9_value_compare()

  3. Perform the desired computation.

  4. Allocate additional jx9_value via the jx9_context_new_scalar() and/or jx9_context_new_array() interfaces especially if the foreign function works with JSON arrays or JSON objects (see below for a working example).

  5. If something goes wrong while processing the input or performing the computation, the foreign function can throw an error via the jx9_context_throw_error() or jx9_context_throw_error_format() interfaces.

  6. If the foreign function wishes to output a message and thus to emulate the print JX9 construct, it may call jx9_context_output() or jx9_context_output_format().

  7. When done, the computation result (i.e. return value) of the foreign function can be set via one of the these interfaces:

    jx9_result_int()

    jx9_result_int64()

    jx9_result_bool()

    jx9_result_double()

    jx9_result_null()

    jx9_result_string()

    jx9_result_value()

    jx9_result_resource()

    jx9_result_string_format()

  8. And finally, return JX9_OK when done. Note that you do not need to release any allocated resource manually via the jx9_context_release_value() interface, the virtual machine will release any allocated resources for you automatically.

Foreign Function Examples

We will start our examples with the simplest foreign function implemented in C which perform a simple right shift operation on a given number and return the new shifted value as it's computation result (i.e. return value), here is the C code:


  1. int shift_func(

  2.   jx9_context *pCtx, /* Call Context */

  3.   int argc, /* Total number of arguments passed to the function */

  4.   jx9_value **argv /* Array of function arguments */

  5. )

  6. {

  7.   int num;

  8.   /* Make sure there is at least one argument and is of the

  9.    * expected type [i.e: numeric].

  10.    */

  11.   if( argc < 1 || !jx9_value_is_numeric(argv[0]) ){

  12.    /*

  13.     * Missing/Invalid argument,throw a warning and return FALSE.

  14.     * Note that you do not need to log the function name,JX9 will

  15.     * automatically append the function name for you.

  16.     */

  17.    jx9_context_throw_error(pCtx,JX9_CTX_WARNING,"Missing numeric argument");

  18.   /* Return false */

  19.    jx9_result_bool(pCtx,0);

  20.    return JX9_OK;

  21.   }

  22.   /* Extract the number */

  23.   num = jx9_value_to_int(argv[0]);

  24.   /* Shift by 1 */

  25.   num <<= 1;

  26.   /* Return the new value */

  27.   jx9_result_int(pCtx,num);

  28.   /* All done */

  29.   return JX9_OK;

  30. }


On line 11, we check if there is at least one argument and is numeric (integer or float or a string that looks like a number). If not, we throw an error on line 17 and we return a Boolean false (zero) on line 19 using the jx9_result_bool() interfaces.


We extract the function argument which is the number to shift on line 23 using the jx9_value_to_int() interface.


We perform the desired computation (right shift the given number by one) on line 25.


And finally, we return the new value as our computation result one line 27 using a call to jx9_result_int().


Now after installing this function (see below for a working example), it can be called from your JX9 script as follows:


   print shift_func(150); //you should see 300.


Another simple foreign function named date_func(). This function does not expect arguments and return the current system date in a string. A typical call to this function would return something like: 2012-23-09 13:53:30. Here is the implementation


  1. int date_func(

  2.   jx9_context *pCtx, /* Call Context */

  3.   int argc, /* Total number of arguments passed to the function */

  4.   jx9_value **argv /* Array of function arguments*/

  5. ){

  6.   time_t tt;

  7.   struct tm *pNow;

  8.   /* Get the current time */

  9.   time(&tt);

  10.   pNow = localtime(&tt);

  11.   /*

  12.    * Return the current date.

  13.    */

  14.   jx9_result_string_format(pCtx,

  15.     "%04d-%02d-%02d %02d:%02d:%02d", /* printf() style format */

  16.      pNow->tm_year + 1900, /* Year */

  17.      pNow->tm_mday, /* Day of the month */

  18.      pNow->tm_mon + 1, /* Month number */

  19.      pNow->tm_hour, /* Hour */

  20.      pNow->tm_min, /* Minutes */

  21.      pNow->tm_sec /* Seconds */

  22.   );

  23.   /* All done */

  24.   return JX9_OK;

  25. }


We get the date on line 10 using the libc localtime() routine and we return it using the jx9_result_string_format() interface on 14. This function is a work-alike of the "printf()" family of functions from the standard C library which is used to append a formatted string.


Download & Compile this C file for a working version of the function defined above plus others foreign functions.

Working With JSON Arrays/Object

Working with JSON arrays or objects is relatively easy and involves two or three additional call to JX9 interfaces. A typical implementation of a foreign function that works with arrays/objects would perform the following operations:

  1. Call jx9_context_new_array() to allocate a fresh jx9_value of type array or object. The new array/object is empty and need to be populated. This interface is unified for JSON arrays as well JSON objects.

  2. Call jx9_context_new_scalar() to allocate a new scalar jx9_value. This value is used to populate array/object entries with the desired value.

  3. Populate the array/object using one or more calls to jx9_array_add_elem() and/or it's wrappers. Again, these interfaces are unified for JSON arrays and objects.

  4. Finally, return the fresh array/object using the jx9_result_value() interface.

We will start our examples by a simple foreign function names array_time_func(). This function does not expects arguments and return the current time in a JSON array. A typical output of this function would look like this:

print(array_time_func());
/* JSON Array */
[14,53,30]

Here is the C code.

  1. int array_time_func(jx9_context *pCtx,int argc,jx9_value **argv)

  2. {

  3.   jx9_value *pArray; /* Our JSON Array */

  4.   jx9_value *pValue; /* Array entries value */

  5.   time_t tt;

  6.   struct tm *pNow;

  7.   /* Get the current time first */

  8.   time(&tt);

  9.   pNow = localtime(&tt);

  10.   /* Create a new json array */

  11.   pArray = jx9_context_new_array(pCtx);

  12.   /* Create a worker scalar value */

  13.   pValue = jx9_context_new_scalar(pCtx);

  14.   if( pArray == 0 || pValue == 0 ){

  15.    /*

  16.     * If the supplied memory subsystem is so sick that we are unable

  17.     * to allocate a tiny chunk of memory, there is no much we can do here.

  18.     * Abort immediately.

  19.     */

  20.     jx9_context_throw_error(pCtx,JX9_CTX_ERR,"Fatal, JX9 is running out of memory");

  21.     /* emulate the die() construct */

  22.     return JX9_ABORT; /* die('Fatal,JX9 is running out of memory'); */

  23.   }

  24.   /* Populate the JSON array.

  25.    * Note that we will use the same worker scalar value (pValue) here rather than

  26.    * allocating a new value for each array entry. This is due to the fact

  27.    * that the populated array will make it's own private copy of the inserted

  28.    * key(if available) and it's associated value.

  29.    */


  30.   jx9_value_int(pValue,pNow->tm_hour); /* Hour */

  31.   /* Insert the hour at the first available index */

  32.   jx9_array_add_elem(pArray,0/* NULL: Assign an automatic index*/,pValue /* Will make it's own copy */);


  33.   /* Overwrite the previous value */

  34.   jx9_value_int(pValue,pNow->tm_min); /* Minutes */

  35.   /* Insert minutes */

  36.   jx9_array_add_elem(pArray,0/* NULL: Assign an automatic index*/,pValue /* Will make it's own copy */);


  37.   /* Overwrite the previous value */

  38.   jx9_value_int(pValue,pNow->tm_sec); /* Seconds */

  39.   /* Insert seconds */

  40.   jx9_array_add_elem(pArray,0/* NULL: Assign an automatic index*/,pValue /* Will make it's own copy */);


  41.   /* Return the array as the function return value */

  42.   jx9_result_value(pCtx,pArray);


  43.   /* All done. Don't worry about freeing memory here, every

  44.    * allocated resource will be released automatically by the engine

  45.    * as soon we return from this foreign function.

  46.    */

  47.   return JX9_OK;

  48. }

Download the C file

We allocate a fresh empty JSON array on line 11 using a call to jx9_context_new_array(). Also, we allocate a fresh scalar value one line 13 using a call to jx9_context_new_scalar().


We populate the scalar jx9_value with the desired value, here the current hour on line 31 using a call to jx9_value_int() and we insert it in the new array using the jx9_array_add_elem() interface on line 33.


This process is repeated again on line 36, 38, 41 and 43. Note that we use the same scalar value to populate the array with the three entries. This is due to the fact that a call to jx9_array_add_elem() and it's wrappers will make a private copy of the inserted value and so it's safe to use the same scalar value for other insertions.


Finally, we return our array as our computation result one line 46 using the jx9_result_value() interface.

Test The Implementation

Now we have implemented our foreign functions, it's time to test them. For that, we will create a simple JX9 program that call each of the defined functions and output their return values. Here is the program:


  print 'shift_func(150) = ' .. shift_func(150) .. JX9_EOL;

  print 'sum_func(7,8,9,10) = ' .. sum_func(7,8,9,10) .. JX9_EOL;

  print 'date_func(5) = ' .. date_func() .. JX9_EOL;

  print 'array_time_func() =' .. array_time_func() .. JX9_EOL;

  print 'object_date_func() =' ..  JX9_EOL;

 dump(object_date_func());

  print 'array_str_split(\'Hello\') =' ..  JX9_EOL;

 dump(array_str_split('Hello'));

When running this program, you should see something like that:

shift_func(150) = 300

sum_func(7,8,9,10) = 34

date_func(5) = 2012-29-12 01:13:58

array_time_func() =[1,13,58]

object_date_func() =

JSON Object(6 {

"tm_year":2012,

"tm_mon":12,

"tm_mday":29,

"tm_hour":1,

"tm_min":13,

"tm_sec":58

}

)

array_str_split('Hello') =

JSON Array(5 ["H","e","l","l","o"])

Now, the main program.(You can get a working version of this program here)

  1. /*

  2. * Container for the foreign functions defined above.

  3. * These functions will be registered later using a call

  4. * to [jx9_create_function()].

  5. */

  6. static const struct foreign_func {

  7. const char *zName; /* Name of the foreign function*/

  8. int (*xProc)(jx9_context *,int,jx9_value **); /* Pointer to the C function performing the computation*/

  9. }aFunc[] = {

  10. {"shift_func",shift_func},

  11. {"date_func", date_func},

  12. {"sum_func", sum_func },

  13. {"array_time_func", array_time_func},

  14. {"array_str_split", array_string_split_func},

  15. {"object_date_func", object_date_func}

  16. };

  17. /*

  18. * Main program: Register the foreign functions defined above, compile and execute

  19. * our JX9 test program.

  20. */

  21. int main(void)

  22. {

  23.   jx9 *pEngine; /* JX9 engine */

  24.   jx9_vm *pVm; /* Compiled JX9 program */

  25.   int rc;

  26.   int i;


  27.   /* Allocate a new JX9 engine instance */

  28.   rc = jx9_init(&pEngine);

  29.   if( rc != JX9_OK ){

  30.        Fatal("Error while allocating a new JX9 engine instance");

  31.   }


  32.   /* Compile the JX9 test program defined above */

  33.   rc = jx9_compile(

  34.          pEngine, /* JX9 engine */

  35.          JX9_PROG, /* JX9 test program */

  36.          -1 /* Compute input length automatically*/,

  37.          &pVm /* OUT: Compiled JX9 program */

  38.        );


  39.  if( rc != JX9_OK ){

  40.    if( rc == JX9_COMPILE_ERR ){

  41.      const char *zErrLog;

  42.      int nLen;

  43.      /* Extract error log */

  44.      jx9_config(pEngine,

  45.          JX9_CONFIG_ERR_LOG,

  46.          &zErrLog,

  47.          &nLen

  48.         );

  49.      if( nLen > 0 ){

  50.         /* zErrLog is null terminated */

  51.         puts(zErrLog);

  52.       }

  53.   }

  54.   /* Exit */

  55.   Fatal("Compile error");

  56. }


  57.   /* Now we have our program compiled,it's time to register

  58.    * our foreign functions.

  59.    */

  60.   for( i = 0 ; i < (int)sizeof(aFunc)/sizeof(aFunc[0]) ; ++i ){

  61.      /* Install the foreign function */

  62.      rc = jx9_create_function(pVm,aFunc[i].zName,aFunc[i].xProc,0 /* NULL: No private data */);

  63.      if( rc != JX9_OK ){

  64.        Fatal("Error while registering foreign functions");

  65.      }

  66.   }

  67.  


  68.  /*

  69.    * Configure our VM:

  70.    * Install the VM output consumer callback defined above.

  71.    */

  72.    rc = jx9_vm_config(pVm,

  73.         JX9_VM_CONFIG_OUTPUT,

  74.         Output_Consumer, /* Output Consumer callback */

  75.         0 /* Callback private data */

  76.       );

  77.   if( rc != JX9_OK ){

  78.      Fatal("Error while installing the VM output consumer callback");

  79.    }

  80.  


  81.  /*

  82.    * Report run-time errors such as unexpected numbers of arguments and so on.

  83.    */

  84.    jx9_vm_config(pVm,JX9_VM_CONFIG_ERR_REPORT);

  85.  


  86.  /*

  87.     * And finally, execute our program. Note that your output (STDOUT in our case)

  88.     * should display the result.

  89.     */

  90.    jx9_vm_exec(pVm,0);

  91.  

  92.   /* All done, cleanup the mess left behind.

  93.     */

  94.    jx9_vm_release(pVm);

  95.    jx9_release(pEngine);


  96.    return 0;

  97. }

Download the C file

We create a new JX9 engine instance using a call to jx9_init() on line 29. This is often the first JX9 API call that an application makes and is a prerequisite in order to compile JX9 code using one of the compile interfaces.


We compile our JX9 test program on line 35 using the jx9_compile() interface.


We register our implemented foreign functions on line 66 using the jx9_create_function() interface.


We configure our Virtual Machine on line 77 by setting a VM output consumer callback named Output_Consumer() (Download the C file to see the implementation) which redirect the VM output to STDOUT.


And finally we execute our JX9 program on line 97 using a call to jx9_vm_exec().


Clean-up is done on line 101 and 102 respectively via calls to jx9_vm_release() and jx9_release().

Other useful links

As you can see, in-process extending under JX9 is extremely powerful yet simple and involves only a single call to jx9_create_function().


Check out the Introduction To The JX9 C/C++ Interface for an introductory overview and roadmap to the dozens of JX9 interface functions.


A separate document, The JX9 C/C++ Interface, provides detailed specifications for all of the various C/C++ APIs for JX9. Once the reader understands the basic principles of operation for JX9, that document should be used as a reference guide.


Any questions, check the Frequently Asked Questions page or visit the Support Page for additional information.


Symisc Systems
Copyright © Symisc Systems