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();
+  }
+}