Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "NativeJSContainer.h"
7 #include "AndroidBridge.h"
8 #include "mozilla/Vector.h"
9 #include "prthread.h"
11 using namespace mozilla;
12 using namespace mozilla::widget;
14 namespace mozilla {
15 namespace widget {
17 class NativeJSContainer
18 {
19 public:
20 static void InitJNI(JNIEnv* env) {
21 if (jNativeJSContainer) {
22 return;
23 }
24 jNativeJSContainer = AndroidBridge::GetClassGlobalRef(
25 env, "org/mozilla/gecko/util/NativeJSContainer");
26 MOZ_ASSERT(jNativeJSContainer);
27 jContainerNativeObject = AndroidBridge::GetFieldID(
28 env, jNativeJSContainer, "mNativeObject", "J");
29 MOZ_ASSERT(jContainerNativeObject);
30 jContainerConstructor = AndroidBridge::GetMethodID(
31 env, jNativeJSContainer, "<init>", "(J)V");
32 MOZ_ASSERT(jContainerConstructor);
34 jNativeJSObject = AndroidBridge::GetClassGlobalRef(
35 env, "org/mozilla/gecko/util/NativeJSObject");
36 MOZ_ASSERT(jNativeJSObject);
37 jObjectContainer = AndroidBridge::GetFieldID(
38 env, jNativeJSObject, "mContainer",
39 "Lorg/mozilla/gecko/util/NativeJSContainer;");
40 MOZ_ASSERT(jObjectContainer);
41 jObjectIndex = AndroidBridge::GetFieldID(
42 env, jNativeJSObject, "mObjectIndex", "I");
43 MOZ_ASSERT(jObjectIndex);
44 jObjectConstructor = AndroidBridge::GetMethodID(
45 env, jNativeJSObject, "<init>",
46 "(Lorg/mozilla/gecko/util/NativeJSContainer;I)V");
47 MOZ_ASSERT(jContainerConstructor);
48 }
50 static jobject CreateInstance(JNIEnv* env, JSContext* cx,
51 JS::HandleObject object) {
52 return CreateInstance(env, new NativeJSContainer(cx, object));
53 }
55 static NativeJSContainer* FromInstance(JNIEnv* env, jobject instance) {
56 MOZ_ASSERT(instance);
58 const jlong fieldValue =
59 env->GetLongField(instance, jContainerNativeObject);
60 NativeJSContainer* const nativeObject =
61 reinterpret_cast<NativeJSContainer* const>(
62 static_cast<uintptr_t>(fieldValue));
63 if (!nativeObject) {
64 AndroidBridge::ThrowException(env,
65 "java/lang/NullPointerException",
66 "Uninitialized NativeJSContainer");
67 }
68 return nativeObject;
69 }
71 static void DisposeInstance(JNIEnv* env, jobject instance) {
72 NativeJSContainer* const container = FromInstance(env, instance);
73 if (container) {
74 env->SetLongField(instance, jContainerNativeObject,
75 static_cast<jlong>(reinterpret_cast<uintptr_t>(nullptr)));
76 delete container;
77 }
78 }
80 static jobject CloneInstance(JNIEnv* env, jobject instance) {
81 NativeJSContainer* const container = FromInstance(env, instance);
82 if (!container || !container->EnsureObject(env)) {
83 return nullptr;
84 }
85 JSContext* const cx = container->mThreadContext;
86 JS::RootedObject object(cx, container->mJSObject);
87 MOZ_ASSERT(object);
89 JSAutoStructuredCloneBuffer buffer;
90 if (!buffer.write(cx, JS::RootedValue(cx, JS::ObjectValue(*object)))) {
91 AndroidBridge::ThrowException(env,
92 "java/lang/UnsupportedOperationException",
93 "Cannot serialize object");
94 return nullptr;
95 }
96 return CreateInstance(env, new NativeJSContainer(cx, Move(buffer)));
97 }
99 static jobject GetInstanceFromObject(JNIEnv* env, jobject object) {
100 MOZ_ASSERT(object);
102 const jobject instance = env->GetObjectField(object, jObjectContainer);
103 MOZ_ASSERT(instance);
104 return instance;
105 }
107 static JSContext* GetContextFromObject(JNIEnv* env, jobject object) {
108 MOZ_ASSERT(object);
109 AutoLocalJNIFrame frame(env, 1);
111 NativeJSContainer* const container =
112 FromInstance(env, GetInstanceFromObject(env, object));
113 if (!container) {
114 return nullptr;
115 }
116 return container->mThreadContext;
117 }
119 static JSObject* GetObjectFromObject(JNIEnv* env, jobject object) {
120 MOZ_ASSERT(object);
121 AutoLocalJNIFrame frame(env, 1);
123 NativeJSContainer* const container =
124 FromInstance(env, GetInstanceFromObject(env, object));
125 if (!container ||
126 !container->EnsureObject(env)) { // Do thread check
127 return nullptr;
128 }
129 const jint index = env->GetIntField(object, jObjectIndex);
130 if (index < 0) {
131 // -1 for index field means it's the root object of the container
132 return container->mJSObject;
133 }
134 return container->mRootedObjects[index];
135 }
137 static jobject CreateObjectInstance(JNIEnv* env, jobject object,
138 JS::HandleObject jsObject) {
139 MOZ_ASSERT(object);
140 MOZ_ASSERT(jsObject);
141 AutoLocalJNIFrame frame(env, 2);
143 jobject instance = GetInstanceFromObject(env, object);
144 NativeJSContainer* const container = FromInstance(env, instance);
145 if (!container) {
146 return nullptr;
147 }
148 size_t newIndex = container->mRootedObjects.length();
149 if (!container->mRootedObjects.append(jsObject)) {
150 AndroidBridge::ThrowException(env,
151 "java/lang/OutOfMemoryError", "Cannot allocate object");
152 return nullptr;
153 }
154 const jobject newObject =
155 env->NewObject(jNativeJSObject, jObjectConstructor,
156 instance, newIndex);
157 AndroidBridge::HandleUncaughtException(env);
158 MOZ_ASSERT(newObject);
159 return frame.Pop(newObject);
160 }
162 // Make sure we have a JSObject and deserialize if necessary/possible
163 bool EnsureObject(JNIEnv* env) {
164 if (mJSObject) {
165 if (PR_GetCurrentThread() != mThread) {
166 AndroidBridge::ThrowException(env,
167 "java/lang/IllegalThreadStateException",
168 "Using NativeJSObject off its thread");
169 return false;
170 }
171 return true;
172 }
173 if (!SwitchContextToCurrentThread()) {
174 AndroidBridge::ThrowException(env,
175 "java/lang/IllegalThreadStateException",
176 "Not available for this thread");
177 return false;
178 }
180 JS::RootedValue value(mThreadContext);
181 MOZ_ASSERT(mBuffer.data());
182 MOZ_ALWAYS_TRUE(mBuffer.read(mThreadContext, &value));
183 if (value.isObject()) {
184 mJSObject = &value.toObject();
185 }
186 if (!mJSObject) {
187 AndroidBridge::ThrowException(env,
188 "java/lang/IllegalStateException", "Cannot deserialize data");
189 return false;
190 }
191 mBuffer.clear();
192 return true;
193 }
195 private:
196 static jclass jNativeJSContainer;
197 static jfieldID jContainerNativeObject;
198 static jmethodID jContainerConstructor;
199 static jclass jNativeJSObject;
200 static jfieldID jObjectContainer;
201 static jfieldID jObjectIndex;
202 static jmethodID jObjectConstructor;
204 static jobject CreateInstance(JNIEnv* env,
205 NativeJSContainer* nativeObject) {
206 InitJNI(env);
207 const jobject newObject =
208 env->NewObject(jNativeJSContainer, jContainerConstructor,
209 static_cast<jlong>(
210 reinterpret_cast<uintptr_t>(nativeObject)));
211 AndroidBridge::HandleUncaughtException(env);
212 MOZ_ASSERT(newObject);
213 return newObject;
214 }
216 // Thread that the object is valid on
217 PRThread* mThread;
218 // Context that the object is valid in
219 JSContext* mThreadContext;
220 // Deserialized object, or nullptr if object is in serialized form
221 JS::Heap<JSObject*> mJSObject;
222 // Serialized object, or empty if object is in deserialized form
223 JSAutoStructuredCloneBuffer mBuffer;
224 // Objects derived from mJSObject
225 Vector<JS::Heap<JSObject*>, 4> mRootedObjects;
227 // Create a new container containing the given deserialized object
228 NativeJSContainer(JSContext* cx, JS::HandleObject object)
229 : mThread(PR_GetCurrentThread())
230 , mThreadContext(cx)
231 , mJSObject(object)
232 {
233 }
235 // Create a new container containing the given serialized object
236 NativeJSContainer(JSContext* cx, JSAutoStructuredCloneBuffer&& buffer)
237 : mThread(PR_GetCurrentThread())
238 , mThreadContext(cx)
239 , mBuffer(Forward<JSAutoStructuredCloneBuffer>(buffer))
240 {
241 }
243 bool SwitchContextToCurrentThread() {
244 PRThread* const currentThread = PR_GetCurrentThread();
245 if (currentThread == mThread) {
246 return true;
247 }
248 return false;
249 }
250 };
252 jclass NativeJSContainer::jNativeJSContainer = 0;
253 jfieldID NativeJSContainer::jContainerNativeObject = 0;
254 jmethodID NativeJSContainer::jContainerConstructor = 0;
255 jclass NativeJSContainer::jNativeJSObject = 0;
256 jfieldID NativeJSContainer::jObjectContainer = 0;
257 jfieldID NativeJSContainer::jObjectIndex = 0;
258 jmethodID NativeJSContainer::jObjectConstructor = 0;
260 jobject
261 CreateNativeJSContainer(JNIEnv* env, JSContext* cx, JS::HandleObject object)
262 {
263 return NativeJSContainer::CreateInstance(env, cx, object);
264 }
266 } // namespace widget
267 } // namespace mozilla
269 namespace {
271 class JSJNIString
272 {
273 public:
274 JSJNIString(JNIEnv* env, jstring str)
275 : mEnv(env)
276 , mJNIString(str)
277 , mJSString(!str ? nullptr :
278 reinterpret_cast<const jschar*>(env->GetStringChars(str, nullptr)))
279 {
280 }
281 ~JSJNIString() {
282 if (mJNIString) {
283 mEnv->ReleaseStringChars(mJNIString,
284 reinterpret_cast<const jchar*>(mJSString));
285 }
286 }
287 operator const jschar*() const {
288 return mJSString;
289 }
290 size_t Length() const {
291 return static_cast<size_t>(mEnv->GetStringLength(mJNIString));
292 }
293 private:
294 JNIEnv* const mEnv;
295 const jstring mJNIString;
296 const jschar* const mJSString;
297 };
299 bool
300 CheckJSCall(JNIEnv* env, bool result) {
301 if (!result) {
302 AndroidBridge::ThrowException(env,
303 "java/lang/UnsupportedOperationException", "JSAPI call failed");
304 }
305 return result;
306 }
308 bool
309 CheckJNIArgument(JNIEnv* env, jobject arg) {
310 if (!arg) {
311 AndroidBridge::ThrowException(env,
312 "java/lang/IllegalArgumentException", "Null argument");
313 return false;
314 }
315 return true;
316 }
318 bool
319 AppendJSON(const jschar* buf, uint32_t len, void* data)
320 {
321 static_cast<nsAutoString*>(data)->Append(buf, len);
322 return true;
323 }
325 template <typename U, typename V,
326 bool (JS::Value::*IsMethod)() const,
327 V (JS::Value::*ToMethod)() const>
328 struct PrimitiveProperty
329 {
330 typedef U Type; // JNI type
331 typedef V NativeType; // JSAPI type
333 static bool InValue(JS::HandleValue val) {
334 return (static_cast<const JS::Value&>(val).*IsMethod)();
335 }
337 static Type FromValue(JNIEnv* env, jobject instance,
338 JSContext* cx, JS::HandleValue val) {
339 return static_cast<Type>(
340 (static_cast<const JS::Value&>(val).*ToMethod)());
341 }
342 };
344 // Statically cast from bool to jboolean (unsigned char); it works
345 // since false and JNI_FALSE have the same value (0), and true and
346 // JNI_TRUE have the same value (1).
347 typedef PrimitiveProperty<jboolean, bool,
348 &JS::Value::isBoolean, &JS::Value::toBoolean> BooleanProperty;
350 typedef PrimitiveProperty<jdouble, double,
351 &JS::Value::isNumber, &JS::Value::toNumber> DoubleProperty;
353 typedef PrimitiveProperty<jint, int32_t,
354 &JS::Value::isInt32, &JS::Value::toInt32> IntProperty;
356 struct StringProperty
357 {
358 typedef jstring Type;
360 static bool InValue(JS::HandleValue val) {
361 return val.isString();
362 }
364 static Type FromValue(JNIEnv* env, jobject instance,
365 JSContext* cx, JS::HandleValue val) {
366 JS::RootedString str(cx, val.toString());
367 size_t strLen = 0;
368 const jschar* const strChars =
369 JS_GetStringCharsAndLength(cx, str, &strLen);
370 if (!CheckJSCall(env, !!strChars)) {
371 return nullptr;
372 }
373 jstring ret = env->NewString(
374 reinterpret_cast<const jchar*>(strChars), strLen);
375 AndroidBridge::HandleUncaughtException(env);
376 MOZ_ASSERT(ret);
377 return ret;
378 }
379 };
381 struct ObjectProperty
382 {
383 typedef jobject Type;
385 static bool InValue(JS::HandleValue val) {
386 return val.isObjectOrNull();
387 }
389 static Type FromValue(JNIEnv* env, jobject instance,
390 JSContext* cx, JS::HandleValue val) {
391 if (val.isNull()) {
392 return nullptr;
393 }
394 JS::RootedObject object(cx, &val.toObject());
395 return NativeJSContainer::CreateObjectInstance(env, instance, object);
396 }
397 };
399 struct HasProperty
400 {
401 typedef jboolean Type;
403 static bool InValue(JS::HandleValue val) {
404 return true;
405 }
407 static Type FromValue(JNIEnv* env, jobject instance,
408 JSContext* cx, JS::HandleValue val) {
409 return JNI_TRUE;
410 }
411 };
413 MOZ_BEGIN_ENUM_CLASS(FallbackOption)
414 THROW,
415 RETURN,
416 MOZ_END_ENUM_CLASS(FallbackOption)
418 template <class Property>
419 typename Property::Type
420 GetProperty(JNIEnv* env, jobject instance, jstring name,
421 FallbackOption option = FallbackOption::THROW,
422 typename Property::Type fallback = typename Property::Type()) {
423 MOZ_ASSERT(env);
424 MOZ_ASSERT(instance);
426 JSContext* const cx =
427 NativeJSContainer::GetContextFromObject(env, instance);
428 const JS::RootedObject object(cx,
429 NativeJSContainer::GetObjectFromObject(env, instance));
430 const JSJNIString strName(env, name);
431 JS::RootedValue val(cx);
433 if (!object ||
434 !CheckJNIArgument(env, name) ||
435 !CheckJSCall(env,
436 JS_GetUCProperty(cx, object, strName, strName.Length(), &val))) {
437 return typename Property::Type();
438 }
439 if (val.isUndefined()) {
440 if (option == FallbackOption::THROW) {
441 AndroidBridge::ThrowException(env,
442 "java/lang/IllegalArgumentException",
443 "Property does not exist");
444 }
445 return fallback;
446 }
447 if (!Property::InValue(val)) {
448 AndroidBridge::ThrowException(env,
449 "java/lang/IllegalArgumentException",
450 "Property type mismatch");
451 return fallback;
452 }
453 return Property::FromValue(env, instance, cx, val);
454 }
456 } // namespace
458 extern "C" {
460 NS_EXPORT void JNICALL
461 Java_org_mozilla_gecko_util_NativeJSContainer_dispose(JNIEnv* env, jobject instance)
462 {
463 MOZ_ASSERT(env);
464 NativeJSContainer::DisposeInstance(env, instance);
465 }
467 NS_EXPORT jobject JNICALL
468 Java_org_mozilla_gecko_util_NativeJSContainer_clone(JNIEnv* env, jobject instance)
469 {
470 MOZ_ASSERT(env);
471 return NativeJSContainer::CloneInstance(env, instance);
472 }
474 NS_EXPORT jboolean JNICALL
475 Java_org_mozilla_gecko_util_NativeJSObject_getBoolean(JNIEnv* env, jobject instance, jstring name)
476 {
477 return GetProperty<BooleanProperty>(env, instance, name);
478 }
480 NS_EXPORT jboolean JNICALL
481 Java_org_mozilla_gecko_util_NativeJSObject_optBoolean(JNIEnv* env, jobject instance,
482 jstring name, jboolean fallback)
483 {
484 return GetProperty<BooleanProperty>(env, instance, name, FallbackOption::RETURN, fallback);
485 }
487 NS_EXPORT jdouble JNICALL
488 Java_org_mozilla_gecko_util_NativeJSObject_getDouble(JNIEnv* env, jobject instance, jstring name)
489 {
490 return GetProperty<DoubleProperty>(env, instance, name);
491 }
493 NS_EXPORT jdouble JNICALL
494 Java_org_mozilla_gecko_util_NativeJSObject_optDouble(JNIEnv* env, jobject instance,
495 jstring name, jdouble fallback)
496 {
497 return GetProperty<DoubleProperty>(env, instance, name, FallbackOption::RETURN, fallback);
498 }
500 NS_EXPORT jint JNICALL
501 Java_org_mozilla_gecko_util_NativeJSObject_getInt(JNIEnv* env, jobject instance, jstring name)
502 {
503 return GetProperty<IntProperty>(env, instance, name);
504 }
506 NS_EXPORT jint JNICALL
507 Java_org_mozilla_gecko_util_NativeJSObject_optInt(JNIEnv* env, jobject instance,
508 jstring name, jint fallback)
509 {
510 return GetProperty<IntProperty>(env, instance, name, FallbackOption::RETURN, fallback);
511 }
513 NS_EXPORT jobject JNICALL
514 Java_org_mozilla_gecko_util_NativeJSObject_getObject(JNIEnv* env, jobject instance, jstring name)
515 {
516 return GetProperty<ObjectProperty>(env, instance, name);
517 }
519 NS_EXPORT jobject JNICALL
520 Java_org_mozilla_gecko_util_NativeJSObject_optObject(JNIEnv* env, jobject instance,
521 jstring name, jobject fallback)
522 {
523 return GetProperty<ObjectProperty>(env, instance, name, FallbackOption::RETURN, fallback);
524 }
526 NS_EXPORT jstring JNICALL
527 Java_org_mozilla_gecko_util_NativeJSObject_getString(JNIEnv* env, jobject instance, jstring name)
528 {
529 return GetProperty<StringProperty>(env, instance, name);
530 }
532 NS_EXPORT jstring JNICALL
533 Java_org_mozilla_gecko_util_NativeJSObject_optString(JNIEnv* env, jobject instance,
534 jstring name, jstring fallback)
535 {
536 return GetProperty<StringProperty>(env, instance, name, FallbackOption::RETURN, fallback);
537 }
539 NS_EXPORT jboolean JNICALL
540 Java_org_mozilla_gecko_util_NativeJSObject_has(JNIEnv* env, jobject instance, jstring name)
541 {
542 return GetProperty<HasProperty>(env, instance, name, FallbackOption::RETURN, JNI_FALSE);
543 }
545 NS_EXPORT jstring JNICALL
546 Java_org_mozilla_gecko_util_NativeJSObject_toString(JNIEnv* env, jobject instance)
547 {
548 MOZ_ASSERT(env);
549 MOZ_ASSERT(instance);
551 JSContext* const cx = NativeJSContainer::GetContextFromObject(env, instance);
552 const JS::RootedObject object(cx, NativeJSContainer::GetObjectFromObject(env, instance));
553 JS::RootedValue value(cx, JS::ObjectValue(*object));
554 nsAutoString json;
556 if (!object ||
557 !CheckJSCall(env,
558 JS_Stringify(cx, &value, JS::NullPtr(), JS::NullHandleValue, AppendJSON, &json))) {
559 return nullptr;
560 }
561 jstring ret = env->NewString(reinterpret_cast<const jchar*>(json.get()), json.Length());
562 AndroidBridge::HandleUncaughtException(env);
563 MOZ_ASSERT(ret);
564 return ret;
565 }
567 } // extern "C"