読者です 読者をやめる 読者になる 読者になる

C++からC++らしくJNIを呼んでみる

JNI C++

 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::vmjni::class_infoは、ある程度C++に慣れれば誰でも作れるはずですが(コピーコンストラクタと代入演算子NewLocalRef()、デストラクタと代入演算子DeleteLocalRef()を忘れないように!)、シグネチャーを型指定で済ますように作るには、かなりC++のテンプレートに慣れていないと難しいのではないでしょうか?
 ものは試し、ちょっと作ってみましょう。

JNIラッパーの構成

 上記のコードを見て気付いた方もいると思いますが、シグネチャーの型指定はJNIの型であるjstringjintではなく、C++の型であるstd::stringint32_tを使っています。何故なら、メソッド呼び出し処理の内部で自動変換してくれる方が扱いやすいので。 場合によっては、同じjstringインスタンス複数メソッド呼び出しで共有したい、という場合もあるかもしれませんが(当然、その方が余計なオーバーヘッドが不要になります)、今回は気にしないものとします。必要であればjstringのラッパー←→jstringの変換クラスを追加してやればよいだけでしょう。

 これらを鑑みると、JNIのC++らしいラッパーの実装は、次のような部分から成るようにできるでしょう:

  1. C++型←→JNI型変換部
  2. JNI型←→シグネチャー文字列変換部
  3. JNI型←→CallXXXMethod変換部

 なお、jstring(あるいは他のリファレンス型)をRAIIでいい感じに構築/破棄してくれる部分も1.の中に含まれるものとします。

C++←→JNI型

 まずは、C++型を指定した時に対応するJNI型を求めるためのメタ関数が必要になります。  この部分をどう設計するかは悩ましいのですが、試しに、次のようにしてみましょう:

  1. 1/2/4/8バイトの符号付き整数型は、それぞれjbyte/jshort/jint/jlongに変換する((本当は「int8/16/32/64_tのみ」としたかったところですが、intで指定できないのもなんか嫌なので……))
  2. 4/8バイトの浮動小数型は、それぞれjfloat/jdoubleに変換する
  3. char16_tjcharjcharに変換する((sizeof(wchar_t) == 2の場合、wchar_tjcharにしていいかも))
  4. booljbooleanjbooleanに変換する
  5. 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つのパターンがあります。

  1. 英字1文字(プリミティブ型)
  2. "L"+クラス名+";"(非プリミティブ型)
  3. "["+他の型(配列型)
  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をそのまま使うのと比べ、かなりわかりやすくなっている事がおわかりいただけるかと思います。

JNI:Java Native Interfaceプログラミング―C/C++コードを用いたJavaアプリケーション開発 (Java books)

JNI:Java Native Interfaceプログラミング―C/C++コードを用いたJavaアプリケーション開発 (Java books)