Smart JNI reference classes for Fennec

Historically, JNI code in Fennec has mostly used raw JNI types like jobject and jstring. However, the need to manage object lifetimes and the lack of strong typing make this practice error-prone. We do have some helper classes like AutoLocalJNIFrame, RefCountedJavaObject, WrappedJavaObject, and AutoGlobalWrappedJavaObject, but I've found them to be inconvenient to use.

Bug 1116868 is introducing several new “smart” classes to improve dealing with JNI references. As a start, instead of using raw jobject types, there are now different smart types for different usages:

Type When to use
Object::LocalRef To replace local jobject references; e.g. local variables
Object::GlobalRef To replace global jobject references; e.g. instance members
Object::Param To replace jobject function parameters

Note that these new classes are under the mozilla::jni namespace, so your code should include it first,

using namespace mozilla::jni; // then use Object::LocalRef
namespace jni = mozilla::jni; // then use jni::Object::LocalRef

These classes make managing lifetimes very easy. Previously, it was easy to make mistakes like this,

jobject obj = GetObject();
// use obj
obj = GetAnotherObject(); // oops! first obj was leaked

This mistake could happen to both local and global references. Now with the smart classes, these errors are eliminated,

Object::LocalRef obj = GetObject();
// use obj
obj = GetAnotherObject(); // first obj was automatically deleted

The new classes also make it easy to convert between local and global references,

// Object::GlobalRef mObj;
Object::LocalRef obj = mObj; // automatic conversion from global to local ref
obj = GetNewObject();
mObj = obj; // automatic conversion from local to global ref

For function parameters, you can pass either a LocalRef or a GlobalRef to a Param parameter,

void SetObject(Object::Param obj);
 
Object::LocalRef obj;
SetObject(obj); // pass in LocalRef
// Object::GlobalRef mObj;
SetObject(mObj); // pass in GlobalRef

For function return values, you should return a LocalRef per JNI convention,

Object::LocalRef GetObject() {
  Object::LocalRef ret = MakeObject();
  return ret;
}

Note that in the above example, only one local reference is ever created because of return value optimization performed by the compiler.

Both LocalRef and GlobalRef support move semantics, so the following example still creates only one local reference overall,

Object::LocalRef GetObject();
 
Object::LocalRef foo;
foo = GetObject();

It also means you can use LocalRef and GlobalRef with container classes like mozilla::Vector without worrying about performance impact.

LocalRef and GlobalRef can be used like pointers/Java references,

Object::GlobalRef ref = nullptr;
ref = GetRef();
if (ref) {
  Object::LocalRef ref2 = ref;
  // compare underlying objects, so a LocalRef
  // and a GlobalRef of the same object are equal
  MOZ_ASSERT(ref == ref2);
}

Other types

jobject is not the only wrapped type; other JNI types correspond to different smart types,

JNI type Use these smart types
jstring String::LocalRef String::GlobalRef String::Param
jclass ClassObject::LocalRef ClassObject::GlobalRef ClassObject::Param
jthrowable Throwable::LocalRef Throwable::GlobalRef Throwable::Param
jbooleanArray BooleanArray::LocalRef BooleanArray::GlobalRef BooleanArray::Param
jbyteArray ByteArray::LocalRef ByteArray::GlobalRef ByteArray::Param
jobjectArray ObjectArray::LocalRef ObjectArray::GlobalRef ObjectArray::Param

And if you use auto-generated classes from widget/android/GeneratedJNIWrappers.h, each class is being updated in bug 1116589 to have its own reference types. For example, mozilla::widget::ViewTransform::LocalRef is a local reference to a Java ViewTransform instance,

ViewTransform::LocalRef vt = ViewTransform::New();
float x = vt->OffsetX(); // get offsetX field
vt->OffsetX(1.0f); // set offsetX field

Using separate classes ensures better type-safety, because type-checking is done by the compiler,

Foo::LocalRef foo;
Bar::LocalRef bar = foo; // error: invalid conversion

However, the auto-generated classes often only accept Object::Param parameters or return Object::LocalRef values. Therefore, as a special case, any LocalRef or GlobalRef can automatically convert to Object::Param, and Object::LocalRef can automatically convert to any other LocalRef.

Object::LocalRef Foo(Object::Param foo);
 
Bar::LocalRef bar;
bar = Foo(bar); // bar is converted to Object::Param
// then return value is converted to Bar::LocalRef

String types

The String types have some custom behavior in addition to the standard behavior. A String::LocalRef or String::GlobalRef can automatically convert to a nsString or a nsCString,

String::LocalRef GetString();
 
nsString str = nsString(GetString());
nsCString cstr = nsCString(GetString());

Conversely, a nsAString or a nsACString can automatically convert to a String::Param,

void SetString(String::Param param);
 
nsString str;
SetString(str);
SetString(NS_LITERAL_CSTRING("text"));
 
String::LocalRef ref;
SetString(ref); // okay too

A String::LocalRef, String::GlobalRef, or String::Param also has a Length method,

size_t GetStringLength(String::Param param) {
  return param.Length();
}
 
MOZ_ASSERT(GetStringLength(NS_LITERAL_STRING("text")) == 4);

Adapting to raw JNI usage

The goal of these new classes is to make using raw JNI types obsolete. However, until all the refactoring is done, there are still cases where raw JNI values are needed, for example to call JNIEnv functions.

To get a raw JNI reference from any LocalRef or GlobalRef, call its Get method,

void Foo(jobject param);
 
Object::LocalRef obj;
Foo(obj.Get());

To turn a raw JNI reference into any Param, call the Foo::Ref::From method,

void Foo(Object::Param param);
 
jobject obj;
Foo(Object::Ref::From(obj));

To return a raw JNI reference from any LocalRef or GlobalRef, call its Forget method,

jobject GetRawRef() {
  Object::LocalRef ref = GetRef();
  return ref.Forget();
}

Use LocalRef::Adopt to manage a returned raw local reference,

jobject GetRawRef();
 
Object::LocalRef ref = Object::LocalRef::Adopt(GetRawRef());

LocalRef also has an Env method that returns a cached JNIEnv pointer,

Object::LocalRef ref = GetRef();
auto cls = ClassObject::LocalRef::Adopt(
  ref.Env()->GetObjectClass(ref.Get()));

Comments