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