Coding With SpiderMonkey

Part III - Console Object

Now that we have successfully compiled the SpiderMonkey engine, and written and compiled a couple programs that make use of the engine and run our scripts, it is now time to start exporting some custom objects which our scripts can then take advantage of. After all, Javascript in its default state is not really all that useful.

Before we go defining our own objects though, I think it would be wise to implement some basic error reporting for our scripts. Having error reporting in place will help as we develop our custom object as we will be able to receive some feedback about whether our objects are getting properly defined and exported to scripts. When we try and use our new object in the script, everything will work if the object was set up properly. If it was not though, we will likely get an error about the object being undefined. To set up error reporting in SpiderMonkey, you use the JS_SetErrorReporter function. This function takes the context you want to set up the error reporting for, and a pointer to a function that will be called whenever an error is encountered. The function pointer needs to point to a function defined as in the style of the JSErrorReporter type, which if you dig around in the source, you will find is defined as so:

typedef void (* JS_DLL_CALLBACK JSErrorReporter)(JSContext *cx, const char *message, JSErrorReport *report);

When an error is encountered, the function is passed in the context in which the error occurred, a message describing what the error that occurred is, and a JSErrorReport structure that defines extended information about the error such as the file the error occurred in, the line number, the source for the line the error occurred on, and where in that line the error occurred. There is more information in that structure, but the ones I mentioned are the only ones I really care much about. In my error handler, I will print the error message and file/line info. I will also use the source code and the pointer to construct an arrow to where the error occurred. Similar to how the firefox javascript console reports its errors. Here is the source for my handler function, along with a link to a complete source file:

static void js_error
_handler(JSContext *ctx, const char *msg, JSErrorReport *er){
    char *pointer=NULL;
    char *line=NULL;
    int len;

    if (er->linebuf != NULL){
        len = er->tokenptr - er->linebuf + 1;
        pointer = malloc(len);
        memset(pointer, '-', len);
        pointer[len-1]='\0';
        pointer[len-2]='^';

        len = strlen(er->linebuf)+1;
        line = malloc(len);
        strncpy(line, er->linebuf, len);
        line[len-1] = '\0';
    }
    else {
        len=0;
        pointer = malloc(1);
        line = malloc(1);
        pointer[0]='\0';
        line[0] = '\0';
    }

    while (len > 0 && (line[len-1] == '\r' || line[len-1] == '\n')){ line[len-1]='\0'; len--; }

    printf("JS Error: %s\nFile: %s:%u\n", msg, er->filename, er->lineno);
    if (line[0]){
        printf("%s\n%s\n", line, pointer);
    }

    free(pointer);
    free(line);
}

To test the new error handler, I simply introduced an error into the source code for test.js. What I did was go ahead and try and use the new Console object that I plan to implement below. It has not been implemented yet though, so of course it throws an error when I try and use it. Here is the line that I added to the end of the script, and the output I got when I attempted to run the new script.

test_r1.js

Console.write(breakdown.minutes);

$./engine test_r1.js
JS Error: ReferenceError: Console is not defined
File: test_r1.js:37
Failed to executed test_r1.js.

Good, now that we have some error reporting, we can start on implementing our first object.

For me, the first object I wanted to have available was something that I could use to print data to the screen. Javascript by default does not provide such a capability and such a capability will be very useful in the future for making sure everything is working properly. Originally I choose to do this via a File object, which I described a little in the first article I wrote. I thought I would go ahead and provide support for reading and writing files at the same time, and do so with the same object. Now, that's not necessarily a horrible idea, but for the purposes of this article, and future articles, I will instead choose to implement a Console object which handles nothing but console I/O using stdin, stdout, and stderr. Here is a brief summer of the object I intend to implement, and the methods it will provide to our scripts.

class Console {
    public:
        static int write(string text);         //Write text to stdout.
        static int writeln(string text);       //Write text to stdout with a new line at the end.
        static int writeError(string text);    //Write text to stderr.
        static int writeErrorln(string text);  //Write text to stderr with a new line at the end.
        static string read();                  //Read a line of text from stdin.
        static string read(int maxchars);      //Read a string up to maxchars in length.
        static int readInt();                  //Read a integer from stdin.
        static float readFloat();              //Read a floating point value from stdin.
        static char readChar();                //Read a single character from stdin.
};

As you can see, I plan on implementing not only methods which will support displaying information, but also methods which will support reading information. This way, I have good I/O system for the console allowing me to make some decent applications using javascript.

