Coding With SpiderMonkey

Part II - Executing Scripts

In my previous article, I showed you how to get a basic application working which successfully loads up and initializes the SpiderMonkey engine. I never did show you how to actually use this engine though, so that is what I will do in today's article. Below you will learn how to go about loading up and executing a javascript file.

So, lets start with the code we had from the previous article. Here's a copy/paste version for those who don't have it any more, or are starting mid-game. You can also download the code by clicking the file name.

#include <stdio.h>
#include <jsapi.h>

JSClass js_global_object_class = {
    "System",
    0,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_EnumerateStub,
    JS_ResolveStub,
    JS_ConvertStub,
    JS_FinalizeStub,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

int main(int argc, char **argv){
    char *fileToRun=NULL;
    JSRuntime *rt=NULL;
    JSContext *cx=NULL;
    JSObject *obj=NULL;

    if (argc > 1){
        fileToRun = argv[1];
    }

    rt = JS_NewRuntime(8L*1024L);
    if (!rt){
        printf("Failed to initialize JS Runtime.\n");
        return 1;
    }

    cx = JS_NewContext(rt, 8L*1024L*1024L);
    if (!cx){
        printf("Failed to initialize JS Context.\n");
        JS_DestroyRuntime(rt);
        return 1;
    }

    obj = JS_NewObject(cx, &js_global_object_class, NULL, NULL);
    if (!obj){
        printf("Failed to create global object.\n");
        JS_DestroyContext(cx);
        JS_DestroyRuntime(rt);
        return 1;
    }

    JS_InitStandardClasses(cx, obj);

    if (fileToRun){
        printf("At this point, we would run the file %s if we were developed more.\n", fileToRun);
    }
    else {
        printf("Usage: %s file\n", argv[0]);
    }


    JS_DestroyContext(cx);
    JS_DestroyRuntime(rt);
    return 0;
}

Put those scripts to work

When you compile and run that file, you'll notice it really serves no purpose at all. We start the JS engine, then we check if a file name was specified. If so, we say that we would run it if we knew how. Otherwise we print the usage information. Now it's time to code in the file execution so it does know how to run the file.

Executing a file in SpiderMonkey is actually a pretty easy process. There are multiple methods to achieve the goal. You can either read the file in yourself and pass it to SpiderMonkey as strings, or you can tell spider monkey to compile the file, and then execute the compiled file. I chose to do the later. I also add some error checking such as making sure the file exists and is readable. I wrote a simple function to encapsulate the process of executing a file into a nice single function call that returns either true or false, depending on if the file was successfully executed.

Here is a copy of my function. I will explain the parts below. Copy and paste this function into the engine.c file above at the end of the file. Remember to add a function prototype to the top of the file as well. In order to use the stat() system call, we also need to include a couple new headers. The headers we need to add are #include <sys/stat.h> and #include <sys/types.h>. Click the file name below to get a new copy of engine.c with the function added.

#include <sys/types.h>
#include <sys/stat.h>

/* Function Prototypes */
JSBool js_engine_execute_file(JSContext *ctx, const char *file);

/* The orginal code ... */

JSBool js_engine_execute_file(JSContext *ctx, const char *file){
    JSScript *script;
    jsval returnValue;
    JSObject *global = JS_GetGlobalObject(ctx);
    struct stat statinfo;

    if (file == NULL){
        return JS_FALSE;
    }

    if (stat(file, &statinfo) == -1){
        return JS_FALSE;
    }

    if (!S_ISREG(statinfo.st_mode)){
        return JS_FALSE;
    }

    script = JS_CompileFile(ctx, global, file);

    if (script == NULL){
        return JS_FALSE;
    }

    return JS_ExecuteScript(ctx, global, script, &returnValue);
}

So, now you have a function, js_engine_execute_file() which can be used to execute a javascript file. Lets go over how this function works, a few blocks of code at a time.

JSScript *script;
jsval returnValue;
JSBool returnVal;
JSObject *global = JS_GetGlobalObject(ctx);
struct stat statinfo;

First up, we declare the variables we need to use in order to execute the file. script holds the compiled code. This is what SpiderMonkey will use to actually execute our script. returnValue is the return value of the script. This is basically whatever the value of the last statement in the file is. returnVal is holds the return value from JS_ExecuteScript and will be what we return from this function. global is the global object for the given context. We obtain this using JS_GetGlobalObject rather than pass it in separately or use a global variable for convenience and to ensure we get the proper global object. Finally we have the variable statinfo which is used to in the call to stat() to make sure the file exists and find out some extra info about the file before trying to execute it.

if (file == NULL){
    return JS_FALSE;
}

if (stat(file, &statinfo) == -1){
    return JS_FALSE;
}

if (!S_ISREG(statinfo.st_mode)){
    return JS_FALSE;
}

Once the variables are declared, we do a few simple validation tests to make sure we can execute this file, hopefully successfully. If the tests fail, we return JS_FALSE to tell the caller the file could not be executed. Right now there isn't any way for the caller to know the specific reason why the file could not be executed. I may add something later, but right now I simply don't care.

The first test is a basic check to make sure we are passed a valid pointer to a file name, and not a NULL pointer. Second we call stat() on the file to get information on the file, and at the same time check to see if the file exists. If the file does exist, or the stat fails for some other reason, return false. Next we check the information retrieved by stat() to see if the filename passed is a regular file, rather than a directory or pipe or some odd thing like that. If it is a regular file, we continue on, if it isn't, we return false to the caller.

script = JS_CompileFile(ctx, global, file);

if (script == NULL){
    return JS_FALSE;
}

Next we use SpiderMonkey's JS_CompileFile to compile the file into the proper format for SpiderMonkey to execute it. This function might return NULL if it's unable to parse and compile the code, therefore we check for a null return value and if one is found, we return false to the caller to inform them that the file could not be executed.

returnVal = JS_ExecuteScript(ctx, global, script, &returnValue);
JS_DestroyScript(ctx, script);
return returnVal;

Last but not least, we use JS_ExecuteScript to execute the script we just compiled. This function will return true or false depending on whether or not the script can be successfully executed or not (not being when there's a runtime error or such). We need to store this in our variable returnVal so that we may pass this information back to the caller as the return value from our function. If desired, it could be captured and acted upon, but that's something best left to the caller. Regardless of whether our script executed successfully or not though, we then need to destroy the compiled script using JS_DestroyScript.

Now that we have our nice js_engine_execute_file() function and know how to use it, let us go fourth and implement it into your main code so that we can execute files! It's really simple with this function. You simply call it and pass in the name of the file to execute, which we have stored in the variable fileToRun, and pass in the context object you want it to be executed in, which we only have one of and that is cx. As usual, click the file name for the complete code.

if (fileToRun){
    if (js_engine_execute_file(cx, fileToRun)){
        printf("File %s has been successfully executed.\n", fileToRun);
    }
    else {
        printf("Failed to executed %s for an unknown reason.\n", fileToRun);
    }
}

Testing your new beast

Now, we need a simple javascript file or two to execute and make sure this works. Remember that JS does not have any native functions or anything to output data, so really all we can do is see if the built-in objects work and make sure the file is successfully executed. We need to stick to only the built-in functions since no custom objects have been created.

Here is a somewhat complex test script I wrote that tests a the Date and Math built in objects, as well as creates a function, calls that function, and uses variables and an object literal syntax. It tests quite a few parts of the standard JS, but is no where near exhaustive. Feel free to take this test script, or write up your own.

In case your wondering what it does, this script defines a function which will calculate the difference between two dates, or the elapsed time if you will. It takes the number of seconds between two dates (or just times) and returns how many years, days, hours, minutes, and seconds have elapsed. As I mentioned above, there's no native JS object to display the results though. I test this function by creating two date objects, and setting one to be the same date as now, but last year. I then get the difference between the two in seconds, and add a random amount of seconds between 0 and the current year (so as of now, that is adding somewhere between 0 and 2006 seconds to the difference) and then call the function.

test.js

var today = new Date();
var lastYear = new Date();
lastYear.setFullYear(today.getFullYear()-1);

/* Takes the difference in seconds between two times and returns an
object that describes how many years, days, hours, minutes, and seconds have
elapsed.
*/
function CalculateTimeDifference(difference){

    var years = Math.floor(difference/(60*60*24*365));
    difference = difference % (60*60*24*365);

    var days = Math.floor(difference/(60*60*24));
    difference = difference % (60*60*24);

    var hours = Math.floor(difference/(60*60));
    difference = difference % (60*60);

    var minutes = Math.floor(difference/60);
    difference = difference % 60;

    var seconds = difference;

    return {years: years, days: days, hours: hours, minutes: minutes, seconds: seconds};
}

var difference = today.getTime() - lastYear.getTime();
//Convert to seconds difference.
difference /= 1000;

//Add in some random amount of time.
difference += Math.random()*today.getFullYear();

var breakdown = CalculateTimeDifference(difference);

So, when you run this script, you should get an output similar to this:

$./engine test.js
File test.js has been successfully executed.

Let's introduce a JS Syntax error just to see what happens when we do. To introduce an error, let's simply delete the word 'function' from line 9, and then re-run the script. You should receive a failed message like below.

$./engine test.js
Failed to executed test.js for an unknown reason.

Attachments

  • engine.c -- Here is an original copy of the code from my last article.
  • engine_r1.c -- Here is the first revision of the engine.c file in this article. This revision adds the js_engine_execute_file() function.
  • engine_r2.c -- Here is the second revision of the engine.c file in this article. This revision activates the new js_engine_execute_file() function by using it in main() to executed the specified file, and return a result message.