C++からC++らしくJNIを呼んでみる
JNI(Java Native Interface)はJavaとCの連携技術です。Javaアプリケーションの一部ロジックをCで書いたり、さらにその中からJavaのメソッドを呼び出したりする事で、アプリケーションの実行環境が制限される代わりに高速化、あるいは非Javaプラットフォームのコードとの共通化が行なえる、という寸法です。
JavaからC++の関数を呼び出す場合
JNIを用いてJavaからC++の関数を呼ぶ場合、こんな感じになります:
// Java package com.cflat; public class Hoge { static { System.loadLibrary("hoge"); } public static native boolean printNative(String s, int x); public static boolean printJava(String s, int x) { System.out.println(s + " " + x); return true; } }
// C++ extern "C" { JNIEXPORT jboolean JNICALL Java_com_cflat_Hoge_print(JNIEnv *env, jstring s, jint x); }
C命名規則で、Java\_<i>packagename</i>\_<i>classname</i>\_<i>methodname</i>()
なる関数を作ってやればOK……C++的には嫌な感じの名前ですが、名前マングリングが実装依存になっているC++では、他の言語から関数を呼んで貰おうと思うとextern "C"
が必須ですので、まあ仕方ありません。せめてもう少し短い名前をだと言うのであればJNIではなくJNA(Java Native Access)を使うという方法が残されています。
C++からJavaのメソッドを呼び出す場合。
次に、今度はC++からJavaを呼び出してみます。
まず、C++側ではJavaVM
のインスタンスに対してGetEnv()
を呼び出す事でJNIEnv*
を取得しなければならないのですが、このJNIEnv*
の値はスレッドごとに変わってしまいます。
しかも、C++側で作られたスレッドには(当然ながら)最初はJNIEnv*
が作られていませんので、まずはAttachCurrentThread()
しなければなりません。もちろん、スレッド破棄時にはDetachCurrentThread()
が必要です。
ただ、その辺は今回したい話とは少しずれて来ますので省略して、既にJNIEnv*
を得たところから始めたいと思います。
そして……実際にやってみると、次のようにイケてない部分ばかりの代物になってしまいます:
// C++ bool printJava(JNIEnv *env, const std::string &s, int32_t x) { jclass klass = env->FindClass("com/cflat/Hoge"); jmethodID method = env->GetMethodID(klass, "printJava", "(Ljava/jang/String;I)Z"); // ←イケてないその1 jstring jstr = env->NewStringUTF(s.c_str()); jboolean result = env->CallStaticBooleanMethod(klass, method, jstr, x); // ←イケてないその2 env->DeleteLocalRef(jstr); // ←イケてないその3 env->DeleteLocalRef(klass); // ←┘ return (result != JNI_FALSE); }
JNIはC++でも使えるとは言っても、C用のインターフェースの最低限の部分を#if defined(__cplusplus)
〜#endif
で囲っただけのライブラリですから、仕方ない事ではあります。
が、もっとC++らしくするのであれば、せめてこんな形にしたいところでしょう:
// C++ bool printJava(jni::vm &vm, const std::string &s, int32_t x) // jni::vmはJavaVMのラッパー(自動的に適切なJNIEnv*を取得する) { jni::class_info klass = vm.get_class("com/cflat/Hoge"); // jni::class_infoはjclassのラッパー auto method = klass.get_static_method<bool(std::string, int32_t)>("printJava"); // シグネチャーは型指定で! return method(s, x); // 呼び出しも、get_static_method()に渡した型の通りにoperator()で! // jclassやらjstringやらはRAIIで勝手に破棄されるので、わざわざDeleteLocalRef()しなくてOK! }
JNIの型のC++らしいラッパーであるjni::vm
やjni::class_info
は、ある程度C++に慣れれば誰でも作れるはずですが(コピーコンストラクタと代入演算子でNewLocalRef()
、デストラクタと代入演算子でDeleteLocalRef()
を忘れないように!)、シグネチャーを型指定で済ますように作るには、かなりC++のテンプレートに慣れていないと難しいのではないでしょうか?
ものは試し、ちょっと作ってみましょう。
JNIラッパーの構成
上記のコードを見て気付いた方もいると思いますが、シグネチャーの型指定はJNIの型であるjstring
やjint
ではなく、C++の型であるstd::string
、int32_t
を使っています。何故なら、メソッド呼び出し処理の内部で自動変換してくれる方が扱いやすいので。
場合によっては、同じjstring
のインスタンスを複数のメソッド呼び出しで共有したい、という場合もあるかもしれませんが(当然、その方が余計なオーバーヘッドが不要になります)、今回は気にしないものとします。必要であればjstring
のラッパー←→jstring
の変換クラスを追加してやればよいだけでしょう。
これらを鑑みると、JNIのC++らしいラッパーの実装は、次のような部分から成るようにできるでしょう:
なお、jstring
(あるいは他のリファレンス型)をRAIIでいい感じに構築/破棄してくれる部分も1.の中に含まれるものとします。
C++←→JNI型
まずは、C++型を指定した時に対応するJNI型を求めるためのメタ関数が必要になります。 この部分をどう設計するかは悩ましいのですが、試しに、次のようにしてみましょう:
- 1/2/4/8バイトの符号付き整数型は、それぞれ
jbyte
/jshort
/jint
/jlong
に変換する((本当は「int8/16/32/64_t
のみ」としたかったところですが、int
で指定できないのもなんか嫌なので……)) - 4/8バイトの浮動小数型は、それぞれ
jfloat
/jdouble
に変換する char16_t
とjchar
はjchar
に変換する((sizeof(wchar_t) == 2
の場合、wchar_t
もjchar
にしていいかも))bool
とjboolean
はjboolean
に変換するtemplate <typename TNative> struct object_converter
をユーザーが明示的に特殊化したクラスはjobject
または互換性を持つ型に変換する
4.まではプリミティブ、5.はクラス型ですね。
……で、この部分を解説しようと思ったのですが、詳細に解説すると大変な事になるので、いろいろと上手く特殊化済みの次のクラスがあるものとします(コピー、ムーブ、デストラクタは省略してます)。
template <typename TNative> struct var { public: typedef TNative native_type; typedef 対応するJNI型 jni_type; var(JNIEnv *, native_type); var(JNIEnv *, jni_type); operator native_type() const; operator jni_type() const; private: jni_type m_type; };
このクラスの特殊ケースとして、インスタンス化のできない次のものを用意します:
template <> struct var<void> { typedef void native_type; typedef void jni_type; var() = delete; }; template <class TNativeRet, class ... TNativeParams> struct var<TNativeRet(TNativeParams...)> { typedef TNativeRet native_type(TNativeParams...); typedef typename var<TNativeRet>::jni_type jni_type(typename var<TNativeParams>::jni_type...); var() = delete; };
……これで、関数タイプのシグネチャーに対しても対応するJNIの関数タイプを求められるようになりました。
JNI型←→シグネチャー文字列
JNIのシグネチャー文字列には、大きく分けて4つのパターンがあります。
これを、できればコンパイル時にjint
と渡せば"I"、jstring
と渡せば"Ljava/lang/String;"、void(jstring, jint)
と渡せば"(Ljava/lang/String;I)V"のように文字列を構築できれば良いのですが、問題は2.のクラス名パターン。
というのも、std::string
は基本的にconstexpr
に対応していませんし、const char*
では文字列結合が困難だからです。テンプレートを使うとしても、テンプレートで文字列リテラルを与えられないので、コンパイル時に文字列構築をする事ができません(「文字列変数のアドレス」を指定する事だけは可能ですが、指示先をコンパイル時に参照できないので意味なし)。
こんな時、☆C++11テクニック☆ 配列を配列で初期化する方法+α - 株式会社CFlatの明後日スタイルのブログで紹介した方法を使って、constexpr
な文字列結合を行なってやればよいでしょう。
これだけわかっていれば、後は面倒なだけで大した事ないので割愛します。
とりあえずsignature<T>::get()
あたりで取れてくるようにすればいいでしょう。
JNI型←→CallXXXMethod
ここまで来れば、後は完全に作業ですね。まず、次のようなクラスを作り……
template <typename TJniRet> struct method_caller_info { static_assert(std::is_convertible<TJniRet, jobject>::value, "TJniRet is not a primitive type, void, nor jobject"); static auto static_method_caller() -> decltype(&JNIEnv::CallStaticObjectMethod) { return &JNIEnv::CallStaticObjectMethod; } static auto instance_method_caller() -> decltype(&JNIEnv::CallObjectMethod) { return &JNIEnv::CallObjectMethod; } static auto nonvirtual_method_caller() -> decltype(&JNIEnv::CallNonvirtualObjectMethod) { return &JNIEnv::CallNonvirtualObjectMethod; } }; template <typename TJniRet> struct method_caller : method_caller_info<TJniRet> { typedef TJniRet result_type; template <typename ... TJniParams> static result_type call_static(const class_info &klass, jmethodID method_id, TJniParams ... params) { return (result_type)(klass.jni_env()->*method_caller_info<TJniRet>::static_method_caller())(klass.jni_value(), method_id, params ...); } template <typename TJObject, typename ... TJniParams> static result_type call_instance(const object<TJObject> &obj, jmethodID method_id, TJniParams ... params) { return (result_type)(obj.jni_env()->*method_caller_info<TJniRet>::instance_method_caller())(obj.jni_value(), method_id, params ...); } template <typename TJObject, typename ... TJniParams> static result_type call_nonvirtual(const object<TJObject> &obj, const class_info &klass, jmethodID method_id, TJniParams ... params) { if (obj.jni_env() != klass.jni_env()) throw env_mismatch(); return (result_type)(obj.jni_env()->*method_caller_info<TJniRet>::nonvirtual_method_caller_type())(obj.jni_value(), klass.jni_value(), method_id, params ...); } };
プリミティブ型用に、C++らしくなくて嫌ですが、こういう時はCプリプロセッサマクロにお任せ。
#define DEFINE_PRIMITIVE_METHOD_CALLER(jni_primitive_type, methodName) \ template <> \ struct method_caller_info<jni_primitive_type> \ { \ static auto static_method_caller() -> decltype(&JNIEnv::CallStatic##methodName##Method) { return &JNIEnv::CallStatic##methodName##Method; } \ static auto instance_method_caller() -> decltype(&JNIEnv::Call##methodName##Method) { return &JNIEnv::Call##methodName##Method; } \ static auto nonvirtual_method_caller() -> decltype(&JNIEnv::CallNonvirtual##methodName##Method) { return &JNIEnv::CallNonvirtual##methodName##Method; } \ }; DEFINE_PRIMITIVE_METHOD_CALLER(void, Void); DEFINE_PRIMITIVE_METHOD_CALLER(jboolean, Boolean); DEFINE_PRIMITIVE_METHOD_CALLER(jbyte, Byte); DEFINE_PRIMITIVE_METHOD_CALLER(jchar, Char); DEFINE_PRIMITIVE_METHOD_CALLER(jshort, Short); DEFINE_PRIMITIVE_METHOD_CALLER(jint, Int); DEFINE_PRIMITIVE_METHOD_CALLER(jlong, Long); DEFINE_PRIMITIVE_METHOD_CALLER(jfloat, Float); DEFINE_PRIMITIVE_METHOD_CALLER(jdouble, Double);
これでまず、戻り値の型を指定して適切なCallXXXMethod()
を取得してくれるクラスmethod_caller
が作れました。後は、実際に呼び出す処理を作ればよいでしょう。
例によっていろいろと省略して、static
メソッドクラスだけ実装してみます:
class method_info { protected: typedef jmethodID (JNIEnv::*get_method_func_type)(jclass klass, const char* name, const char* signature); method_info(const class_info &klass, get_method_func_type func, const char *name, const char *signature); class_info m_class; jmethodID m_method; }; template <class TRet> struct static_caller { template <class ... TMethodParams> static typename marshal::var<TRet>::native_type call(const class_info &klass, jmethodID method, TMethodParams ... params) { typedef typename marshal::method_caller<typename marshal::var<TRet>::jni_type> method_caller; auto retval = method_caller::call_static(klass, method, params ...); return (typename marshal::var<TRet>::native_type)marshal::var<TRet>(klass.jni_env(), retval); } }; template <> struct static_caller<void> { template <class ... TMethodParams> static void call(const class_info &klass, jmethodID method, TMethodParams ... params) { typedef typename marshal::method_caller<void> method_caller; method_caller::call_static(klass, method, params ...); } }; template <class TRet, class ... TParams> class static_method_info<TRet(TParams...)> : private method_info { public : static_method_info(const class_info &klass, const char *name) : method_info(klass, &JNIEnv::GetStaticMethodID, name, marshal::signature<TRet(TParams...)>()) {} typename marshal::var<TRet>::native_type operator()(const TParams & ... params) { if (!m_method) throw std::runtime_error("jni: Invalid method is called."); return static_caller<TRet>::call(m_class, m_method, (typename marshal::var<TParams>::jni_type)marshal::var<TParams>(m_class.jni_env(), params) ...); } };
途中のstatic_caller
クラスは、戻り値がvoid
とそれ以外で、戻り値の型変換をする必要があるかどうかが変わるため。関数テンプレートは部分特殊化ができないため、クラスを作っています。
実際のコード
実際に、弊社アプリ『Fuse』のAndroid版で、上記の仕組みを使ってみました。
Fuseはなるべく共通のソースコードを用いてiPhone/Android両対応できるよう、Cocos2d-xにて開発されていますので、基本的な部分はC++で開発し、プラットフォーム依存な部分のみをObjective-C++、あるいはJava+JNIで開発しています((一応、Cocos2d-xのAndroid用プロジェクトには、JNIEnv*
の取得部分だけを微妙に使い易くしたようなヘルパークラスがあるのですが))。
Fuseでは、例えばGoogle Analyticsのイベント送信等をC++コードから行なうために、Javaで次のようなクラスを作っています:
package com.cflat; import com.google.analytics.tracking.android.*; import android.app.Activity; public class GoogleAnalyticsTracker { static Activity m_activity = null ; public static void sendAppView(String screenName) { if (m_activity == null) return ; EasyTracker tracker = EasyTracker.getInstance(m_activity) ; tracker.set(Fields.SCREEN_NAME, screenName); tracker.send(MapBuilder.createAppView().build()); } public static void sendEvent(String category, String action, String label, int value) { if (m_activity == null) return ; EasyTracker tracker = EasyTracker.getInstance(m_activity) ; tracker.send(MapBuilder.createEvent(category, action, label, Long.valueOf(value)).build()) ; } public static void initialize(Activity activity) { m_activity = activity ; EasyTracker.getInstance(m_activity).activityStart(m_activity); } public static void terminate() { EasyTracker.getInstance(m_activity).activityStop(m_activity); } }
これを、C++側からは次のような形で呼び出しています:
using namespace cflat::jni; bool Tracker::sendAppViewImpl(const char *screenName) { auto method = vm::instance().get_class(CLASS_NAME).get_static_method<void(std::string)>("sendAppView") ; if (!method) return false ; method(screenName) ; return true ; } bool Tracker::sendEventImpl(const char *category, const char *action, const char *label, int value) { auto method = vm::instance().get_class(CLASS_NAME).get_static_method<void(std::string, std::string, std::string, int)>("sendEvent") ; if (!method) return false ; method(category, action, label, value) ; return true ; }
JNIをそのまま使うのと比べ、かなりわかりやすくなっている事がおわかりいただけるかと思います。