The way you go about creating this object in SpiderMonkey is really to define all the methods, and create a specification for them that you pass to the object when you create it, then you export the object to the scripts. This isn't so much the method I learned in while teaching myself, but this is the method I will try and demonstrate here. So, lets begin by creating the necessary methods. By looking around the documentation, you will eventually see that methods are just C functions with a certain signature. This signature is defined by the JSNative function definition. As of right now, this function definition does not seem to be documented, but by looking at the simple "how to" guides, or by looking at the source code for SpiderMonkey, you can find out what the definition is. JSNative is defined like this:

typedef JSBool (* JSNative)(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

Essentially, what this means is that your function needs to return type JSBool, and must accept five parameters. The parameters that it accepts are defined as: * JSContext *cx - This is the context in which the script is being executed. Use this anywhere you need to use the context, such as in calls to other JS_* functions. * JSObject *obj - This is the object which is being worked with. At least, this is what I've gathered it is. I am still not entirely sure, particularly when it comes to object constructor functions. For normal methods on an object though, I'm pretty sure this points to the current instance. * uintN argc - This is how many parameters were passed to the function in the script. If you want to enforce parameters on your method, this is how you would go about doing so. You can also use this to adjust how the function behaves by how many parameters are passed. * jsval *argv - This is an array of jsval types, where each one is one of the parameters passed to the function in the script. * jsval *rval - This is a pointer to a single jsval. This is what the return value of the function call will be in the script.

Writing

Now that we know what a JSNative function looks like, let us create our first function for the Console object. The first function I am going to define is the write function. This is the most useful function we will create. When I was learning I did these all one function at a time. I will do the same here. I will show you how to create the write function, then I will create the object and define it in the script so that we can start using it, and see the write function in action. Then I will go over creating the rest of the functions.

I find it useful to name the function based on the object they belong to and the method they implement in the JS code. As such, I will name this function Console_write. Let me just provide you with the entire function up front, then I will explain what's going on with it and why it's written as it is.

JSBool Console_write(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        /* No arguments passed in, so do nothing. */
        /* We still want to return JS_TRUE though, other wise an exception will be thrown by the engine. */
        *rval = INT_TO_JSVAL(0); /* Send back a return value of 0. */
        return JS_TRUE;
    }
    else {
        /* "Hidden" feature.  The function will accept multiple arguments.  Each one is considered to be a string
        and will be written to the console accordingly.  This makes it possible to avoid concatenation in the code
        by using the Console.write function as so:
        Console.write("Welcome to my application", name, ". I hope you enjoy the ride!");
        */
        int i;
        size_t amountWritten=0;
        for (i=0; i<argc; i++){
            JSString *val = JS_ValueToString(cx, argv[i]); /* Convert the value to a javascript string. */
            char *str = JS_GetStringBytes(val); /* Then convert it to a C-style string. */
            size_t length = JS_GetStringLength(val); /* Get the length of the string, # of chars. */
            amountWritten = fwrite(str, sizeof(*str), length, stdout); /* write the string to stdout. */
        }
        *rval = INT_TO_JSVAL(amountWritten); /* Set the return value to be the number of bytes/chars written */
        return JS_TRUE;
    }
}

First in this function, we check how many arguments were passed. If there were no arguments passed, we simply do nothing and return true to the JS engine, and 0 to the caller by storing 0 in the rval variable. Methods need to return true to the JS engine almost always. If you return false, it will cause the JS engine to generate and throw an exception.

Next, if there were arguments passed in we take them and get the string value for each argument and print it out. You'll notice from the comment that it's actually possible to use this function by passing in multiple arguments, rather than just a single argument.

Now that the write method is implemented, it's time to create the object's class. This is done just like we did when creating the global object in the first article. The only thing that needs to change for now is the name. In addition to the class structure though, we also need to define a JSFunctionSpec array which will define which methods are available for the class. I chose to define this as a global variable just like I do with the class structure.

