Initial checkin of OOPHM plugins into trunk. Testing of non-XPCOM plugins
is still required, and more platforms need to be built.
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5868 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/plugins/xpcom/FFSessionHandler.cpp b/plugins/xpcom/FFSessionHandler.cpp
new file mode 100755
index 0000000..908e755
--- /dev/null
+++ b/plugins/xpcom/FFSessionHandler.cpp
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+#include "FFSessionHandler.h"
+#include "HostChannel.h"
+#include "JavaObject.h"
+#include "JSRunner.h"
+#include "Debug.h"
+#include "XpcomDebug.h"
+#include "scoped_ptr/scoped_ptr.h"
+#include "RootedObject.h"
+#include "InvokeMessage.h"
+#include "ServerMethods.h"
+
+#include "jsapi.h"
+#include "nsCOMPtr.h"
+#include "nsIJSContextStack.h"
+#include "nsIPrincipal.h"
+#include "nsServiceManagerUtils.h"
+
+static JSContext* getJSContext() {
+ // Get JSContext from stack.
+ nsCOMPtr<nsIJSContextStack> stack =
+ do_GetService("@mozilla.org/js/xpc/ContextStack;1");
+ if (!stack) {
+ return NULL;
+ }
+
+ JSContext *cx;
+ if (NS_FAILED(stack->Peek(&cx))) {
+ return NULL;
+ }
+ return cx;
+}
+
+FFSessionHandler::FFSessionHandler(HostChannel* channel)
+ : SessionData(channel, this, getJSContext()), jsObjectId(0),
+ jsObjectsById(NULL), stringObjectClass(NULL) {
+ // TODO(jat): is there a way to avoid calling this twice, without keeping
+ // JSContext in an instance field?
+ JSContext* ctx = getJSContext();
+ if (!JS_AddNamedRoot(ctx, &jsObjectsById, "jsObjectsById")) {
+ Debug::log(Debug::Error) << "Error rooting jsObjectsById" << Debug::flush;
+ }
+ jsObjectsById = JS_NewArrayObject(ctx, 0, NULL);
+ if (!jsObjectsById) {
+ Debug::log(Debug::Error) << "Error rooting jsObjectsById" << Debug::flush;
+ }
+ if (!JS_AddNamedRoot(ctx, &toStringTearOff, "toStringTearOff")) {
+ Debug::log(Debug::Error) << "Error rooting toStringTearOff" << Debug::flush;
+ }
+ getStringObjectClass(ctx);
+ getToStringTearOff(ctx);
+}
+
+void FFSessionHandler::getStringObjectClass(JSContext* ctx) {
+ jsval str = JS_GetEmptyStringValue(ctx);
+ JSObject* obj = 0;
+ if (!JS_ValueToObject(ctx, str, &obj)) {
+ return;
+ }
+ if (!obj) {
+ return;
+ }
+ stringObjectClass = JS_GET_CLASS(ctx, obj);
+}
+
+void FFSessionHandler::getToStringTearOff(JSContext* ctx) {
+ jsval funcVal;
+ if (!JS_GetProperty(ctx, global, "__gwt_makeTearOff", &funcVal)) {
+ Debug::log(Debug::Error) << "Could not get function \"__gwt_makeTearOff\""
+ << Debug::flush;
+ return;
+ }
+ jsval jsargs[3] = {
+ JSVAL_NULL, // no proxy
+ INT_TO_JSVAL(InvokeMessage::TOSTRING_DISP_ID), // dispId
+ JSVAL_ZERO // arg count is zero
+ };
+ if (!JS_CallFunctionValue(ctx, global, funcVal, 3, jsargs, &toStringTearOff)) {
+ jsval exc;
+ if (JS_GetPendingException(ctx, &exc)) {
+ Debug::log(Debug::Error)
+ << "__gwt_makeTearOff(null,0,0) threw exception "
+ << dumpJsVal(ctx, exc) << Debug::flush;
+ } else {
+ Debug::log(Debug::Error) << "Error creating toString tear-off"
+ << Debug::flush;
+ }
+ // TODO(jat): show some crash page and die
+ }
+}
+
+FFSessionHandler::~FFSessionHandler(void) {
+ Debug::log(Debug::Debugging) << "FFSessionHandler::~FFSessionHandler" << Debug::flush;
+ disconnect();
+ if (runtime) {
+ JS_RemoveRootRT(runtime, &jsObjectsById);
+ jsObjectsById = NULL;
+ JS_RemoveRootRT(runtime, &toStringTearOff);
+ runtime = NULL;
+ }
+}
+
+void FFSessionHandler::freeValue(HostChannel& channel, int idCount, const int* ids) {
+ Debug::DebugStream& dbg = Debug::log(Debug::Spam)
+ << "FFSessionHandler::freeValue [ ";
+ JSContext* ctx = getJSContext();
+
+ for (int i = 0; i < idCount; ++i) {
+ int objId = ids[i];
+ dbg << objId << " ";
+ jsval toRemove;
+ if (JS_GetElement(ctx, jsObjectsById, objId, &toRemove) && JSVAL_IS_OBJECT(toRemove)) {
+ jsIdsByObject.erase(JSVAL_TO_OBJECT(toRemove));
+ JS_DeleteElement(ctx, jsObjectsById, objId);
+ } else {
+ Debug::log(Debug::Error) << "Error deleting js objId=" << objId << Debug::flush;
+ }
+ }
+
+ dbg << "]" << Debug::flush;
+}
+
+void FFSessionHandler::loadJsni(HostChannel& channel, const std::string& js) {
+ Debug::log(Debug::Spam) << "FFSessionHandler::loadJsni " << js << "(EOM)" << Debug::flush;
+ JSContext* ctx = getJSContext();
+ if (!JSRunner::eval(ctx, global, js)) {
+ Debug::log(Debug::Error) << "Error executing script" << Debug::flush;
+ }
+}
+
+void FFSessionHandler::sendFreeValues(HostChannel& channel) {
+ unsigned n = javaObjectsToFree.size();
+ if (n) {
+ scoped_array<int> ids(new int[n]);
+ int i = 0;
+ for (std::set<int>::iterator it = javaObjectsToFree.begin();
+ it != javaObjectsToFree.end(); ++it) {
+ ids[i++] = *it;
+ }
+ if (ServerMethods::freeJava(channel, this, n, ids.get())) {
+ javaObjectsToFree.clear();
+ }
+ }
+}
+
+bool FFSessionHandler::invoke(HostChannel& channel, const Value& thisObj, const std::string& methodName,
+ int numArgs, const Value* const args, Value* returnValue) {
+ Debug::log(Debug::Spam) << "FFSessionHandler::invoke " << thisObj.toString()
+ << "::" << methodName << Debug::flush;
+ JSContext* ctx = getJSContext();
+
+ // Used to root JSthis and args while making the JS call
+ // TODO(jat): keep one object and just keep a "stack pointer" into that
+ // object on the native stack so we don't keep allocating/rooting/freeing
+ // an object
+ RootedObject argsRoot(ctx, "FFSessionhandler::invoke");
+ argsRoot = JS_NewArrayObject(ctx, 0, NULL);
+ if (!JS_SetArrayLength(ctx, argsRoot.get(), numArgs + 1)) {
+ Debug::log(Debug::Error)
+ << "FFSessionhandler::invoke - could not set argsRoot length"
+ << Debug::flush;
+ return true;
+ }
+
+ jsval jsThis;
+ if (thisObj.isNull()) {
+ jsThis = OBJECT_TO_JSVAL(global);
+ Debug::log(Debug::Spam) << " using global object for this" << Debug::flush;
+ } else {
+ makeJsvalFromValue(jsThis, ctx, thisObj);
+ if (Debug::level(Debug::Spam)) {
+ Debug::log(Debug::Spam) << " obj=" << dumpJsVal(ctx, jsThis)
+ << Debug::flush;
+ }
+ }
+ if (!JS_SetElement(ctx, argsRoot.get(), 0, &jsThis)) {
+ Debug::log(Debug::Error)
+ << "FFSessionhandler::invoke - could not set argsRoot[0] to this"
+ << Debug::flush;
+ return true;
+ }
+
+ jsval funcVal;
+ // TODO: handle non-ASCII method names
+ if (!JS_GetProperty(ctx, global, methodName.c_str(), &funcVal)) {
+ Debug::log(Debug::Error) << "Could not get function " << methodName << Debug::flush;
+ return true;
+ }
+
+ scoped_array<jsval> jsargs(new jsval[numArgs]);
+ for (int i = 0; i < numArgs; ++i) {
+ makeJsvalFromValue(jsargs[i], ctx, args[i]);
+ if (Debug::level(Debug::Spam)) {
+ Debug::log(Debug::Spam) << " arg[" << i << "] = " << dumpJsVal(ctx,
+ jsargs[i]) << Debug::flush;
+ }
+ if (!JS_SetElement(ctx, argsRoot.get(), i + 1, &jsargs[i])) {
+ Debug::log(Debug::Error)
+ << "FFSessionhandler::invoke - could not set args[" << (i + 1) << "]"
+ << Debug::flush;
+ return true;
+ }
+ }
+
+ if (JS_IsExceptionPending(ctx)) {
+ JS_ClearPendingException(ctx);
+ }
+
+ jsval rval;
+ JSBool ok = JS_CallFunctionValue(ctx, JSVAL_TO_OBJECT(jsThis), funcVal,
+ numArgs, jsargs.get(), &rval);
+
+ if (!ok) {
+ if (JS_GetPendingException(ctx, &rval)) {
+ makeValueFromJsval(*returnValue, ctx, rval);
+ Debug::log(Debug::Debugging) << "FFSessionHandler::invoke "
+ << thisObj.toString() << "::" << methodName << " threw exception "
+ << dumpJsVal(ctx, rval) << Debug::flush;
+ } else {
+ Debug::log(Debug::Error) << "Non-exception failure invoking "
+ << methodName << Debug::flush;
+ returnValue->setUndefined();
+ }
+ } else {
+ makeValueFromJsval(*returnValue, ctx, rval);
+ }
+ Debug::log(Debug::Spam) << " return= " << *returnValue << Debug::flush;
+ return !ok;
+}
+
+/**
+ * Invoke a plugin-provided method with the given args. As above, this method does not own
+ * any of its args.
+ *
+ * Returns true if an exception occurred.
+ */
+bool FFSessionHandler::invokeSpecial(HostChannel& channel, SpecialMethodId method, int numArgs,
+ const Value* const args, Value* returnValue) {
+ Debug::log(Debug::Spam) << "FFSessionHandler::invokeSpecial" << Debug::flush;
+ return false;
+}
+
+/**
+ * Convert UTF16 string to UTF8-encoded std::string.
+ *
+ * @return UTF8-encoded string.
+ */
+static std::string utf8String(const jschar* str, unsigned len) {
+ std::string utf8str;
+ while (len-- > 0) {
+ unsigned ch = *str++;
+ // check for paired surrogates first, leave unpaired surrogates as-is
+ if (ch >= 0xD800 && ch < 0xDC00 && len > 0 && *str >= 0xDC00 && *str < 0xE000) {
+ ch = ((ch & 1023) << 10) + (*str++ & 1023) + 0x10000;
+ len--;
+ }
+ if (ch < 0x80) { // U+0000 - U+007F as 0xxxxxxx
+ utf8str.append(1, ch);
+ } else if (ch < 0x800) { // U+0080 - U+07FF as 110xxxxx 10xxxxxx
+ utf8str.append(1, 0xC0 + ((ch >> 6) & 31));
+ utf8str.append(1, 0x80 + (ch & 63));
+ } else if (ch < 0x10000) { // U+0800 - U+FFFF as 1110xxxx 10xxxxxx 10xxxxxx
+ utf8str.append(1, 0xE0 + ((ch >> 12) & 15));
+ utf8str.append(1, 0x80 + ((ch >> 6) & 63));
+ utf8str.append(1, 0x80 + (ch & 63));
+ } else { // rest as 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ utf8str.append(1, 0xF0 + ((ch >> 18) & 7));
+ utf8str.append(1, 0x80 + ((ch >> 12) & 63));
+ utf8str.append(1, 0x80 + ((ch >> 6) & 63));
+ utf8str.append(1, 0x80 + (ch & 63));
+ }
+ }
+ return utf8str;
+}
+
+/**
+ * Creates a JSString from a UTF8-encoded std::string.
+ *
+ * @return the JSString object, which owns its memory buffer.
+ */
+static JSString* stringUtf8(JSContext* ctx, const std::string& utf8str) {
+ unsigned len = 0;
+ for (unsigned i = 0; i < utf8str.length(); ++i) {
+ char ch = utf8str[i];
+ switch (ch & 0xF8) {
+ // continuation & invalid chars
+ default:
+ // ASCII characters
+ case 0x00: case 0x08: case 0x10: case 0x18:
+ case 0x20: case 0x28: case 0x30: case 0x38:
+ case 0x40: case 0x48: case 0x50: case 0x58:
+ case 0x60: case 0x68: case 0x70: case 0x78:
+ // 2-byte UTF8 characters
+ case 0xC0: case 0xC8: case 0xD0: case 0xD8:
+ // 3-byte UTF8 characters
+ case 0xE0: case 0xE8:
+ ++len;
+ break;
+ case 0xF0:
+ len += 2;
+ break;
+ }
+ }
+ // Account for null terminator even if it isn't included in the string length
+ // Note that buf becomes owned by the JSString and must not be freed here.
+ jschar* buf = static_cast<jschar*>(JS_malloc(ctx, (len + 1) * sizeof(jschar)));
+ if (!buf) {
+ return NULL;
+ }
+ jschar* p = buf;
+ unsigned codePoint;
+ int charsLeft = -1;
+ for (unsigned i = 0; i < utf8str.length(); ++i) {
+ char ch = utf8str[i];
+ if (charsLeft >= 0) {
+ if ((ch & 0xC0) != 0x80) {
+ // invalid, missing continuation character
+ *p++ = static_cast<jschar>(0xFFFD);
+ charsLeft = -1;
+ } else {
+ codePoint = (codePoint << 6) | (ch & 63);
+ if (!--charsLeft) {
+ if (codePoint >= 0x10000) {
+ codePoint -= 0x10000;
+ *p++ = static_cast<jschar>(0xD800 + ((codePoint >> 10) & 1023));
+ *p++ = static_cast<jschar>(0xDC00 + (codePoint & 1023));
+ } else {
+ *p++ = static_cast<jschar>(codePoint);
+ }
+ charsLeft = -1;
+ }
+ }
+ continue;
+ }
+ // Look at the top 5 bits to determine how many bytes are in this character.
+ switch (ch & 0xF8) {
+ default: // skip invalid and continuation chars
+ break;
+ case 0x00: case 0x08: case 0x10: case 0x18:
+ case 0x20: case 0x28: case 0x30: case 0x38:
+ case 0x40: case 0x48: case 0x50: case 0x58:
+ case 0x60: case 0x68: case 0x70: case 0x78:
+ *p++ = static_cast<jschar>(ch);
+ break;
+ case 0xC0: case 0xC8: case 0xD0: case 0xD8:
+ charsLeft = 1;
+ codePoint = ch & 31;
+ break;
+ case 0xE0: case 0xE8:
+ charsLeft = 2;
+ codePoint = ch & 15;
+ break;
+ case 0xF0:
+ charsLeft = 3;
+ codePoint = ch & 7;
+ break;
+ }
+ }
+ // null terminator, apparently some code expects a terminator even though
+ // the strings are counted. Note that this null word should not be included
+ // in the length, and that the buffer becomes owned by the JSString object.
+ *p = 0;
+ return JS_NewUCString(ctx, buf, p - buf);
+}
+
+void FFSessionHandler::makeValueFromJsval(Value& retVal, JSContext* ctx,
+ const jsval& value) {
+ if (JSVAL_IS_VOID(value)) {
+ retVal.setUndefined();
+ } else if (JSVAL_IS_NULL(value)) {
+ retVal.setNull();
+ } else if (JSVAL_IS_INT(value)) {
+ retVal.setInt(JSVAL_TO_INT(value));
+ } else if (JSVAL_IS_BOOLEAN(value)) {
+ retVal.setBoolean(JSVAL_TO_BOOLEAN(value));
+ } else if (JSVAL_IS_STRING(value)) {
+ JSString* str = JSVAL_TO_STRING(value);
+ retVal.setString(utf8String(JS_GetStringChars(str),
+ JS_GetStringLength(str)));
+ } else if (JSVAL_IS_DOUBLE(value)) {
+ retVal.setDouble(*JSVAL_TO_DOUBLE(value));
+ } else if (JSVAL_IS_OBJECT(value)) {
+ JSObject* obj = JSVAL_TO_OBJECT(value);
+ if (JavaObject::isJavaObject(ctx, obj)) {
+ retVal.setJavaObject(JavaObject::getObjectId(ctx, obj));
+ } else if (JS_GET_CLASS(ctx, obj) == stringObjectClass) {
+ // JS String wrapper object, treat as a string primitive
+ JSString* str = JS_ValueToString(ctx, value);
+ retVal.setString(utf8String(JS_GetStringChars(str),
+ JS_GetStringLength(str)));
+ // str will be garbage-collected, does not need to be freed
+ } else {
+ // It's a plain-old JavaScript Object
+ std::map<JSObject*, int>::iterator it = jsIdsByObject.find(obj);
+ if (it != jsIdsByObject.end()) {
+ retVal.setJsObjectId(it->second);
+ } else {
+ // Allocate a new id
+ int objId = ++jsObjectId;
+ JS_SetElement(ctx, jsObjectsById, objId, const_cast<jsval*>(&value));
+ jsIdsByObject[obj] = objId;
+ retVal.setJsObjectId(objId);
+ }
+ }
+ } else {
+ Debug::log(Debug::Error) << "Unhandled jsval type " << Debug::flush;
+ retVal.setString("Unhandled jsval type");
+ }
+}
+
+void FFSessionHandler::makeJsvalFromValue(jsval& retVal, JSContext* ctx,
+ const Value& value) {
+ switch (value.getType()) {
+ case Value::NULL_TYPE:
+ retVal = JSVAL_NULL;
+ break;
+ case Value::BOOLEAN:
+ retVal = BOOLEAN_TO_JSVAL(value.getBoolean());
+ break;
+ case Value::BYTE:
+ retVal = INT_TO_JSVAL((int) value.getByte());
+ break;
+ case Value::CHAR:
+ retVal = INT_TO_JSVAL((int) value.getChar());
+ break;
+ case Value::SHORT:
+ retVal = INT_TO_JSVAL((int) value.getShort());
+ break;
+ case Value::INT: {
+ int intValue = value.getInt();
+ if (INT_FITS_IN_JSVAL(intValue)) {
+ retVal = INT_TO_JSVAL(intValue);
+ } else {
+ JS_NewNumberValue(ctx, (jsdouble) intValue, &retVal);
+ }
+ break;
+ }
+ // TODO(jat): do we still need long support in the wire format and Value?
+// case Value::LONG:
+// retVal = value.getLong();
+// break;
+ case Value::FLOAT:
+ JS_NewNumberValue(ctx, (jsdouble) value.getFloat(), &retVal);
+ break;
+ case Value::DOUBLE:
+ JS_NewNumberValue(ctx, (jsdouble) value.getDouble(), &retVal);
+ break;
+ case Value::STRING:
+ {
+ JSString* str = stringUtf8(ctx, value.getString());
+ retVal = STRING_TO_JSVAL(str);
+ }
+ break;
+ case Value::JAVA_OBJECT:
+ {
+ int javaId = value.getJavaObjectId();
+ std::map<int, JSObject*>::iterator i = javaObjectsById.find(javaId);
+ if (i == javaObjectsById.end()) {
+ JSObject* obj = JavaObject::construct(ctx, this, javaId);
+ javaObjectsById[javaId] = obj;
+ // We may have previously released the proxy for the same object id,
+ // but have not yet sent a free message back to the server.
+ javaObjectsToFree.erase(javaId);
+ retVal = OBJECT_TO_JSVAL(obj);
+ } else {
+ retVal = OBJECT_TO_JSVAL(i->second);
+ }
+ }
+ break;
+ case Value::JS_OBJECT:
+ {
+ int jsId = value.getJsObjectId();
+ if (!JS_GetElement(ctx, jsObjectsById, jsId, &retVal)) {
+ Debug::log(Debug::Error) << "Error getting jsObject with id " << jsId << Debug::flush;
+ }
+ if (!JSVAL_IS_OBJECT(retVal)) {
+ Debug::log(Debug::Error) << "Missing jsObject with id " << jsId << Debug::flush;
+ }
+ }
+ break;
+ case Value::UNDEFINED:
+ retVal = JSVAL_VOID;
+ break;
+ default:
+ Debug::log(Debug::Error) << "Unknown Value type " << value.toString() << Debug::flush;
+ }
+}
+
+void FFSessionHandler::freeJavaObject(int objectId) {
+ if (!javaObjectsById.erase(objectId)) {
+ Debug::log(Debug::Error) << "Trying to free unknown JavaObject: " << objectId << Debug::flush;
+ return;
+ }
+ javaObjectsToFree.insert(objectId);
+}
+
+void FFSessionHandler::disconnect() {
+ Debug::log(Debug::Debugging) << "FFSessionHandler::disconnect" << Debug::flush;
+ JSContext* ctx = getJSContext();
+ bool freeCtx = false;
+ if (!ctx) {
+ Debug::log(Debug::Debugging) << " creating temporary context"
+ << Debug::flush;
+ freeCtx = true;
+ ctx = JS_NewContext(runtime, 8192);
+ if (ctx) {
+ JS_SetOptions(ctx, JSOPTION_VAROBJFIX);
+#ifdef JSVERSION_LATEST
+ JS_SetVersion(ctx, JSVERSION_LATEST);
+#endif
+ }
+ }
+ if (ctx) {
+ JS_BeginRequest(ctx);
+ for (std::map<int, JSObject*>::iterator it = javaObjectsById.begin();
+ it != javaObjectsById.end(); ++it) {
+ int javaId = it->first;
+ JSObject* obj = it->second;
+ if (JavaObject::isJavaObject(ctx, obj)) {
+ // clear the SessionData pointer -- JavaObject knows it is
+ // disconnected if this is null
+ JS_SetPrivate(ctx, obj, NULL);
+ javaObjectsToFree.erase(javaId);
+ }
+ }
+ JS_EndRequest(ctx);
+ if (freeCtx) {
+ JS_DestroyContext(ctx);
+ }
+ } else {
+ Debug::log(Debug::Warning)
+ << "FFSessionHandler::disconnect - no context available"
+ << Debug::flush;
+ }
+ HostChannel* channel = getHostChannel();
+ if (channel->isConnected()) {
+ channel->disconnectFromHost();
+ }
+}