michael@0: /* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "NativeJSContainer.h" michael@0: #include "AndroidBridge.h" michael@0: #include "mozilla/Vector.h" michael@0: #include "prthread.h" michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::widget; michael@0: michael@0: namespace mozilla { michael@0: namespace widget { michael@0: michael@0: class NativeJSContainer michael@0: { michael@0: public: michael@0: static void InitJNI(JNIEnv* env) { michael@0: if (jNativeJSContainer) { michael@0: return; michael@0: } michael@0: jNativeJSContainer = AndroidBridge::GetClassGlobalRef( michael@0: env, "org/mozilla/gecko/util/NativeJSContainer"); michael@0: MOZ_ASSERT(jNativeJSContainer); michael@0: jContainerNativeObject = AndroidBridge::GetFieldID( michael@0: env, jNativeJSContainer, "mNativeObject", "J"); michael@0: MOZ_ASSERT(jContainerNativeObject); michael@0: jContainerConstructor = AndroidBridge::GetMethodID( michael@0: env, jNativeJSContainer, "", "(J)V"); michael@0: MOZ_ASSERT(jContainerConstructor); michael@0: michael@0: jNativeJSObject = AndroidBridge::GetClassGlobalRef( michael@0: env, "org/mozilla/gecko/util/NativeJSObject"); michael@0: MOZ_ASSERT(jNativeJSObject); michael@0: jObjectContainer = AndroidBridge::GetFieldID( michael@0: env, jNativeJSObject, "mContainer", michael@0: "Lorg/mozilla/gecko/util/NativeJSContainer;"); michael@0: MOZ_ASSERT(jObjectContainer); michael@0: jObjectIndex = AndroidBridge::GetFieldID( michael@0: env, jNativeJSObject, "mObjectIndex", "I"); michael@0: MOZ_ASSERT(jObjectIndex); michael@0: jObjectConstructor = AndroidBridge::GetMethodID( michael@0: env, jNativeJSObject, "", michael@0: "(Lorg/mozilla/gecko/util/NativeJSContainer;I)V"); michael@0: MOZ_ASSERT(jContainerConstructor); michael@0: } michael@0: michael@0: static jobject CreateInstance(JNIEnv* env, JSContext* cx, michael@0: JS::HandleObject object) { michael@0: return CreateInstance(env, new NativeJSContainer(cx, object)); michael@0: } michael@0: michael@0: static NativeJSContainer* FromInstance(JNIEnv* env, jobject instance) { michael@0: MOZ_ASSERT(instance); michael@0: michael@0: const jlong fieldValue = michael@0: env->GetLongField(instance, jContainerNativeObject); michael@0: NativeJSContainer* const nativeObject = michael@0: reinterpret_cast( michael@0: static_cast(fieldValue)); michael@0: if (!nativeObject) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/NullPointerException", michael@0: "Uninitialized NativeJSContainer"); michael@0: } michael@0: return nativeObject; michael@0: } michael@0: michael@0: static void DisposeInstance(JNIEnv* env, jobject instance) { michael@0: NativeJSContainer* const container = FromInstance(env, instance); michael@0: if (container) { michael@0: env->SetLongField(instance, jContainerNativeObject, michael@0: static_cast(reinterpret_cast(nullptr))); michael@0: delete container; michael@0: } michael@0: } michael@0: michael@0: static jobject CloneInstance(JNIEnv* env, jobject instance) { michael@0: NativeJSContainer* const container = FromInstance(env, instance); michael@0: if (!container || !container->EnsureObject(env)) { michael@0: return nullptr; michael@0: } michael@0: JSContext* const cx = container->mThreadContext; michael@0: JS::RootedObject object(cx, container->mJSObject); michael@0: MOZ_ASSERT(object); michael@0: michael@0: JSAutoStructuredCloneBuffer buffer; michael@0: if (!buffer.write(cx, JS::RootedValue(cx, JS::ObjectValue(*object)))) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/UnsupportedOperationException", michael@0: "Cannot serialize object"); michael@0: return nullptr; michael@0: } michael@0: return CreateInstance(env, new NativeJSContainer(cx, Move(buffer))); michael@0: } michael@0: michael@0: static jobject GetInstanceFromObject(JNIEnv* env, jobject object) { michael@0: MOZ_ASSERT(object); michael@0: michael@0: const jobject instance = env->GetObjectField(object, jObjectContainer); michael@0: MOZ_ASSERT(instance); michael@0: return instance; michael@0: } michael@0: michael@0: static JSContext* GetContextFromObject(JNIEnv* env, jobject object) { michael@0: MOZ_ASSERT(object); michael@0: AutoLocalJNIFrame frame(env, 1); michael@0: michael@0: NativeJSContainer* const container = michael@0: FromInstance(env, GetInstanceFromObject(env, object)); michael@0: if (!container) { michael@0: return nullptr; michael@0: } michael@0: return container->mThreadContext; michael@0: } michael@0: michael@0: static JSObject* GetObjectFromObject(JNIEnv* env, jobject object) { michael@0: MOZ_ASSERT(object); michael@0: AutoLocalJNIFrame frame(env, 1); michael@0: michael@0: NativeJSContainer* const container = michael@0: FromInstance(env, GetInstanceFromObject(env, object)); michael@0: if (!container || michael@0: !container->EnsureObject(env)) { // Do thread check michael@0: return nullptr; michael@0: } michael@0: const jint index = env->GetIntField(object, jObjectIndex); michael@0: if (index < 0) { michael@0: // -1 for index field means it's the root object of the container michael@0: return container->mJSObject; michael@0: } michael@0: return container->mRootedObjects[index]; michael@0: } michael@0: michael@0: static jobject CreateObjectInstance(JNIEnv* env, jobject object, michael@0: JS::HandleObject jsObject) { michael@0: MOZ_ASSERT(object); michael@0: MOZ_ASSERT(jsObject); michael@0: AutoLocalJNIFrame frame(env, 2); michael@0: michael@0: jobject instance = GetInstanceFromObject(env, object); michael@0: NativeJSContainer* const container = FromInstance(env, instance); michael@0: if (!container) { michael@0: return nullptr; michael@0: } michael@0: size_t newIndex = container->mRootedObjects.length(); michael@0: if (!container->mRootedObjects.append(jsObject)) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/OutOfMemoryError", "Cannot allocate object"); michael@0: return nullptr; michael@0: } michael@0: const jobject newObject = michael@0: env->NewObject(jNativeJSObject, jObjectConstructor, michael@0: instance, newIndex); michael@0: AndroidBridge::HandleUncaughtException(env); michael@0: MOZ_ASSERT(newObject); michael@0: return frame.Pop(newObject); michael@0: } michael@0: michael@0: // Make sure we have a JSObject and deserialize if necessary/possible michael@0: bool EnsureObject(JNIEnv* env) { michael@0: if (mJSObject) { michael@0: if (PR_GetCurrentThread() != mThread) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/IllegalThreadStateException", michael@0: "Using NativeJSObject off its thread"); michael@0: return false; michael@0: } michael@0: return true; michael@0: } michael@0: if (!SwitchContextToCurrentThread()) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/IllegalThreadStateException", michael@0: "Not available for this thread"); michael@0: return false; michael@0: } michael@0: michael@0: JS::RootedValue value(mThreadContext); michael@0: MOZ_ASSERT(mBuffer.data()); michael@0: MOZ_ALWAYS_TRUE(mBuffer.read(mThreadContext, &value)); michael@0: if (value.isObject()) { michael@0: mJSObject = &value.toObject(); michael@0: } michael@0: if (!mJSObject) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/IllegalStateException", "Cannot deserialize data"); michael@0: return false; michael@0: } michael@0: mBuffer.clear(); michael@0: return true; michael@0: } michael@0: michael@0: private: michael@0: static jclass jNativeJSContainer; michael@0: static jfieldID jContainerNativeObject; michael@0: static jmethodID jContainerConstructor; michael@0: static jclass jNativeJSObject; michael@0: static jfieldID jObjectContainer; michael@0: static jfieldID jObjectIndex; michael@0: static jmethodID jObjectConstructor; michael@0: michael@0: static jobject CreateInstance(JNIEnv* env, michael@0: NativeJSContainer* nativeObject) { michael@0: InitJNI(env); michael@0: const jobject newObject = michael@0: env->NewObject(jNativeJSContainer, jContainerConstructor, michael@0: static_cast( michael@0: reinterpret_cast(nativeObject))); michael@0: AndroidBridge::HandleUncaughtException(env); michael@0: MOZ_ASSERT(newObject); michael@0: return newObject; michael@0: } michael@0: michael@0: // Thread that the object is valid on michael@0: PRThread* mThread; michael@0: // Context that the object is valid in michael@0: JSContext* mThreadContext; michael@0: // Deserialized object, or nullptr if object is in serialized form michael@0: JS::Heap mJSObject; michael@0: // Serialized object, or empty if object is in deserialized form michael@0: JSAutoStructuredCloneBuffer mBuffer; michael@0: // Objects derived from mJSObject michael@0: Vector, 4> mRootedObjects; michael@0: michael@0: // Create a new container containing the given deserialized object michael@0: NativeJSContainer(JSContext* cx, JS::HandleObject object) michael@0: : mThread(PR_GetCurrentThread()) michael@0: , mThreadContext(cx) michael@0: , mJSObject(object) michael@0: { michael@0: } michael@0: michael@0: // Create a new container containing the given serialized object michael@0: NativeJSContainer(JSContext* cx, JSAutoStructuredCloneBuffer&& buffer) michael@0: : mThread(PR_GetCurrentThread()) michael@0: , mThreadContext(cx) michael@0: , mBuffer(Forward(buffer)) michael@0: { michael@0: } michael@0: michael@0: bool SwitchContextToCurrentThread() { michael@0: PRThread* const currentThread = PR_GetCurrentThread(); michael@0: if (currentThread == mThread) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: }; michael@0: michael@0: jclass NativeJSContainer::jNativeJSContainer = 0; michael@0: jfieldID NativeJSContainer::jContainerNativeObject = 0; michael@0: jmethodID NativeJSContainer::jContainerConstructor = 0; michael@0: jclass NativeJSContainer::jNativeJSObject = 0; michael@0: jfieldID NativeJSContainer::jObjectContainer = 0; michael@0: jfieldID NativeJSContainer::jObjectIndex = 0; michael@0: jmethodID NativeJSContainer::jObjectConstructor = 0; michael@0: michael@0: jobject michael@0: CreateNativeJSContainer(JNIEnv* env, JSContext* cx, JS::HandleObject object) michael@0: { michael@0: return NativeJSContainer::CreateInstance(env, cx, object); michael@0: } michael@0: michael@0: } // namespace widget michael@0: } // namespace mozilla michael@0: michael@0: namespace { michael@0: michael@0: class JSJNIString michael@0: { michael@0: public: michael@0: JSJNIString(JNIEnv* env, jstring str) michael@0: : mEnv(env) michael@0: , mJNIString(str) michael@0: , mJSString(!str ? nullptr : michael@0: reinterpret_cast(env->GetStringChars(str, nullptr))) michael@0: { michael@0: } michael@0: ~JSJNIString() { michael@0: if (mJNIString) { michael@0: mEnv->ReleaseStringChars(mJNIString, michael@0: reinterpret_cast(mJSString)); michael@0: } michael@0: } michael@0: operator const jschar*() const { michael@0: return mJSString; michael@0: } michael@0: size_t Length() const { michael@0: return static_cast(mEnv->GetStringLength(mJNIString)); michael@0: } michael@0: private: michael@0: JNIEnv* const mEnv; michael@0: const jstring mJNIString; michael@0: const jschar* const mJSString; michael@0: }; michael@0: michael@0: bool michael@0: CheckJSCall(JNIEnv* env, bool result) { michael@0: if (!result) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/UnsupportedOperationException", "JSAPI call failed"); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: bool michael@0: CheckJNIArgument(JNIEnv* env, jobject arg) { michael@0: if (!arg) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/IllegalArgumentException", "Null argument"); michael@0: return false; michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: bool michael@0: AppendJSON(const jschar* buf, uint32_t len, void* data) michael@0: { michael@0: static_cast(data)->Append(buf, len); michael@0: return true; michael@0: } michael@0: michael@0: template michael@0: struct PrimitiveProperty michael@0: { michael@0: typedef U Type; // JNI type michael@0: typedef V NativeType; // JSAPI type michael@0: michael@0: static bool InValue(JS::HandleValue val) { michael@0: return (static_cast(val).*IsMethod)(); michael@0: } michael@0: michael@0: static Type FromValue(JNIEnv* env, jobject instance, michael@0: JSContext* cx, JS::HandleValue val) { michael@0: return static_cast( michael@0: (static_cast(val).*ToMethod)()); michael@0: } michael@0: }; michael@0: michael@0: // Statically cast from bool to jboolean (unsigned char); it works michael@0: // since false and JNI_FALSE have the same value (0), and true and michael@0: // JNI_TRUE have the same value (1). michael@0: typedef PrimitiveProperty BooleanProperty; michael@0: michael@0: typedef PrimitiveProperty DoubleProperty; michael@0: michael@0: typedef PrimitiveProperty IntProperty; michael@0: michael@0: struct StringProperty michael@0: { michael@0: typedef jstring Type; michael@0: michael@0: static bool InValue(JS::HandleValue val) { michael@0: return val.isString(); michael@0: } michael@0: michael@0: static Type FromValue(JNIEnv* env, jobject instance, michael@0: JSContext* cx, JS::HandleValue val) { michael@0: JS::RootedString str(cx, val.toString()); michael@0: size_t strLen = 0; michael@0: const jschar* const strChars = michael@0: JS_GetStringCharsAndLength(cx, str, &strLen); michael@0: if (!CheckJSCall(env, !!strChars)) { michael@0: return nullptr; michael@0: } michael@0: jstring ret = env->NewString( michael@0: reinterpret_cast(strChars), strLen); michael@0: AndroidBridge::HandleUncaughtException(env); michael@0: MOZ_ASSERT(ret); michael@0: return ret; michael@0: } michael@0: }; michael@0: michael@0: struct ObjectProperty michael@0: { michael@0: typedef jobject Type; michael@0: michael@0: static bool InValue(JS::HandleValue val) { michael@0: return val.isObjectOrNull(); michael@0: } michael@0: michael@0: static Type FromValue(JNIEnv* env, jobject instance, michael@0: JSContext* cx, JS::HandleValue val) { michael@0: if (val.isNull()) { michael@0: return nullptr; michael@0: } michael@0: JS::RootedObject object(cx, &val.toObject()); michael@0: return NativeJSContainer::CreateObjectInstance(env, instance, object); michael@0: } michael@0: }; michael@0: michael@0: struct HasProperty michael@0: { michael@0: typedef jboolean Type; michael@0: michael@0: static bool InValue(JS::HandleValue val) { michael@0: return true; michael@0: } michael@0: michael@0: static Type FromValue(JNIEnv* env, jobject instance, michael@0: JSContext* cx, JS::HandleValue val) { michael@0: return JNI_TRUE; michael@0: } michael@0: }; michael@0: michael@0: MOZ_BEGIN_ENUM_CLASS(FallbackOption) michael@0: THROW, michael@0: RETURN, michael@0: MOZ_END_ENUM_CLASS(FallbackOption) michael@0: michael@0: template michael@0: typename Property::Type michael@0: GetProperty(JNIEnv* env, jobject instance, jstring name, michael@0: FallbackOption option = FallbackOption::THROW, michael@0: typename Property::Type fallback = typename Property::Type()) { michael@0: MOZ_ASSERT(env); michael@0: MOZ_ASSERT(instance); michael@0: michael@0: JSContext* const cx = michael@0: NativeJSContainer::GetContextFromObject(env, instance); michael@0: const JS::RootedObject object(cx, michael@0: NativeJSContainer::GetObjectFromObject(env, instance)); michael@0: const JSJNIString strName(env, name); michael@0: JS::RootedValue val(cx); michael@0: michael@0: if (!object || michael@0: !CheckJNIArgument(env, name) || michael@0: !CheckJSCall(env, michael@0: JS_GetUCProperty(cx, object, strName, strName.Length(), &val))) { michael@0: return typename Property::Type(); michael@0: } michael@0: if (val.isUndefined()) { michael@0: if (option == FallbackOption::THROW) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/IllegalArgumentException", michael@0: "Property does not exist"); michael@0: } michael@0: return fallback; michael@0: } michael@0: if (!Property::InValue(val)) { michael@0: AndroidBridge::ThrowException(env, michael@0: "java/lang/IllegalArgumentException", michael@0: "Property type mismatch"); michael@0: return fallback; michael@0: } michael@0: return Property::FromValue(env, instance, cx, val); michael@0: } michael@0: michael@0: } // namespace michael@0: michael@0: extern "C" { michael@0: michael@0: NS_EXPORT void JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSContainer_dispose(JNIEnv* env, jobject instance) michael@0: { michael@0: MOZ_ASSERT(env); michael@0: NativeJSContainer::DisposeInstance(env, instance); michael@0: } michael@0: michael@0: NS_EXPORT jobject JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSContainer_clone(JNIEnv* env, jobject instance) michael@0: { michael@0: MOZ_ASSERT(env); michael@0: return NativeJSContainer::CloneInstance(env, instance); michael@0: } michael@0: michael@0: NS_EXPORT jboolean JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_getBoolean(JNIEnv* env, jobject instance, jstring name) michael@0: { michael@0: return GetProperty(env, instance, name); michael@0: } michael@0: michael@0: NS_EXPORT jboolean JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_optBoolean(JNIEnv* env, jobject instance, michael@0: jstring name, jboolean fallback) michael@0: { michael@0: return GetProperty(env, instance, name, FallbackOption::RETURN, fallback); michael@0: } michael@0: michael@0: NS_EXPORT jdouble JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_getDouble(JNIEnv* env, jobject instance, jstring name) michael@0: { michael@0: return GetProperty(env, instance, name); michael@0: } michael@0: michael@0: NS_EXPORT jdouble JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_optDouble(JNIEnv* env, jobject instance, michael@0: jstring name, jdouble fallback) michael@0: { michael@0: return GetProperty(env, instance, name, FallbackOption::RETURN, fallback); michael@0: } michael@0: michael@0: NS_EXPORT jint JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_getInt(JNIEnv* env, jobject instance, jstring name) michael@0: { michael@0: return GetProperty(env, instance, name); michael@0: } michael@0: michael@0: NS_EXPORT jint JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_optInt(JNIEnv* env, jobject instance, michael@0: jstring name, jint fallback) michael@0: { michael@0: return GetProperty(env, instance, name, FallbackOption::RETURN, fallback); michael@0: } michael@0: michael@0: NS_EXPORT jobject JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_getObject(JNIEnv* env, jobject instance, jstring name) michael@0: { michael@0: return GetProperty(env, instance, name); michael@0: } michael@0: michael@0: NS_EXPORT jobject JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_optObject(JNIEnv* env, jobject instance, michael@0: jstring name, jobject fallback) michael@0: { michael@0: return GetProperty(env, instance, name, FallbackOption::RETURN, fallback); michael@0: } michael@0: michael@0: NS_EXPORT jstring JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_getString(JNIEnv* env, jobject instance, jstring name) michael@0: { michael@0: return GetProperty(env, instance, name); michael@0: } michael@0: michael@0: NS_EXPORT jstring JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_optString(JNIEnv* env, jobject instance, michael@0: jstring name, jstring fallback) michael@0: { michael@0: return GetProperty(env, instance, name, FallbackOption::RETURN, fallback); michael@0: } michael@0: michael@0: NS_EXPORT jboolean JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_has(JNIEnv* env, jobject instance, jstring name) michael@0: { michael@0: return GetProperty(env, instance, name, FallbackOption::RETURN, JNI_FALSE); michael@0: } michael@0: michael@0: NS_EXPORT jstring JNICALL michael@0: Java_org_mozilla_gecko_util_NativeJSObject_toString(JNIEnv* env, jobject instance) michael@0: { michael@0: MOZ_ASSERT(env); michael@0: MOZ_ASSERT(instance); michael@0: michael@0: JSContext* const cx = NativeJSContainer::GetContextFromObject(env, instance); michael@0: const JS::RootedObject object(cx, NativeJSContainer::GetObjectFromObject(env, instance)); michael@0: JS::RootedValue value(cx, JS::ObjectValue(*object)); michael@0: nsAutoString json; michael@0: michael@0: if (!object || michael@0: !CheckJSCall(env, michael@0: JS_Stringify(cx, &value, JS::NullPtr(), JS::NullHandleValue, AppendJSON, &json))) { michael@0: return nullptr; michael@0: } michael@0: jstring ret = env->NewString(reinterpret_cast(json.get()), json.Length()); michael@0: AndroidBridge::HandleUncaughtException(env); michael@0: MOZ_ASSERT(ret); michael@0: return ret; michael@0: } michael@0: michael@0: } // extern "C"