JSClass js_Console_class = {
    "Console",
    0,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_EnumerateStub,
    JS_ResolveStub,
    JS_ConvertStub,
    JS_FinalizeStub,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

JSFunctionSpec Console_methods[] = {
    {"write", Console_write, 1, 0, 0},
    {NULL},
};

The JSFunctionSpec structure is a simple one. It has five fields. These fields are: * name - The name of the function as scripts will see it. * call - The native C function which the engine will call when a script tries to call this function. * nargs - The number of arguments that the function is supposed to be passed. In my experience so far though, this number does not really seem to make a difference at all. You can still call the function with any number of arguments. * flags - Some flags for the function. Right now this field seems to be unused. The documentation mentions a couple flags, but it also says they should not be used in new code and I have yet to see any other flags defined. * extra - Reserved for later. Just set it to 0.

When defining an array of function specifications, you need to end the array with a specification which has a name of NULL. This is how the function which uses the specification, JS_DefineFunctions knows how many definitions there are. JS_DefineFunctions will take the array of functions just defined and actually associate them with an object, obj.

To create the actual Console object on which the methods will be defined, we will use the JS_DefineObject. There are several ways to define a new object, but JS_DefineObject will work best in our case. What we want to do is create a new object based on the just defined class, and make it a property of the global object so that way it is available to our scripts. JS_DefineObject lets us do that all in one function call. Once the object is defined, we then set up the methods using JS_DefineFunctions and once that is done, we will be ready to test out our new object.

engine_r2.c

/* Define our new object and make it a property of the global object. */
obj = JS_DefineObject(cx, obj, js_Console_class.name, &js_Console_class, NULL, JSPROP_PERMANENT|JSPROP_READONLY|JSPROP_ENUMERATE);
if (!obj){
    printf("Failed to create Console object.\n");
    JS_DestroyContext(cx);
    JS_DestroyRuntime(rt);
    return 1;
}

/* Define the console object's methods. */
JS_DefineFunctions(cx, obj, Console_methods);

Now it's time to test our new object. To do this, lets just add a new statement to the bottom of our script that outputs the results of the calculation of the date difference. The statement I added looks like this:

test_r2.js

Console.write(difference, " represents ", breakdown.years, " years, ", breakdown.days, " days, ",
    breakdown.hours, " hours, ", breakdown.minutes, " minutes, and "+breakdown.seconds+" seconds\n");

Finally, upon running the script I receive this output.

$./engine test_r2.js
31537860 represents 1 years, 0 days, 0 hours, 31 minutes, and 0 seconds
File test_r2.js has been successfully executed.

Now, lets finish creating the rest of the write functions. They are all basically the same thing, just with very minor changes. I won't bother explaining each one. I'll just give you the code, and you can hopefully figure it out based on the explanation above for the write function.

engine_r3.c

JSBool Console_writeln(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    /* Just call the write function and then write out a \n */
    Console_write(cx, obj, argc, argv, rval);
    fwrite("\n", sizeof("\n"), 1, stdout);
    *rval = INT_TO_JSVAL(JSVAL_TO_INT(*rval)+1);
    return JS_TRUE;
}

JSBool Console_writeError(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        *rval = INT_TO_JSVAL(0);
        return JS_TRUE;
    }
    else {
        int i;
        size_t amountWritten=0;
        for (i=0; i<argc; i++){
            JSString *val = JS_ValueToString(cx, argv[i]);
            char *str = JS_GetStringBytes(val);
            size_t length = JS_GetStringLength(val);
            amountWritten = fwrite(str, sizeof(*str), length, stderr);
        }
        *rval = INT_TO_JSVAL(amountWritten);
        return JS_TRUE;
    }
}

JSBool Console_writeErrorln(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    /* Just call the writeError function and then write out a \n */
    Console_writeError(cx, obj, argc, argv, rval);
    fwrite("\n", sizeof("\n"), 1, stderr);
    *rval = INT_TO_JSVAL(JSVAL_TO_INT(*rval)+1);
    return JS_TRUE;
}

I also modified the test.js file to use the new functions, just to make sure they are working right.

test_r3.js

Console.writeError('Calculating time span for ', difference, ' seconds...');
var breakdown = CalculateTimeDifference(difference);
Console.writeErrorln('done');

Console.writeln(breakdown.years, " years, ", breakdown.days, " days, ",
    breakdown.hours, " hours, ", breakdown.minutes, " minutes, and "+breakdown.seconds+" seconds\n");

Reading

Now we have a nice way to write data to the console. We still need a way to read data in though. After all, I/O stands for Input/Output, not just Output :). So now it's time to make our various .read* functions. Let's first off create the generic .read() and .read(bytes) functions. These will actually be a single function in the C source, just like they would be a single function in JS as well. What we will do is simply check the amount and type of arguments passed into the function using the argc/argv parameters.

engine_r4.c

