Posted in Articles

By Miguel Ibero

Mobile Architect

@miguel_ibero

A C++ wrapper for the JNI library

When writing games for Android, one of the more tedious tasks is to write the code to communicate between the native Java interfaces and your main C++ game. In this article we will discuss an implementation used at Social Point that reduces the amount of C++ boilerplate code needed and prevents us from commiting typical mistakes.

Most of mobile games nowadays are written in C++ using the openGL library for drawing, since it has the advantage that the code can be shared by iOS and Android versions. On the other hand, communication with native SDKs that add aditional value to the game has become extremely important, for example to add social elements or operating system features. On iOS this is pretty straightforward to do this, as you can mix Objective-C with C++ changing the source file type to "Objective-C++ Source". On Android you have to use the Java Native Interface library, which uses classic C idioms and has a number of common pitfalls.

For example, this code will call a Java method passing a string as a parameter.

JNIEnv* env; 
if(java->GetEnv(&env, JNI_VERSION_1_4) == JNI_OK)
{
    jobject obj;
    jclass cls = env->GetObjectClass(env, obj);
    jmethodID mid = env->GetMethodID(env, cls, "MethodName", "(Ljava/lang/String;)V");
    jstring javaString = env->NewStringUTF(nativeString.c_str());
    env->CallVoidMethod(obj, mid, javaString);
    env->DeleteGlobalRef(cls);
}

This other example defines a native function that can be called from Java.

extern "C"
JNIEXPORT void JNICALL Java_ClassName_MethodName
  (JNIEnv *env, jobject obj, jstring javaString)
{
    //Get the native string from javaString
    const char *nativeString = env->GetStringUTFChars(javaString, 0);

    //Do something with the nativeString

    env->ReleaseStringUTFChars(javaString, nativeString);
}

The code snippets in this article are somewhat simplified to be easier to understand, to look at the complete code visit this url.

Resource acquisition is initialization

Cooked

First of all, when writing our C++ wrapper we though that it would be very useful to use the RAII idiom. This has two advantages, we have exactly one C++ object representing one Java object, and we won't have to remember to release all the dangling references. There are 3 different types of references in JNI, JniObject will hold global references to the class and the object, ensuring that they are not deleted by the garbage collector until the C++ object is destroyed.

class JniObject
{
private:
    jclass _class;
    jobject _instance;

public:

    JniObject(jclass class, jobject obj):
    _class(nullptr), _instance(nullptr)
    {
        init(class, obj);
    }

    JniObject(jobject obj):
    _class(nullptr), _instance(nullptr)
    {
        init(nullptr, obj);
    }

    JniObject(const JniObject& other) :
    _instance(nullptr), _class(nullptr)
    {
        init(other._instance, other._class);
    }

    JNIEnv* getEnvironment()
    {
        return Jni::get().getEnvironment();
    }

    void init(jobject objId, jclass classId)
    {
        JNIEnv* env = getEnvironment();
        if(!classId)
        {
            classId = env->GetObjectClass(objId);
        }
        if(classId)
        {
            _class = (jclass)env->NewGlobalRef(classId);
        }
        if(objId)
        {
            _instance = env->NewGlobalRef(objId);
        }
    }

    void clear()
    {
        JNIEnv* env = getEnvironment();
        if(_class)
        {
            env->DeleteGlobalRef(_class);
            _class = nullptr;
        }
        if(_instance)
        {
            env->DeleteGlobalRef(_instance);
            _instance = nullptr;
        }
    }

    JniObject::operator bool() const
    {
        return _instance != nullptr;
    }

    ~JNIObject()
    {
        clear();
    }
}

The init function creates global references that will retain class and instance until clear is called.

For this to work, we will need access to the JNIEnv*, so let's write a singleton for that.

class Jni
{
private:
    typedef std::map<std::string, jclass> ClassMap;
    JavaVM* _java;
    JNIEnv* _env;

    Jni():
    _java(nullptr), _env(nullptr)
    {
    }

    Jni(const Jni& other)
    {
        assert(false);
    }

public:

    static Jni& get()
    {
        static Jni jni;
        return jni;
    }

    void setJava(JavaVM* java)
    {
        _java = java;
    }

    JavaVM* getJava()
    {
        return _java;
    }

    JNIEnv* getEnvironment()
    {
        if(!_env)
        {
            assert(_java);
            _java->GetEnv((void**)&_env, JNI_VERSION_1_4);
            _java->AttachCurrentThread(&_env, nullptr);
        }
        assert(_env);
        return _env;
    }
};

This singleton should be called by your game's JNI_OnLoad method.

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    Jni::get().setJava(vm);
    return JNI_VERSION_1_4;
}

Automatic method signatures

