Using iOS and Android Libraries in Cocos2D-X

Been working on some libs on both iOS and Android platforms for a long time, recently needed to support Cocos2d-x framework, I have no interest to write a whole new lib based on C++, nightmare for maintenance, So I worked on to integrate both libs to Cocos2d-x framework.

iOS

Imagine my lib name is libHumbleAssistant.a, with a header file HumbleAssistant.h.

1
2
3
@interface HumbleAssistant : NSObject
+ (void)doSomething:(NSString *)order;
@end

Lucky me, Objective C belongs to C family, which could integrate with C and C++ seamlessly.

At first, create a .h header file as the real interface for C++ code, and name it as humble_assistant_cpp.h.

1
2
3
4
class HumbleAssistantCpp {
public:
static void do_something(char *order);
};

Then, create its source humble_assistant_cpp.mm, .mm stands for Objective-C++, in which, people can hybrid programming with Objective C and C++.

1
2
3
4
5
6
#import "HumbleAssistant.h"
#import "humble_assistant_cpp.h"

void HumbleAssistantCpp::do_something(char *order) {
[HumbleAssistant doSomething:[NSString stringWithUTF8String:order]];
}

Now, the file structures looks like this

-mylibs
  -ios
    - HumbleAssistant.h
    - libHumbleAssistant.a
    - humble_assistant_cpp.h
    - humble_assistant_cpp.mm

In a Cocos2d-x project, add those files, and include the humble_assistant_cpp.h to any C++ code that is about to ask the humble assistant a favor. It's that simple.

Android

JNI is used to bind Cocos2d-x and Android Java interfaces.

In order to call a method in a jar lib from C++, JNI code must be written.

Imagine my android lib is humbleAssistant.jar, and has an interface as below.

1
2
3
4
5
6
package com.my.libs;
public final class HumbleAssistant {
public static void doSomething(String order) {
// do something
}
}

To expose this method to C++, first, create a header file humble_assistant_jni.h.

1
2
3
4
extern "C"
{
extern void j_do_something(char *order);
}

Then, its implementation humble_assistant_jni.cpp, a little complicated due to the JNI's obscure grammar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "platform/android/jni/JniHelper.h"
#include "humble_assistant_jni.h"

// java Class location
#define CLASS_NAME "com/my/libs/HumbleAssistant"
extern "C"
{
void j_do_something(char *order) {
cocos2d::JniMethodInfo methodInfo;
if (cocos2d::JniHelper::getStaticMethodInfo(methodInfo, CLASS_NAME,
"doSomething", "(Ljava/lang/String;)V")) {
jstring j_order = methodInfo.env->NewStringUTF(order);
methodInfo.env->CallStaticVoidMethod(methodInfo.classID,
methodInfo.methodID,
j_order);
methodInfo.env->DeleteLocalRef(j_order);
}
}
}

Cocos2d-x is a cross platform framework, our libs need to be that too, only one single interface is needed.

In iOS, a humble_assistant_cpp.h is created, which is the universal header file for both platforms. And humble_assistant_cpp.mm is the source file.

So, in Android, we have to make a new source file humble_assistant_cpp.cpp.

1
2
3
4
5
6
#include "humble_assistant_cpp.h"
#include "humble_assistant_jni.h"

void HumbleAssistant::do_something(char *order) {
j_do_something(order);
}

Now, the file structures looks like this

-mylibs
  -ios
    - HumbleAssistant.h
    - libHumbleAssistant.a
    - humble_assistant_cpp.h
    - humble_assistant_cpp.mm
  -android
    - humble_assistant_jni.h
    - humble_assistant_jni.cpp
    - humble_assistant_cpp.cpp

But it's not finished.

We have to change the makefile in "proj.android/jni/android.mk" to add source files and the header file.

1
2
3
4
5
6
7
# Add source files
LOCAL_SRC_FILES := ... \
../../sdk/android/humble_assistant_jni.cpp \
../../sdk/android/humble_assistant_cpp.cpp

# Include header file
LOCAL_C_INCLUDES := ... $(LOCAL_PATH)/../../mylibs/ios

And also, put the jar lib into "proj.android/libs" folder.

It's done. Looks much more complicated than iOS way, and could be even worse.

If the doSomething interface needs a HashMap parameter, JNI code will be full of stupid code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void j_do_something(char *order, std::map<char*, char*> attributes) {
cocos2d::JniMethodInfo methodInfo;
if (cocos2d::JniHelper::getStaticMethodInfo(methodInfo, CLASS_NAME,
"doSomething", "(Ljava/lang/String;Ljava/util/Map;)V")) {
jobject jattributes = 0;
jclass hashMapClass = methodInfo.env->FindClass("java/util/HashMap");
if (hashMapClass) {
jsize mapSize = attributes.size();
jmethodID init = methodInfo.env->GetMethodID(hashMapClass, "<init>", "(I)V");
jobject hashMap = methodInfo.env->NewObject(hashMapClass, init, mapSize);
jmethodID putMethod = methodInfo.env->GetMethodID(hashMapClass, "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

for (std::map<char*, char*>::const_iterator it = attributes.begin();
it != attributes.end(); ++it) {
jstring key = methodInfo.env->NewStringUTF(it->first);
jstring value = methodInfo.env->NewStringUTF(it->second);
methodInfo.env->CallObjectMethod(hashMap, putMethod, key, value);
methodInfo.env->DeleteLocalRef(value);
methodInfo.env->DeleteLocalRef(key);
}
jattributes = hashMap;
methodInfo.env->DeleteLocalRef(hashMapClass);
}

jstring jorder = methodInfo.env->NewStringUTF(order);
methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID,
jorder, jattributes);
if (jattributes)
methodInfo.env->DeleteLocalRef(jattributes);
methodInfo.env->DeleteLocalRef(jorder);
}
}

Feel lucky again that my lib only exposes static methods, with simple parameters.

Maybe that's why I don't like cross platform frameworks, and Android.