Drawing the red line

Date view Thread view Subject view Author view

From: Godmar Back (gback@marker.cs.utah.edu)
Date: Sun Nov 15 1998 - 21:12:06 EST


 Hi,

I finally attacked the old problem of exceptions thrown in the VM.
For Alexandre: see the kaffe-core mailing list archive for our discussion.

The problem was that certain functions in the VM would throw exceptions,
which would cause them not to return, and this would result in memory leaks
and deadlocks.

We decided to go to a classical return code model instead.
So, I made the necessary changes (I'm done with the jit, will do the
intrp soon).

The changes are large in number (>3000 lines of diff, although that
includes a few other changes in my tree, like the new gc block scheme),
but they are essentially straightforward. I will try to outline the
changes I made before I'll check them in.

The VM code is essentially divided in two sections. Think of it as a
horizontal red line. Above the red line, it is safe to throw exceptions.
Below the red line, exceptions must not be thrown.

For functions below the red line, which previously used to simply throw
errors or exceptions, I added an additional parameter of type
"struct _errorInfo *":

        typedef struct _errorInfo {
                char *classname; /* e.g. "java.lang.NoSuchMethodError" */
                char *mess; /* e.g. "add" */
        } errorInfo;

Each such function signals success or failure using its return value.

For functions returning pointers, a NULL pointer means failure.
For function previously returning void, I chose "bool" as the return
value: note false means failure here. [I'm willing to argue about
these conventions. I always use "if (f() == false)" or "if (f() == true)"
in order to avoid confusion.]

We now have three cases, which I will deal with in order.
I'm giving examples for each case.

1. If a function above the red line calls a function below the red line,
it will pass an errorInfo struct. For example:

void*
soft_new(Hjava_lang_Class* c)
{
        Hjava_lang_Object* obj;
        errorInfo info;

        if (processClass(c, CSTATE_OK, &info) == false) {
                throwError(&info);
        }
        obj = newObject(c);
        return (obj);
}

"throwError" is a new function that takes an errorInfo struct and
creates the corresponding exception objects and throws it.

2. If a function below the red line calls another function
below the red line, it must

    a) pass on its errorInfo*,
    b) check for a failure return code and
    c) if the call failed, clean up its state and return to its
        caller.

The errorInfo is only filled in right where the failure occurs.
For example:

bool
processClass(Hjava_lang_Class* class, int tostate, errorInfo *einfo)
{
        bool success = true; /* optimistic */
        ...
        lockStaticMutex(&classLock);
        ...
                /* Load and link the super class */
                if (class->superclass) {
                        /* propagate failures in super class loading and
                         * processing
                         */
                        class->superclass = getClass((uintp)class->superclass,
                                                        class, einfo);
                        if (class->superclass == 0) {
                                success = false;
                                goto done;
                        }
                        if (processClass(class->superclass, CSTATE_LINKED,
                                         einfo) == false) {
                                success = false;
                                goto done;
                        }
                        ...
                }

done:
                         
        unlockStaticMutex(&classLock);
        return (success);
}

In this example, processClass (below the red line) calls two other functions
below the red line: processClass and getClass.
Notice how it
    a) passes on its einfo
    b) checks for failure (getClass returning NULL, or processClass
       returning NULL)
    c) cleans up state (unlockStaticMutex) and returns false.

Let me give you a second example for a below the red line function
which actually sets the errorInfo:

Method*
findMethod(Hjava_lang_Class* class, Utf8Const* name, Utf8Const* signature, error
Info *einfo)
{
        ...
        for (; class != 0; class = class->superclass) {
                Method* mptr = findMethodLocal(class, name, signature);
                if (mptr != NULL) {
                        return mptr;
                }
        }
        SET_LANG_EXCEPTION_MESSAGE(einfo, NoSuchMethodError, name->data)
        return (0);
}

In this example, findMethod will store the information that an
NoSuchMethodError for a given method occurred in einfo, so that its caller
can later on throw a NoSuchMethodError. This example demonstrates how
the errorInfo stores information for later use.

3. What if a function below the red calls a function above the red line?
Since the function below must not throw any exception, it must
catch all exceptions possibly thrown by the function above the red-line.

Examples for such function calls are

    + invocation of the static initializer in processClass. All exceptions
    thrown there must be mapped to an ExceptionInInitializer exception.
    This is still broken.

    + invocation of a Java method ClassLoader.loadClass from loadClass
    in classMethod.c.

This can be implemented in one of two ways:

    a) using a catch mechanism in C, namely the macros the BEGIN_ and
    END_EXCEPTION macros in jni.c

    b) by calling out to Java wrapper functions which will catch
    Throwable, and always return from there.
    For example, loadClass now doesn't call ClassLoader.loadClass anymore,
    but a private function Java method loadClassVM:

    Hjava_lang_Class*
    loadClass(Utf8Const* name, Hjava_lang_ClassLoader* loader, errorInfo *einfo)
    {
            ...
            if (loader != NULL) {
                    ...
                    clazz = (Hjava_lang_Class*)do_execute_java_method((Hjava_lang_Object*)loader, "loadClassVM", "(Ljava/lang/String;Z)Ljava/lang/Class;", 0, false, str, true).l;
                    if (clazz == NULL) {
                            SET_LANG_EXCEPTION_MESSAGE(einfo, ClassNotFoundException, name->data)
                            return NULL;
                    }
                    ...
            }
            ...
    }

    where we define loadClassVM like so:

    /**
     * The VM will always call loadClassVM, and never loadClass directly
     * because otherwise an exception might go uncaught.
     */
    private Class loadClassVM(String name, boolean resolve) {
            Class clazz = null;
            try {
                    clazz = loadClass(name, resolve);
            } catch (Throwable _) { }
            return clazz;
    }

    Note that in this case, the below the red line function is responsible
    for filling in the errorInfo (SET_LANG_EXCEPTION_MESSAGE does that).

Note that we lose a bit of information in solution b): we will not know
what problem causes loadClass to throw an exception, instead, everything
will look like an ClassNotFoundException. This may or may not be acceptable
to us.

The fact that many functions now won't throw exceptions anymore allowed
me to get rid of some code in jni.c. For instance, in Kaffe_GetFieldID,
the call to lookupClassField won't throw any exceptions. Hence,
Kaffe_GetFieldID can now be coded like so:

jfieldID
Kaffe_GetFieldID(JNIEnv* env, jclass cls, const char* name, const char* sig)
{
        Field* fld;
        errorInfo info;
        
        fld = lookupClassField((Hjava_lang_Class*)cls, makeUtf8Const((char*)name
, -1), false, &info);
        if (fld == NULL) {
                postError(env, &info);
        }
        
        return (fld);
}

where postError post the exception for the JNI code in the usual
manner:

static void
postError(JNIEnv* env, errorInfo* info)
{
        jclass errclass = Kaffe_FindClass(env, info->classname);
        if (errclass != 0) {
                Kaffe_ThrowNew(env, errclass, info->mess);
        }
}

Finally, I should point out that some functions do not do the cleanup
they should be doing. For instance, functions calling lookupClassEntry
will not destroy the centry if the loading of a class failed. This
problem will either be fixed by explicitly destroying centries, or by
exploiting the collector once we attack class gc.

Feedback is welcome, before we'll move on and tackle the even harder problem
of guaranteeing VM consistency in the face of --- ouch! --- asynchronous
exceptions.

        - Godmar


Date view Thread view Subject view Author view

This archive was generated by hypermail 2b29 : Sat Sep 23 2000 - 19:57:03 EDT