JNI Object management in cocos2d-x

29 Mar 2015

JNI, the gateway to heaven

Cross platform development is a bitch. You don't want to implement each feature per platform. Luckely, there is cocos2d-x, a cross platform game engine. However, it doesn't implement all the features each platform has to offer.

For example, if you want to use sensors on mobile devices (gyroscope, accelerometer), you have to make some custom code for each platform. For iOS, its easy: create a C++ header, implement it in Objective-C and you're done. Objective-C is a superset of c, and it's possible to mix C/C++ with Objective-C. (using the mm extension) For Android, it's a whole different story. Android uses Java, which is completely different than C/C++. There is no possible way to implement the body of a class in Java (as opposed to Objective-C). However, it's possible to use JNI, a glue between Java and C/C++. I consider it the "gateway to heaven". (and Java is not heaven)

Calling static Java methods

It's possible to call static methods in Java. First, define the method in Java:

Java

package org.example.jni;

public class GyroscopeBridge {
	static void enableGyroscope() {
		// some java code to enable the gyroscope
	}
}

The following piece of code will look for the static method "enableGyroscope()" in the class org.example.jni.GyroscopeBridge. When found, the helper will return true. I will use a cocos2d-x helper in the example, because it's easy to use.

C++

cocos2d::JniMethodInfo methodInfo;
if (cocos2d::JniHelper::getStaticMethodInfo(methodInfo, "org/example/jni/GyroscopeBridge", "enableGyroscope", "()V")) {
	methodInfo.env->CallStaticObjectMethod(methodInfo.classID, methodInfo.methodID)
	methodInfo.env->DeleteLocalRef(methodInfo.classID);
}

The CallStaticObjectMethod will actually call the java method. Now that's simple.

Calling C++ method from java

The example above is great, but it's one way traffic: from C++ to Java. It's possible to let the method return something (in the example, it returns void), but what if you want to call C++ periodically. The gyroscope will possibly generate values once per x time. It's possible call C++ methods from Java. You will have to define a "native" method in Java:

Java

public class GyroscopeBridge {

	//other methods

	private native void nativeCallback(final float someValue);
}

And the callback in C++

C++

extern "C" {
	void Java_org_example_jni_cpp_GyroscopeBridge_nativeCallback(JNIEnv* env, jobject thiz, jfloat someValue) {
    	     // someValue is a jfloat, but you can use it as a normal float
	}
}

So both methods are defined. Notice that the method is wrapped withextern "C". This makes sure the method can be linked and used. The following piece of code will show you how to call the native method in a cocos2d-x scope (running on the OpenGL thread)

Java

final float someValue = 10.0f;
Cocos2dxHelper.runOnGLThread(new Runnable() {
	@Override
	public void run() {
		nativeCallback(someValue);
	}
});

Calling non-static Java methods

The examples above showed how to call a static method. However, you can also call object methods. To do that, you'll need an instance of the java object. The instance can be made via JNI, or you could create a static factory method which will return the instance. I'll go for the last option. The following example will show you how to return an object instance:

Java

public class GyroscopeBridge {
     public static GyroscopeBridge create() {
		return new GyroscopeBridge();
	}
}

Then, call it from C++

C++

cocos2d::JniMethodInfo methodInfo;
if (cocos2d::JniHelper::getStaticMethodInfo(
	methodInfo,
	"org/example/jni/GyroscopeBridge",
	"create",
	"()Lorg/example/jni/GyroscopeBridge;")
) {
	jobject tmp = methodInfo.env->CallStaticObjectMethod(methodInfo.classID, methodInfo.methodID);
	jobject javaInstance = cocos2d::JniHelper::getEnv()->NewGlobalRef(tmp);
	methodInfo.env->DeleteLocalRef(methodInfo.classID);
}

Now, the variable javaInstance will point to the java object. We called NewGlobalRef to make sure the object doesn't get garbage collected. After you're done with the object, you have use the following code to release the object:

C++

cocos2d::JniHelper::getEnv()->DeleteGlobalRef(javaObject);

