JNIってよくわからないですよね。
そういうのがよくわからなくてもアプリを作れちゃうのがフレームワークの力だと思いますが、やっぱりこの子も操れる方ができることが増えるだろうから、これからちょっとずつちゃんと勉強しようかしないか考えたり考えなかったりしてる今日この頃です。
ネイティブ側からJava側のメソッドを呼び出すのは今までやってたんだけど、Java側からネイティブ側を呼び出すのが(´・ω・`)
でも、やってみたら結構簡単だったのでちょろっとまとめておきます。
今回やること
どうせなので、cocos2d-xで「OK/Cancel」のコールバックができるダイアログボックスを実装してみようと思います。
実装としては、簡単にしたいので以下の感じにします。
- コールバック用のインターフェイスを作成する
- ダイアログ呼び出し時に、一時的にコールバッククラスをクラス変数に保持する
- ネイティブ連携をし、Android(iOS)側でダイアログを表示する
- 押されたボタンの値をネイティブ側に返す
※必然的にiOS側のobjective-cも扱いますが、そっちに関しては今回ソースコードの公開のみに留めます。
※iOSとAndroidで実装を分ける方法は下の記事を見てね。
cocos2d-xでiOSとAndroidの処理を分ける-たそがれブランチ
ちなみに、OKだけのダイアログは簡単に呼び出せる
余談になりますが、OKだけのダイアログは、もうcocos2d-x側で実装されています。
//ダイアログを表示する CCMessageBox("内容","タイトル");
簡単ですね。
ただ、このCCMessageBox()はOKのみの実装となり、Cancelはできません。
またコールバックもないので、本当にただメッセージを表示するだけの用途にしか使えません(それでもとても役に立ちますが)。
事前準備:インターフェイスやらヘッダファイルやらを作成
最初は別にJNIとは関係ありません。
というわけで、ソースコードと簡単な説明のみの記述とします。
#define ALERTBUTTON_OK 1 #define ALERTBUTTON_CANCEL 2 namespace MyExt{ //ダイアログ作成用の構造体 typedef struct _msgBox{ const char* title; //タイトル const char* message; //本文 const char* okButton; //OKボタンの文字 const char* cancelButton; //Cancelボタンの文字 }MessageBox; //ボタンの値 typedef enum _msgBoxResultType{ kMessageBoxResultOk = ALERTBUTTON_OK, kMessageBoxResultCancel = ALERTBUTTON_CANCEL, }messageBoxResultType; //コールバックのインターフェイス class MessageBoxCallback{ public: virtual void onResult(messageBoxResultType type) = 0; virtual ~MessageBoxCallback(){}; }; //ダイアログ表示のUtilクラス class Native{ public: static void openMessageDialog(const MessageBox &msgBox , MessageBoxCallback* callback); } }
さて、このヘッダファイルは「Android」「iOS」両方で使用するインターフェイスとなります。
コメントにだいたいの説明は書きましたが、MessageBoxの構造体に情報を格納し、Native::openMessageDialog()メソッド時にその構造体を引数として渡します。
またその第二引数にはMessageBoxCallbackインターフェイスを実装させたクラスを渡します。
JNIを使ってネイティブ側に実装する。
今回、JNIを使って以下の処理を行います。
- ネイティブ側からJava側の静的メソッドを呼び出す(ダイアログ呼び出し)
- Java側からネイティブ側のメソッドを呼び出す(コールバック)
ネイティブ側(C/C++側)からJavaメソッドの呼び出し方
cocos2d-xには便利なJniHelper.hが用意されてありますので、それを使っていきます。
このJniHelperのソースを見るとどんな感じでJNIとお付き合いすればいいかがなんとなくわかりますから一度は目を通してみた方がいいかも。
さっきのopenMessageDialogに実装していきます。
//cppファイル using namespace MyExt; //ここだけで使う定義 #define JNI_PACKAGE_CLASS_NAME "Javaのクラス名(パッケージ)" #define JNI_METHOD_NAME "Java側のメソッド名" //コールバッククラスを一時的に保持するクラス変数を用意しとくよ static MessageBoxCallback* sMessageBoxCallback = NULL; void Native::openMessageDialog(const MessageBox &msgBox , MessageBoxCallback* callback){ //(1) if(sMessageBoxCallback != callback){ if(sMessageBoxCallback != NULL){ delete sMessageBoxCallback; } sMessageBoxCallback = callback; } //(2) JniMethodInfo methodInfo; if(JniHelper::getStaticMethodInfo(methodInfo , JNI_PACKAGE_CLASS_NAME , JNI_METHOD_NAME , "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")){ //(3) jstring title = methodInfo.env->NewStringUTF(msgBox.title); jstring message = methodInfo.env->NewStringUTF(msgBox.message); jstring okButton = methodInfo.env->NewStringUTF(msgBox.okButton); jstring cancelButton = methodInfo.env->NewStringUTF(msgBox.cancelButton); //(4) methodInfo.env->CallStaticVoidMethod(methodInfo.classID , methodInfo.methodID , title , message , okButton , cancelButton); //(5) methodInfo.env->DeleteLocalRef(title); methodInfo.env->DeleteLocalRef(message); methodInfo.env->DeleteLocalRef(okButton); methodInfo.env->DeleteLocalRef(cancelButton); methodInfo.env->DeleteLocalRef(methodInfo.classID); } }
なんというか、jstringを配列にして渡したいなぁって思うんですけどやり方を見つけられませんでした(´・ω・`)
もしやり方知ってる方いたら直したいので教えてほしいです(´・ω・`)
やってること自体は簡単で、
(1)コールバッククラスが渡されたらセットし
(2)Java側のメソッド情報を取得し
(3)char*型のデータをJavaのString型に変換して
(4)Java側の戻り値がvoid型のメソッドを呼び出し
(5)メモリ解放などの後処理を行う
ということをしてます。
JniHelper::getStaticMethodInfoに関して
内部的には以下のことをおこなっています。
- JavaVM環境ポインタを受け取る
- そのポインタからFindClass()メソッドを呼び出し、クラスIDを取得する
- 更にそのポインタからGetMethodID()メソッドを呼び出し、メソッドIDを取得する
- それらを、cocos2d-xで用意した構造体(JniMethodInfo)に格納する
第一引数 JniMethodInfo構造体
第二引数 Java側のクラス名(パッケージから記述)
第三引数 メソッド名(そのまま)
第四引数 メソッドの引数と戻り値(型を指定)
たとえば
パッケージ名:jp.brbranch.lib
クラス名:MyActivity
の場合、以下のように記述します。
jp/brbranch/lib/MyActivity
メソッドの引数と戻り値の書き方が変
まあ変といったところで仕方がありませんが、こういう書き方にしないといけないみたいです。
「(引数1引数2…)戻り値」
Javaの型 | JNIの呪文 |
---|---|
void | V |
boolean | Z |
int | I |
long | J |
float | F |
double | D |
short | S |
char | C |
byte | B |
オブジェクト | L(パッケージ名/クラス名); Ljava/lang/String; |
また、それぞれの前方に「[」をつけると配列になるそうな。
書き方の例はこのサイトがわかりやすかったです。
JNIサンプル集 | 技術者のたまごブログ
(3)〜(5)に関しては、JNIで何か作ったら最後解放しないとダメよって憶えとく以外には特に記述することもないですね。
Java側に実装するよ
さて、では次はJava側にメソッドを実装します。
上に書いたパッケージとメソッドを使って記載していきます。
//jp.brbranch.lib.MyActivity内 public class MyActivity extends Cocos2dxActivity { //Activityを保持 private static MyActivity my; //Java->C側のメソッド呼び出し private static native void onMessageBoxResult(final int num); protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); my = this; } /* (省略) */ //C->java側のメソッド public static void showAlertDialog(final String title , final String message , final String okButton , final String cancelButton){ //UIスレッド上で呼び出してもらう my.runOnUiThread(new Runnable(){ @Override public void run(){ new AlertDialog.Builder(Cocos2dxActivity.getContext()) .setTitle(title) .setMessage(message) .setPositiveButton(okButton, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { Cocos2dxMyActivity.onMessageBoxResult(1); } }) .setNegativeButton(cancelButton, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Cocos2dxMyActivity.onMessageBoxResult(2); } }) .show(); } }); } }
6行目の「private static native void onMessageBoxResult(final int num);」がネイティブ側との連携用メソッドです。
Java側はこの記述だけでOK。
Java->C側のメソッドを実装する
そんなわけで、次にC側のメソッドを実装していきます。
Native::openMessageDialogを実装したファイルの下部にでも以下のコードを付け足します。
#ifdef __cplusplus extern "C"{ #endif JNIEXPORT void JNICALL Java_jp_brbranch_lib_MyActivity_onMessageBoxResult(JNIEnv* env , jobject thiz , jint jNum){ int num = jNum; if(sMessageBoxCallback != NULL){ sMessageBoxCallback->onResult(static_cast<messageBoxResultType>(num)); delete sMessageBoxCallback; sMessageBoxCallback = NULL; } return; } #ifdef __cplusplus } #endif
これが、C/C++側での書き方です。
ひとつづつ解説していくと、
#ifdef __cplusplus 〜 #endif部分
なんか、g++でコンパイルされる場合defineされるらしく
「extern “C”{ }で囲んだ部分はC言語だよ」
って教える為に必要らしいです。
変なメソッド名
またしても変なメソッド名ですが、これもこういう規則みたい。
ちなみに、もしクラス名やメソッド名の中にアンダーバーがあった場合「_1」でエスケープできるそうです。
(参考)
【Android】【NDK】JNIのパッケージ名、クラス名に_(アンダースコア)がある場合
呼び出してみる
では、実際に実装して呼び出してみます。
//以下のクラスを作成 class MyCallback : public MyExt::MessageBoxCallback{ public: void onResult(Cocos2dExt::messageBoxResultType type); }; //ソースファイル内 using namespace MyExt; void MyCallback::onResult(messageBoxResultType type){ switch(type){ case kMessageBoxResultCancel: CCMessageBox("キャンセルしないで下さいね。\nちょっとびっくりしました。","キャンセルが押された"); break; case kMessageBoxResultOk: CCMessageBox("OKするんですね","OKが押された"); break; } } //こんな感じで呼び出す Native::openMessageDialog((MessageBox){"テストタイトル","テスト本文","テストOK","キャンセル"} , new MyCallback());
すると、こんな感じになります。
無事実装できました\(^o^)/
ソースコードを公開しときます。
使い物になるかはわかりませんが、GitHubを使いたい子なので公開しておきます。
パブリックドメイン扱いにするので、好きに作り変えたり何でもして下さい。
https://github.com/brbranch/cocos2dx_dialog_sample
※バグがあってもごめんなさいしか言えないので、それでもよければですが…。
※ちょっと名前が変わってます。NativeではなくDialogという名前になってます。