JSBool Console_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        /* Read a full line off the console.  */
        char *finalBuff = NULL;
        char *newBuff = NULL;
        char tmpBuff[100];
        size_t totalLen = 0;
        int len = 0;
        do {
            memset(tmpBuff, 0, sizeof(tmpBuff));
            fgets(tmpBuff, sizeof(tmpBuff), stdin);
            len = strlen(tmpBuff);


            if (len > 0){
                char lastChar = tmpBuff[len-1];

                newBuff = JS_realloc(cx, finalBuff, totalLen+len+1);
                if (newBuff == NULL){
                    JS_free(cx, finalBuff);
                    *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
                    return JS_TRUE;
                }
                else {
                    finalBuff = newBuff;
                    memset(finalBuff+totalLen, 0, len);
                }

                strncat(finalBuff+totalLen, tmpBuff, len);
                totalLen += len;
                if (lastChar == '\n' || lastChar == '\r' || feof(stdin)){
                    JSString *str = JS_NewString(cx, finalBuff, totalLen);
                    *rval = STRING_TO_JSVAL(str);
                    return JS_TRUE;
                }
            }
            else if (feof(stdin) && totalLen == 0){
                *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
                return JS_TRUE;
            }
            else if (totalLen > 0){
                JSString *str = JS_NewString(cx, finalBuff, totalLen);
                *rval = STRING_TO_JSVAL(str);
                return JS_TRUE;
            }
        } while (1);
    }
    else {
        int32 maxlen=0;
        if (JS_ValueToInt32(cx, argv[0], &maxlen) == JS_TRUE){
            JSString *ret=NULL;
            size_t amountRead = 0;
            char *newPointer = NULL;
            char *cstring = JS_malloc(cx, sizeof(*cstring)*(maxlen+1));
            if (cstring == NULL){
                *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
                return JS_TRUE;
            }
            memset(cstring, 0, sizeof(*cstring)*(maxlen+1));
            amountRead = fread(cstring, sizeof(*cstring), maxlen, stdin);
            newPointer = JS_realloc(cx, cstring, amountRead);
            if (newPointer == NULL){
                JS_free(cx, cstring);
                *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
                return JS_TRUE;
            }
            else {
                cstring = newPointer;
            }

            ret = JS_NewString(cx, cstring, sizeof(*cstring)*amountRead);
            *rval = STRING_TO_JSVAL(ret);
            return JS_TRUE;
        }
        else {
            *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
            return JS_TRUE;
        }
    }
}

Obviously, that's a lot more code than the write functions were. The reason why is that we have to do some memory management and data marshaling between C and JS. If you look through the code, you'll notice the use of some new functions. I'll go over each of the new functions below a little. I'm not going to explain the syntax or the standard C stuff much as I expect that you should already know these parts well enough to understand them.

The first new function you will probably notice in there is the use of the JS_realloc function. This function is essentially the same as the standard C realloc function, except it allocates the memory in a way that SpiderMonkey will know about the memory and be able to manage it. You have to use this function to allocate memory that you want to pass back to scripts as strings, as SpiderMonkey needs to be able to control this memory. Along with this function, there are the companion JS_malloc and JS_free functions for you to use rather than the standard malloc and free functions.

The next new function you are likely to see is JS_NewString. This function creates a new string that can be sent to the script. This function does not duplicate what is in the bytes variable though, instead it just re-uses this memory, but in a different format. So, after you call this function, you need to stop doing any work with the character array bytes. Because this memory is reused, it needs to be allocated using either JS_malloc or JS_realloc as mentioned above. This way SpiderMonkey is able to manage it.

Finally, we have the JS_ValueToInt32 function which is used to convert the value in v to an int32 variable in C. There are some rules for how this conversion takes place, but using it essentially allows your script to be a little more lax when it comes to understanding what they pass in. By using this the user can pass the function a string or object or such as well, and if it can be converted to a number, it will be.

That should be enough for you to understand how the read functions will work. Feel free to write if you have additional questions about how these functions work. They are fairly straight forward though. Just make sure you read the linked documentation, and it should all make sense.

Now that we have our generic read function created, lets create the special functions, readInt, readChar and readFloat. These three functions are not all that difficult to write. Rather than cover each individually, I will just write all three and then briefly describe what happens.

engine_r5.c

JSBool Console_readInt(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    int32 readinteger = 0;
    if (fscanf(stdin, "%d", &readinteger) == 1){
        if (JS_NewNumberValue(cx, readinteger, rval) == JS_TRUE){
            return JS_TRUE;
        }
    }
    *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
    return JS_TRUE;
}

JSBool Console_readFloat(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    jsdouble readfloat = 0;
    if (fscanf(stdin, "%lf", &readfloat) == 1){
        if (JS_NewDoubleValue(cx, readfloat, rval) == JS_TRUE){
            return JS_TRUE;
        }
    }
    *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
    return JS_TRUE;
}


JSBool Console_readChar(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    JSString *str=NULL;
    char ch, *ptr=NULL;
    if (feof(stdin)){
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        return JS_TRUE;
    }

    ch=fgetc(stdin);
    if (ch == EOF){
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        return JS_TRUE;
    }

    ptr = JS_malloc(cx, sizeof(char)*1);
    if (ptr == NULL){
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        return JS_TRUE;
    }

    *ptr = ch;
    str = JS_NewString(cx, ptr, sizeof(char)*1);
    *rval = STRING_TO_JSVAL(str);
    return JS_TRUE;
}