After this you can invoke every public method that's been defined in the java class. If you want to call a method, use the following piece of code:

C++

cocos2d::JniMethodInfo methodInfo;
if (cocos2d::JniHelper::getMethodInfo(methodInfo, "org/example/jni/GyroscopeBridge", "somePublicMethod", "()V")) {
	methodInfo.env->CallVoidMethod(javaInstance, methodInfo.methodID);
	methodInfo.env->DeleteLocalRef(methodInfo.classID);
}

Make sure javaInstance is available where you'll do this.

Multiple instances with JNI

So we discussed how to call methods on a Java object. But wat if you want to have multiple C++ objects and multiple Java objects. The use case for this could be when you have a bluetooth bridge and want to communicate with multiple devices. Because the callbacks from Java go through a normal method (without object scope), you don't know which C++ object it wants to perform the callback on. The callback does receive a jobject, a pointer to the Java object.

C++

void Java_org_cocos2dx_cpp_SomeBridge_nativeSomeMethod(JNIEnv* env, jobject thiz) {
	SomeBridge* bridge = (SomeBridge*)AndroidBridgeManager::getNativeObject(thiz);
}

The parameter thiz is the pointer to the Java object. The code calls an AndroidBridgeManager, which is a manager that contains a mapping between the Java and C++ objects, and returns the C++ object. The manager is not a default JNI class, but a simple object manager I developed:

C++

// AndroidBridgeManager.h
class AndroidBridgeManager {
public:
	static void add(void* object, const char* bridgeClass);
	static void remove(void* object);
	static void* getNativeObject(jobject obj);
	static jobject getJavaObject(void* obj);

protected:
	static std::map _instances;
};

// AndroidBridgeManager.cpp
std::map AndroidBridgeManager::_instances;

void AndroidBridgeManager::add(void* object, const char* bridgeClass) {
	if (!getJavaObject(object)) {
		char returnType[100];
		sprintf(returnType, "()L%s;", bridgeClass);

		cocos2d::JniMethodInfo methodInfo;
		if (cocos2d::JniHelper::getStaticMethodInfo(
			methodInfo,
			bridgeClass,
			"create",
			returnType)
		) {
			jobject tmp = methodInfo.env->CallStaticObjectMethod(methodInfo.classID, methodInfo.methodID);
			_instances[object] = cocos2d::JniHelper::getEnv()->NewGlobalRef(tmp);
			methodInfo.env->DeleteLocalRef(methodInfo.classID);
		}
	}
};
void AndroidBridgeManager::remove(void* object) {
	jobject javaObject = getJavaObject(object);
	if (javaObject) {
		_instances.erase(object);
		cocos2d::JniHelper::getEnv()->DeleteGlobalRef(javaObject);
	}
}

void* AndroidBridgeManager::getNativeObject(jobject obj) {
	JNIEnv* env = cocos2d::JniHelper::getEnv();
	for (auto instance : _instances) {
		if (env->IsSameObject(obj, instance.second)) {
			return instance.first;
		}
	}
	return nullptr;
};

jobject AndroidBridgeManager::getJavaObject(void* obj) {
	if (_instances.count(obj) > 0) {
		return _instances.at(obj);
	}
	return nullptr;
}

The manager always expects the Java object to have a create method, but you could also let it create objects. (see the JNI docs how to do this) The following pieces of code show how to use the manager:

C++

//adding an instance, this is the C++ object you want to map to the Java object, the string is the namespace of the Java object
AndroidBridgeManager::add(this, "org/example/jni/SomeBridge");

//removing an instance (in the destructor of the bridge class)
AndroidBridgeManager::remove(this);

//getting the Java object, so that you call object methods
jobject javaInstance = AndroidBridgeManager::getJavaObject(this);

//getting the C++ object in callbacks, thiz is a jobject
SomeBridge* bridge = (SomeBridge*)AndroidBridgeManager::getNativeObject(thiz);

Now that was a long post. I hope it will be useful for you and that it will save you some time when working with JNI.

back to blog
comments powered by Disqus