Coding With SpiderMonkey

Part IV - Files and Exceptions

Finally got around to creating another article for SpiderMonkey. In this article, I will look at creating a new file object that will be used to get information about files, and create, delete, rename, or copy files around on the file system. I will also look into exceptions for use when there is an error, like when creating a file or when an incorrect number of arguments is passed to a method.

I will also look at a couple new things that I changed from the last article, and show what I think is probably the more proper way to go about doing this. An example of such a change is with how I defined the Console object. I now will be using the JS_InitClass function instead of JS_DefineObject to create the console object. They have the same effect in the code, but I believe that using JS_InitClass is more proper.

The bugs squashed.

As I mentioned above, I changed how I defined the Console object in the code. Here is the code that I was using previously to define the console object.

engine.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);

This worked fine, and produced the effect I wanted. It felt like a little bit of a hack though when I did this. I did not find another way though. While working on the code for this article, I finally found the other way which feels more proper to me. This other way is to use the JS_InitClass function. This function registers and prepares a class in spider monkey which then allows it to be access from inside the scripts. According to the documentation, if the constructor parameter is NULL, then static_ps, and static_fs are also null. However, I violated this when creating my Console object. I don't want this object to have a constructor, but I do want it to have static methods. I passed NULL for constructor and static_ps but a function spec array for static_fs. This seems to work just fine.

Passing the function spec in for the fs parameter appears to work equally well. I am unsure which is proper. Using the static_fs makes more sense as they are static functions. The documentation says no to that however. I guess then it must be the other way. Passing the methods in through fs must be proper. So, here is the code I am using now.

engine_r1.c

/* Define our new object. */
obj = JS_InitClass(cx, globalObj, NULL, &js_Console_class, NULL, 0, NULL, Console_methods, NULL, NULL);
if (!obj){
    printf("Failed to create Console object.\n");
    JS_DestroyContext(cx);
    JS_DestroyRuntime(rt);
    return 1;
}

You may have notice in the example code above I used the variable globalObj which does not appear in the previous articles code. That is one more change I made. When I defined the global object, I saved it to it's own variable rather than re-using the obj variable. This way I can keep access to it for later objects I define, such as the file object. So the definition of the global object now looks like this:

engine_r1.c

JSObject *globalObj=NULL;
JSObject *obj=NULL;

...

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

JS_InitStandardClasses(cx, globalObj);

Fairly simple and straight forward. I just declared a new JSObject variable called globalObj and used it wherever I wanted to reference the global object.

Try it! Oops, never mind.

Exceptions are kind of nice occasionally. I still haven't come to like them so much that I use them everywhere, but they are nice in some instances. What I will show you next is how to throw an exception from within your objects in case of something bad. I'll show this in three examples. The first example is for the constructor for the File object. It requires that one parameter, the file name, be passed. No parameters passed generate an exception, while additional parameters are simply ignored. The second example also deals with parameter count, but instead with the static create() function. The third example is an exception that is thrown if you try and create a file that already exists, or there is an error while trying to create the file.

To throw an exception from one of your methods with SpiderMonkey, you use the JS_SetPendingException function. You can send back any javascript value as the exception's content. For my purposes, I just threw back a simple JSString that described the problem which occurred. This seemed to be the easiest method, while still allowing to provide enough information for a developer to diagnose the problem.

Before I get started, let me describe the file object I am going to create to demonstrate exceptions. Here is a c++ like definition of the object:

class File {
    File(string filename);
    static void create(string filename);
    static void touch(string filename);
    static bool exists(string filename);
    void create();
    void touch();
    bool exists();
};

Each method throws an exception. create() will throw an exception if the file already exists. touch() and exists will throw an exception if something happens such that the stat() call fails. That should be rare, but it is covered just in case. In the next section I will go over the code for this file object and you will get to see this exception handling code in action.

Files galore

Now that you see the structure of the object, lets get into some code. First, we have the JSClass spec to define the object and the JSFunctionSpec to define the methods. We declare these as global variables so they are accessible anywhere.

engine_r2.c