As you can see, these functions are pretty easy. I just used the standard fscanf function to read in integers and floats for the readInt and readFloat functions. For readChar I just read a single character using fgetc and passed it back to the script as a one character string.

In all the read functions I made them return false if there was some problem reading from the console, like if EOF was reached somehow. A better idea would probably be to make them throw an exception, but I currently don't know how to throw custom exceptions, so I decided to simply make them return false. I might re-do this in my next article to use exceptions.

Now it's time to test these new functions. To test these functions I created a new test.js file that uses each one. I tried to actually use each one in the script for something, but could not come up w/ a good use for readFloat, so I just threw it in as a replacement for Math.random. As you can see, now we have something of a usable applications.

test_r5.js

Console.write('Please enter your name: ');
var name = Console.read();
if (name === false){
    Console.writeErrorln('I/O Error: Unable to read from console.');
}
else {
    //Trim excess white space from the input.
    name = name.replace(/(^\s*|\s*$)/g, '');

    Console.write(name+', Please enter number of seconds to find time span for: ');
    var difference = Console.readInt();
    if (difference === false){
        Console.writeErrorln('I/O Error: Unable to read from console.');
    }
    else {
        Console.writeErrorln('Read: ', typeof difference, '(', difference, ')');
        //Gobble up the new line that is left on the input stream.
        Console.read();
        Console.write(name+', Please enter a random number between 0 and 1: ');
        var rand = Console.readFloat();
        if (rand === false){
            Console.writeErrorln('I/O Error: Unable to read from console.');
        }
        else {
            Console.writeErrorln('Read: ', typeof rand, '(', rand, ')');
            //Gobble up the new line that is left on the input stream.
            Console.read();
            Console.write('Alright ', name, ', are you ready?[yn]: ');
            var ready = Console.readChar();
            if (ready === false){
                Console.writeErrorln('I/O Error: Unable to read from console.');
            }
            else {
                Console.writeErrorln('Read: ', typeof ready, '(', ready, ')');
                if (ready == 'y' || ready == 'Y'){
                    rand = Math.floor(rand*1000)/1000;
                    Console.writeln('Adding a ', rand*100, ' percent margin.');
                    difference += Math.floor(difference * rand);
                    Console.writeln('New number of seconds is: '+difference);
                    Console.write('Calculating...');
                    var breakdown = CalculateTimeDifference(difference);
                    Console.writeln('done');
                    Console.writeln(breakdown.years, " years, ", breakdown.days, " days, ",
                    breakdown.hours, " hours, ", breakdown.minutes, " minutes, and "+breakdown.seconds+" seconds\n");
                }
            }
        }
    }
}

The big if/else branches are due to the fact that there's no way to stop a script mid-way right now. A workaround for this lacking feature would have been to put this into a function and used return to stop the function when an error was encountered. Also, if exceptions were used instead of returning false on error, then not catching them would allow the script to halt if there were a problem with the input.

A quick run of this new script shows that everything is working just fine. Here is the output I get when I do a test run.

$./engine test_r5.js
Please enter your name: keith
keith, Please enter number of seconds to find time span for: 293838202
Read: number(293838202)
keith, Please enter a random number between 0 and 1: 0.239383
Read: number(0.239383)
Alright keith, are you ready?[yn]: yyeeesssss!!!!
Read: string(y)
Adding a 23.9 percent margin.
New number of seconds is: 364065532
Calculating...done
11 years, 198 days, 17 hours, 18 minutes, and 52 seconds

File test_r5.js has been successfully executed.

Attachments

  • engine.c -- Here is an original copy of the code from my last article.
  • test.js -- Here is an original copy of the test javascript file from my last article.
  • engine_r1.c -- This revision adds an error reporting function.
  • test_r1.js -- This revision has an error in order to test the new error reporting function.
  • engine_r2.c -- This revision adds the Console object, and it's .write function.
  • test_r2.js -- This revision tests the new Console object, and it's .write function.
  • engine_r3.c -- This revision adds the rest of the console object's writing functions.
  • test_r3.js -- This revision tests the rest of the console objects writing functions.
  • engine_r4.c -- This revision adds the console's .read function.
  • test_r4.js -- This revision tests the console's new .read function.
  • engine_r5.c -- This revision adds the rest of the console's reading functions.
  • test_r5.js -- This revision tests the rest of the console's reading functions.

Support free Wi-Fi

openwireless.org

Support free maps

openstreetmap.org