Evaluating JavaScript in an Embedded XULRunner/Gecko Window

I intended to write something with more substance tonight, but I’m
exhausted from wrasslin’ with Gecko/XULRunner/SpiderMonkey in a
days-long marathon debugging session. None of you will understand
this entry, because its intent is to contain enough keywords and
content that others don’t have to go through the pain that I did.

If you’re embedding Gecko/XULRunner/SpiderMonkey into your
application, and you want to evaluate some JavaScript in the context
of an nsIDOMWindow or nsIWebBrowser, you’d think you’d have many
approaches. You could call JS_EvaluateScript or JS_EvaluateUCScript
directly, getting the JSContext from the nsIScriptContext and the
JSObject* global from the nsIScriptGlobalObject… However, I simply
could not get this to work: I kept running into crazy errors inside of
JS_InitArrayClass. I still don’t understand those errors.

People suggested using EvaluateString and EvaluateStringWithValue on
nsIScriptContext, but that failed in an empty window (I define empty
as not having called nsIWebNavigation::LoadURI) because it did not
have a security principal (nsIPrincipal). Eventually I learned that
you can grab the system principal from the nsIScriptSecurityManager
service and pass that directly to EvaluateStringWithValue. With a few
more minor details, this approach worked in all cases that we care
about so far!

Here is the final magic incantation:

typedef std::map<jsval, boost::python::object> ReferenceMap;

boost::python::object GeckoWindow::evalJavaScript(const std::wstring& js) {
    nsresult rv;

    nsCOMPtr<nsIPrincipal> principal;
    nsCOMPtr<nsIScriptSecurityManager> secMan = do_GetService(
        NS_SCRIPTSECURITYMANAGER_CONTRACTID);
    rv = secMan->GetSystemPrincipal(getter_AddRefs(principal));
    if (NS_FAILED(rv)) {
        throw GeckoError("Failed to get system principal");
    }

    nsCOMPtr<nsIScriptGlobalObject> sgo = do_GetInterface(webBrowser);
    nsCOMPtr<nsIScriptContext> ctx = sgo->GetContext();

    JSContext* cx = reinterpret_cast<JSContext*>(ctx->GetNativeContext());
    Assert(cx);
    uint32 previous = JS_SetOptions(
        cx,
        JS_GetOptions(cx) | JSOPTION_DONT_REPORT_UNCAUGHT);

    jsval out;
    rv = ctx->EvaluateStringWithValue(
        nsString(js.data(), js.size()),
        sgo->GetGlobalJSObject(),
        principal,
        "mozembed",
        0,
        nsnull,
        &out,
        nsnull);

    JS_SetOptions(cx, previous);

    JSAutoRequest ar(cx);
    JSAutoLocalRootScope alrs(cx);

    maybeThrowPythonExceptionFromJsContext(cx);

    if (NS_SUCCEEDED(rv)) {
        ReferenceMap references;
        return buildPythonObjectFromJsval(references, cx, out);
    } else {
        throw GeckoEvalUnknownError("eval failed with no exception set");
    }
}

void GeckoWindow::maybeThrowPythonExceptionFromJsContext(JSContext* cx) {
    jsval exception;
    if (JS_GetPendingException(cx, &exception)) {
        JS_ClearPendingException(cx);
        ReferenceMap references;
        boost::python::object py_exc_value(buildPythonObjectFromJsval(
            references,
            cx,
            exception));
        throw GeckoEvalError(py_exc_value.ptr());
    }
}