//Function prototypes.
JSBool File_ctor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool File_static_create(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool File_static_touch(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool File_static_exists(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool File_exists(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool File_create(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool File_touch(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

...

//Class definition
JSClass js_File_class = {
"File",
    JSCLASS_HAS_PRIVATE,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_EnumerateStub,
    JS_ResolveStub,
    JS_ConvertStub,
    JS_FinalizeStub,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

...

//Structure to hold file details when a file object is created
//Initialized in the File constructor.
struct _FileInformation {
    char *filename;
    FILE *fp;
};

typedef struct _FileInformation FileInformation;

//Non-static methods.
JSFunctionSpec File_methods[] = {
    {"create", File_create, 0, 0 ,0},
    {"touch", File_touch, 0, 0, 0},
    {"exists", File_exists, 0, 0 ,0},
    {NULL}
};

//Static methods.
JSFunctionSpec File_static_methods[] = {
    {"create", File_static_create, 0, 0 ,0},
    {"touch", File_static_touch, 0, 0, 0},
    {"exists", File_static_exists, 0, 0 ,0},
    {NULL}
};

First up, let's setup the static methods for the new File object so we have an easy and convenient way to test the functionality of the functions. The static functions will be nearly identical to the non static functions, the only difference will be where we obtain the name of the file upon which we will be acting. In a static context we will get the file name as the first parameter, while in a non-static context it will be what was specified in the constructor.

engine_r2.c

JSBool File_static_create(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        jsval exception = printf_exception("Not enough parameters to create file.  File.create(<filename>)");
        JS_SetPendingException(cx, exception);
        return JS_FALSE;
    }
    else {
        char *str=NULL;
        size_t length=0;
        struct stat existsCheck;

        length = JSString_to_CString(JS_ValueToString(cx, argv[0]), &str);


        /* First see if the file already exists.  If it does, throw an exception. */

        if (stat(str, &existsCheck) == 0){
            jsval exception = printf_exception("File '%s' already exists, cannot create.", str);
            JS_SetPendingException(cx, exception);
            return JS_FALSE;
        }
        else if (errno == ENOENT){
            FILE *fp = NULL;
            fp = fopen(str, "w");
            if (fp){
                fclose(fp);
                *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
            }
            else {
                *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
            }
            return JS_TRUE;
        }
        else {
            char *msg = staterrorstr(errno);
            jsval exception = printf_exception("%s", msg);
            free(msg);
            JS_SetPendingException(cx, STRING_TO_JSVAL(exception));
            return JS_FALSE;
        }
    }
}

First off, we check the number of arguments passed to our static function. If there are no arguments passed in, we throw an exception saying we are missing the proper arguments. The function is only defined to take one argument. However, if more than a single argument is passed we will just ignore the rest of the arguments rather than throw an error. This allows for some future expandability of the object.

After we validate the proper number of arguments, we extract the first argument into a C-String variable so that we can use it in our C functions for file operations. The first operation we want to do is a file exists check. We want to throw an exception if a file already exists and we are trying to create it. We check if a file exists by calling stat() on the file and checking the return value. If for some reason the stat() call fails other than a "file does not exist" error, we will throw an exception with an error message describing what is wrong.

Once we have verified that the file does not yet exist, we will create it using the standard fopen() function and the "w" mode parameter. We check to make sure the file was created and we have a valid file pointer. If it was, we close the file and return true. If we got a NULL file pointer back, we just return false.

The other two functions are essentially the same deal, with just slightly different checks on the file name. I wont describe them in detail, but just show the code for you.

engine_r2.c

JSBool File_static_touch(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        jsval exception = printf_exception(cx, "Incorrect number of agruments passed.  Expected one.");
        JS_SetPendingException(cx, exception);
    }
    else {
        struct stat existsCheck;
        char *file = NULL;
        JSString_to_CString(JS_ValueToString(cx, argv[0]), &file);

        if (stat(file, &existsCheck) == 0){
            struct utimbuf times;
            times.actime = time(NULL);
            times.modtime = time(NULL);

            if (utime(file, &times) == 0){
                *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
            }
            else {
                char *msg = staterrorstr(errno);
                jsval exception = printf_exception(cx, "touch failed.  (%d) %s", errno, msg);
                free(msg);
                JS_SetPendingException(cx, exception);
                return JS_FALSE;
            }
        }
        else if (errno == ENOENT){
            FILE *fp = fopen(file, "w");
            if (fp){
                fclose(fp);
                *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
            }
            else {
                char *msg = staterrorstr(errno);
                jsval exception = printf_exception(cx, "touch failed, could not create file.  (%d) %s", errno, msg);
                free(msg);
                JS_SetPendingException(cx, exception);
                return JS_FALSE;
            }
        }
        else {
            char *msg = staterrorstr(errno);
            jsval exception = printf_exception(cx, "Existance check failed (%d) %s", errno, msg);
            free(msg);
            JS_SetPendingException(cx, exception);
            return JS_FALSE;
        }
    }
    return JS_TRUE;
}

JSBool File_static_exists(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        jsval exception = printf_exception(cx, "Incorrect number of agruments passed.  Expected one.");
        JS_SetPendingException(cx, exception);
    }
    else {
        struct stat existsCheck;
        char *file = NULL;
        JSString_to_CString(JS_ValueToString(cx, argv[0]), &file);

        if (stat(file, &xistsCheck) == 0){
            *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
        }
        else if (errno == ENOENT){
            *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        }
        else {
            char *msg = staterrorstr(errno);
            jsval exception = printf_exception(cx, "Existance check failed (%d) %s", errno, msg);
            free(msg);
            JS_SetPendingException(cx, exception);
            return JS_FALSE;
        }
    }
    return JS_TRUE;
}

Construction ahead

Now we can look at constructing a file object. This makes the file object useful because you can work with multiple files easily when you can construct a separate object for each one. For our constructor, we will only require the person to pass the name of the file. The file need not exist at the time of construction, as we will have exists() and create() methods that can be used as part of your file operations.

A constructor function is setup with the same prototype as other functions. It takes the context, and object, parameter count, parameter array and return value pointer for it's parameters. The obj is the new object that is created and represents the 'this' variable inside of javascript. The return value pointer is not used for constructors as there is no return value.

If you specified the JSCLASS_HAS_PRIVATE flag in your class definition, then you will also have the ability to set a private member item to your object. This will probably be a pointer to a structure in most cases. That is what I will be using for my file object, so there is room to expand the private variables list in the future. For now, I simply have a structure holding the filename, and a null file pointer. You can set this private data by using the JS_SetPrivate function. You can retrieve this private data later in other functions by using the companion function JS_GetPrivate

So, lets have a look at the code for the constructor and non-static member functions. Like I mentioned, it is basically the same as the code for the static member functions, except that we grab the file name from the object instead of the parameter list.

engine_r2.c

JSBool File_ctor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    if (argc < 1){
        char *str = strdup("Not enough parameters to File constructor.");
        JSString *exceptionDescription = JS_NewString(cx, str, strlen(str));
        JS_SetPendingException(cx, STRING_TO_JSVAL(exceptionDescription));
        return JS_FALSE;
    }
    else {
        char *file=NULL;
        size_t fileNameLength=0;

        FileInformation *info = malloc(sizeof(*info));
        info->fp = NULL;
        info->filename = NULL;

        fileNameLength = JSString_to_CString(JS_ValueToString(cx, argv[0]), &file);

        info->filename = strdup(file);
        JS_SetPrivate(cx, obj, info);
    }
    return JS_TRUE;
}

JSBool File_create(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    /* First see if the file already exists.  If it does, throw an exception. */
    struct stat existsCheck;
    FileInformation *info = (FileInformation*)JS_GetPrivate(cx, obj);

    if (stat(info->filename, &existsCheck) == 0){
        jsval exception = printf_exception(cx, "Cannot create file '%s'.  A file by that name already exists.", info->filename);
        JS_SetPendingException(cx, exception);
        return JS_FALSE;
    }
    else if (errno == ENOENT){
        FILE *fp = NULL;
        fp = fopen(info->filename, "w");
        if (fp){
        fclose(fp);
        *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
        }
        else {
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        }
        return JS_TRUE;
    }
    else {
        char *msg = staterrorstr(errno);
        jsval exception = printf_exception(cx, "Creation failed.  (%d) %s", errno, msg);
        free(msg);
        JS_SetPendingException(cx, exception);
        return JS_FALSE;
    }
    return JS_TRUE;
}

JSBool File_touch(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    struct stat existsCheck;
    FileInformation *info = (FileInformation*)JS_GetPrivate(cx, obj);

    if (stat(info->filename, &existsCheck) == 0){
        struct utimbuf times;
        times.actime = time(NULL);
        times.modtime = time(NULL);

        if (utime(info->filename, &times) == 0){
            *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
        }
        else {
            char *msg = staterrorstr(errno);
            jsval exception = printf_exception(cx, "touch failed.  (%d) %s", errno, msg);
            free(msg);
            JS_SetPendingException(cx, exception);
            return JS_FALSE;
        }
    }
    else if (errno == ENOENT){
        FILE *fp = fopen(info->filename, "w");
        if (fp){
            fclose(fp);
            *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
        }
        else {
            char *msg = staterrorstr(errno);
            jsval exception = printf_exception(cx, "touch failed, could not create file.  (%d) %s", errno, msg);
            free(msg);
            JS_SetPendingException(cx, exception);
            return JS_FALSE;
        }
    }
    else {
        char *msg = staterrorstr(errno);
        jsval exception = printf_exception(cx, "Existance check failed (%d) %s", errno, msg);
        free(msg);
        JS_SetPendingException(cx, exception);
        return JS_FALSE;
    }
    return JS_TRUE;
}

JSBool File_exists(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
    /* First see if the file already exists.  If it does, throw an exception. */
    struct stat existsCheck;
    FileInformation *info = (FileInformation*)JS_GetPrivate(cx, obj);

    if (stat(info->filename, &existsCheck) == 0){
        *rval = BOOLEAN_TO_JSVAL(JS_TRUE);
    }
    else if (errno == ENOENT){
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
    }
    else {
        char *msg = staterrorstr(errno);
        jsval exception = printf_exception(cx, "Existance check failed. (%d) %s", errno, msg);
        free(msg);
        JS_SetPendingException(cx, exception);
        return JS_FALSE;
    }
    return JS_TRUE;
}

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 a new file class with a constructor and two methods. It includes exceptions which are thrown in case of errors when using this new object.
  • test_r1.js -- This revision tests the new file object.
  • engine_r2.c -- This revision contains a complete new file object. Constructor, static, and non-static methods are all implemented.
  • test_r2.js -- This revision tests the new file object.