One of the major hassles of writing jni code is having to write method signatures. A JNI method signature contains a keyword for each type of argument passed and for the return value. The possible values are:

  • void: V
  • jboolean: Z
  • jbyte: B
  • jchar: C
  • jshort: S
  • jlong: J
  • jint: I
  • jfloat: F
  • jobject: L + class name + ;
  • jstring: Ljava/lang/String;
  • jarray: [ + class name

These signatures can be automatically generated on compile time with the help of variadic template recursion. First we will define a template method to get a signature part for a given variable.

template<typename Type>
static std::string getSignaturePart(const Type& type);

We're passing the corresponding value as an argument for two reasons. First, C++ has automatic template type deduction for arguments, that means we won't need to explicitly write the template types, the compiler will do it for us. Second, we need the actual argument value for the case the argument is another JniObject since they can have different signature values depending on the class.

Next we write template specializations for each value.

template<>
std::string getSignaturePart<std::string>(const std::string& val)
{
   return "Ljava/lang/String;";
}

template<>
std::string getSignaturePart(const JniObject& val)
{
   return val.getSignature();
}

template<>
std::string getSignaturePart(const jboolean& val)
{
   return "Z";
}

/* similar methods for every type... */

template<>
std::string getSignaturePart(const jobject& val)
{
   return getSignaturePart(JniObject(val));
}

std::string getSignaturePart()
{
   return "V";
}

Now to generate the entire signature we will need to use recursive variadic templates.

template<typename Arg, typename... Args>
static void buildSignature(std::ostringstream& os, const Arg& arg, const Args&... args)
{
    os << getSignaturePart(arg);
    buildSignature(os, args...);
}

static void buildSignature(std::ostringstream& os)
{
}

template<typename Return, typename... Args>
static std::string createSignature(const Return& ret, const Args&... args)
{
    std::ostringstream os;
    os << "(";
    buildSignature(os, args...);
    os << ")" << getSignaturePart(ret);
    return os.str();
}

The buildSignature method calls itself each time with one less argument and adds each signature to the output stream. To get the signature for a jobject we need to know the class path, and to get it we need to call a Java method. This is done using the methods explained in the next sections, so bear with me.

std::string JniObject::getSignature() const
{
    return std::string("L")+getClassPath()+";";
}

std::string JniObject::getClassPath() const
{
    // this calls the Java code object.getClass().getName()
    return JniObject("java/lang/Class", _class).call("getName", std::string());
}

Converting objects between C++ and Java

Kissed

The next thing we need is a simple way to convert between C++ and Java objects. Jni has defined a union called jvalue that is used to pass arguments to Java functions. We use a template method to convert from C++ to jvalue.

template<typename Type>
static jvalue convertToJavaValue(const Type& obj);

The same way as before, we have to add specializations for each type.

template<>
jvalue JniObject::convertToJavaValue(const bool& obj)
{
    jvalue val;
    val.z = obj;
    return val;
}

/* similar methods for every type... */

template<>
jvalue JniObject::convertToJavaValue(const JniObject& obj)
{
    return convertToJavaValue(obj.getInstance());
}

template<>
jvalue JniObject::convertToJavaValue(const jstring& obj)
{
    jvalue val;
    val.l = obj;
    return val;
}

template<>
jvalue JniObject::convertToJavaValue(const std::string& obj)
{
    JNIEnv* env = getEnvironment();
    if (!env)
    {
        return jvalue();
    }
    return convertToJavaValue(env->NewStringUTF(obj.c_str()));
}

A very similar code is used to convert in the other direction.

template<typename Type>
static bool convertFromJavaObject(JNIEnv* env, jobject obj, Type& out);

With this we can build another recursive template to get an array of jvalue objects that can be used to call a Java method.

template<typename... Args>
static jvalue* createArguments(const Args&... args)
{
    jvalue* jargs = (jvalue*)malloc(siseof(jvalue)*sizeof...(Args));
    buildArguments(jargs, 0, args...);
    return jargs;
}

static jvalue* createArguments()
{
    return nullptr;
}

template<typename Arg, typename... Args>
static jvalue* buildArguments(jvalue* jargs, unsigned pos, const Arg& arg, const Args&... args)
{
    jargs[pos] = convertToJavaValue(arg);
    buildArguments(jargs, pos+1, args...);
}

static jvalue* buildArguments(jvalue* jargs, unsigned pos)
{
}

Calling a Java method

Now that we can create the signature an the arguments array, the next step is calling a Java method. JNI has a different function for each possible return type, so let's create another template function to unify them into one. Like before, we're passing the return value as a reference for automatic type deduction. For methods that return objects we use the convertFromJavaObject method we created earlier to convert from Java to C++.

template<typename Return>
void callJavaMethod(JNIEnv* env, jobject objId, jmethodID methodId, jvalue* args, Return& out);

template<>
void JniObject::callJavaMethod(JNIEnv* env, jobject objId, jmethodID methodId, jvalue* args, jboolean& out)
{
    out = env->CallBooleanMethodA(objId, methodId, args);
}

/* similar functions for each return type... */

template<>
void JniObject::callJavaMethod(JNIEnv* env, jobject objId, jmethodID methodId, jvalue* args, std::string& out)
{
    callJavaObjectMethod(env, objId, methodId, args, out);
}

template<>
void JniObject::callJavaMethod(JNIEnv* env, jobject objId, jmethodID methodId, jvalue* args, JniObject& out)
{
    callJavaObjectMethod(env, objId, methodId, args, out);
}

template<typename Return>
void callJavaObjectMethod(JNIEnv* env, jobject objId, jmethodID methodId, jvalue* args, Return& out)
{
    jobject jout = nullptr;
    callJavaMethod(env, objId, methodId, args, jout);
    out = convertFromJavaObject<Return>(jout);
}

The final method that calls Java looks something like this. Passing a default return value as a second argument is used for automatic template deduction and also as a default return value in case Java throws an exception.

template<typename Return, typename... Args>
Return call(const std::string& name, const Return& defRet, Args&&... args)
{
    JNIEnv* env = getEnvironment();
    jclass classId = getClass();
    jobject objId = getInstance();

    std::string signature(createSignature(defRet, args...));
    jmethodID methodId = env->GetMethodID(classId, name.c_str(), signature.c_str());
    if (!methodId || env->ExceptionCheck())
    {
        env->ExceptionClear();
        return defRet;
    }
    else
    {
        jvalue* jargs = createArguments(args...);
        Return result;
        callJavaMethod(env, objId, methodId, jargs, result);
        if (env->ExceptionCheck())
        {
            env->ExceptionClear();
            return defRet;
        }
        else
        {
            return result;
        }
    }
}

To call methods that return void we will need a different method callVoid since there is no way to pass a void parameter. Similar code can also be used to call static methods and fields.

Finding classes by name

Now we are working with jclass references, next step it to get the jclass from the class name. For this we create an addtitional method in the Jni class that will cache the correspondences.

typedef std::map<std::string, jclass> ClassMap;
ClassMap _classes;

jclass getClass(const std::string& classPath)
{
    ClassMap::const_iterator itr = _classes.find(classPath);
    if(itr != _classes.end())
    {
        return itr->second;
    }
    JNIEnv* env = getEnvironment();
    if(env)
    {
        jclass cls = (jclass)env->FindClass(classPath.c_str());
        if (cls)
        {

            cls = (jclass)env->NewGlobalRef(cls);
            _classes[classPath] = cls;
            return cls;
        }
        else
        {
            env->ExceptionClear();
        }
    }
    return nullptr;
}

Now we can add a JniObject constructor to accept the class name instead of the jclass reference.

JniObject(const std::string& classPath, jobject javaObj=nullptr):
_class(nullptr), _instance(nullptr)
{
    init(Jni::get().getClass(classPath), javaObj);
}

To Java and back

Jived

To find the entry points of our Java code from our C++ we use singletons. An easy way to define a singleton in Java is using an enum, since Java enums are just classes where the fields are implicitly static final and of the class type.

public enum NativeController
{
    instance;

    void doStuff()
    {
    }
}

Getting this singleton and calling doStuff is now very easy. Remember that the second parameter in staticField is the default value that will be returned if getting instance would fail.

JniObject native("NativeController").staticField("instance", JniObject());
native.call("doStuff");

Now normally when we do something in Java, we want to get something back. The simple way of doing this is with the return value of the Java method. Since we are converting the results its automatic to get a string.

public enum NativeController
{
    instance;

    String doStuff()
    {
        return "done";
    }
}
JniObject native("NativeController").staticField("instance", JniObject());
std::string result = native.call("doStuff", std::string());

Finally if we do something asyncronous in Java, like an HTTP request, we will get the response after the method returns. To get a notification in C++ we will need to define native methods. This is a feature of the JNI library.

public enum NativeController
{
    instance;

    void doStuff(long ptr)
    {
        doStuffFinish("done", ptr);
    }

    private static native void doStuffFinish(String result, long ptr);
}

In addition to the result string, we are passing a long. This is done in a similar fashion to the classic C technique of a function pointer with a void* argument. The void* argument can be used to pass any data to the function. In this case, if we would call multiple times doStuff we could pass different data, and the in doStuffFinish we could recover the data and know which response came from which doStuff call. We added automatic void* to jlong conversion to JniObject to simplify this pattern.

JniObject native("NativeController").staticField("instance", JniObject());
native.call("doStuff", new Data());
native.call("doStuff", new Data());
JNIEXPORT void JNICALL NativeController_doStuffFinish
(JNIEnv*, jobject, jstring jresult, jlong jptr)
{
    Data* data = (Data*)jptr;
    if(!data)
    {
        return;
    }
    std::string result;
    hydra::JniObject::convertFromJavaObject(jresult, result);
    // do C++ stuff
    delete data;
}

Conclusion

The complete code has additional features like container conversion from std::vector to Java array and from std::map to java.util.Map and the ability to create new Java objects. Let's hope that in the future google provides more support for C++ developers on their Android platform, like the excellent google game services C++ SDK. Check out the entire code for JniObject here.


Comments

comments powered by Disqus