boost::python::object GeckoWindow::buildPythonObjectFromJsval(
    ReferenceMap& references,
    JSContext* cx,
    const jsval v
) {
    using namespace boost::python;

    if (v == JSVAL_TRUE) {
        return object(handle<>(Py_True));
    } else if (v == JSVAL_FALSE) {
        return object(handle<>(Py_False));
    } else if (v == JSVAL_NULL) {
        return object(handle<>(Py_None));
    } else if (v == JSVAL_VOID) {
        return object(handle<>(Py_None));
    } else if (JSVAL_IS_INT(v)) {
        return object(handle<>(PyInt_FromLong(JSVAL_TO_INT(v))));
    } else if (JSVAL_IS_NUMBER(v)) {
        return object(handle<>(PyFloat_FromDouble(*JSVAL_TO_DOUBLE(v))));
    // } else if (JSVAL_IS_STRING(v)) {
    //
    } else if (JSVAL_IS_OBJECT(v)) {
        JSObject* obj = JSVAL_TO_OBJECT(v);

        if (references.count(v)) {
            return references[v];
        }

        if (JS_IsArrayObject(cx, obj)) {
            list rv;
            references[v] = rv;
            jsuint length;
            if (JS_GetArrayLength(cx, obj, &length)) {
                jsval element;
                for (jsuint i = 0; i < length; ++i) {
                    if (JS_GetElement(cx, obj, i, &element)) {
                        rv.append(buildPythonObjectFromJsval(references, cx, element));
                    }
                }
            }
            return rv;
        } else {
            dict rv;
            references[v] = rv;

            JSObject* iterator = JS_NewPropertyIterator(cx, obj);
            if (!iterator) {
                throw GeckoEvalUnknownError("Error creating object property iterator while marshalling");
            }
            for (;;) {
                jsid propertyName;
                if (!JS_NextProperty(cx, iterator, &propertyName)) {
                    throw GeckoEvalUnknownError("Error enumerating property list of object while marshalling");
                }

                if (propertyName == JSVAL_VOID) {
                    break;
                }

                jsval propertyNameValue;
                jsval propertyValue;
                object k;

                if (!JS_IdToValue(cx, propertyName, &propertyNameValue)) {
                    throw GeckoEvalUnknownError("Error converting property name to jsval while marshalling");
                }
                if (JSVAL_IS_INT(propertyNameValue)) {
                    jsint propertyIndex = JSVAL_TO_INT(propertyNameValue);
                    k = long_(propertyIndex);

                    if (!JS_LookupElement(cx, obj, propertyIndex, &propertyValue)) {
                        throw GeckoEvalUnknownError("Error looking up property value by index");
                    }
                } else if (JSVAL_IS_STRING(propertyNameValue)) {
                    JSString* kjsstr = JSVAL_TO_STRING(propertyNameValue);
                    std::wstring kstr(JS_GetStringChars(kjsstr), JS_GetStringLength(kjsstr));
                    k = object(kstr);

                    if (!JS_LookupUCProperty(cx, obj, kstr.c_str(), kstr.size(), &propertyValue)) {
                        throw GeckoEvalUnknownError("Error looking up property value by name");
                    }
                } else {
                    throw GeckoEvalUnknownError("Unknown property name type while marshalling");
                }

                rv[k] = buildPythonObjectFromJsval(references, cx, propertyValue);
            }
            return rv;
        }
    } else {
        // We don't know what type it is, or we can't marshal it,
        // so convert it to a string and hope for the best...
        JSString* string = JS_ValueToString(cx, v);
        return str(std::wstring(JS_GetStringChars(string), JS_GetStringLength(string)));
    }
}

Hope that helps, and Godspeed.

1 thought on “Evaluating JavaScript in an Embedded XULRunner/Gecko Window”

  1. More magic:

    // This is important: we need to ensure the 'inner' DOM window is created.
    // If we don't, it is initialized while XPConnect is walking the outer scope
    // chain, which reinitializes the JS context.  This causes JS_SetOptions to be
    // called in the middle of execution, disabling our DONT_REPORT_UNCAUGHT flag,
    // which prevents exceptions from propagating out.
    nsCOMPtr<nsIDOMWindow> domWindow;
    rv = webBrowser->GetContentDOMWindow(getter_AddRefs(domWindow));
    if (NS_FAILED(rv)) {
        throw GeckoError("Failed to get content DOM window");
    }
    nsCOMPtr<nsPIDOMWindow> outer(do_QueryInterface(domWindow));
    nsCOMPtr<nsPIDOMWindow>(outer->EnsureInnerWindow());
    

    Run this code before calling JS_SetOptions.

Leave a Reply

Your email address will not be published. Required fields are marked *