LoginSignup
11
8

More than 5 years have passed since last update.

ラッパークラス開発で学ぶ実践C++14 ~JNI~

Last updated at Posted at 2018-01-21

Android JNI の学習と、C++11/14 1の学習を兼ねて、また、github に好みのもの2がなかったということもあって、(いまさら) JNI ラッパーを自作した。

これを題材にしてC++の実践方法を記事にしようと思ったが、前座のつもりで書いたライブラリ紹介が思いの外長かったため、実践の話は別の機会に預け、今回は紹介のみとした。

概要

uc-jni.hppヘッダファイル一つコンパイル不要。インクルードするだけで即座に使える。
ユーティリティ関数もあるが、コードの大部分を型の定義と解決に費やしている。

元のJNI関数を使う場合と比べて、余計な実行時コストがなく、簡潔、型安全でリークしにくいコードが書ける(はずだ)。

メソッドやフィールドへのアクセス

スタティックメソッド呼び出し

JNIでまず鬱陶しいのが、タイプシグネチャ型ごと定義された関数群だ。

uc-jni.hpp を使うとスタティックメソッドの呼び出しは以下のように書ける。

// java.lang.System クラスを、C++コード上では System という型で定義する。
DEFINE_JCLASS_ALIAS(System, java/lang/System);


// void System.gc() メソッドを取得する。
auto gc = uc::jni::make_static_method<System, void()>("gc");


// System.gc() を実行する。
gc();

タイプシグネチャは必要ないし、関数の呼び分けも考えなくていい。
それを解決するために必要な情報は、コードの上2行ですべて得ることができる。
そして、これらはすべてコンパイル時に解決されるため、実行時に余計な処理が増えることもない

また、gc()void() 型で定義されるため、引数や戻り値に値を指定すればコンパイルエラーとなる。

// コンパイルエラー
gc(10);

// コンパイルエラー
int x = gc();

これだけ情報を詰め込んでも余計なリソースは一切消費しない。sizeof(gc) == sizeof(jmethodID) である。

インスタンスメソッド呼び出し

インスタンスメソッドの場合は、以下のように対象インスタンスを第1引数に渡す必要がある

// String Object.toString() メソッドを取得する。
auto toString = uc::jni::make_method<jobject, jstring()>("toString");

// String jstr = obj.toString();
auto jstr = toString(obj);

jstr の型は local_ref<jstring> というもので、これは std::unique_ptr<jstring,> の別名だ。
スコープが切れた時点で DeleteLocalRef() が自動的に呼び出される。

もし、戻り値の jstring を操作するのが面倒なら、代わりに std::string を返すようにすることもできる。

                                              //  ↓ 関数の型を std::string() とする。
auto toString = uc::jni::make_method<jobject, std::string()>("toString"); 

これは、呼び出しごとにstd::string変換処理が入る
そのコストが気になるようならjstring()型を選択すればよい。

同様に jbooleanboolに変換することもできる。

コンストラクタ呼び出し・フィールドへのアクセス

メソッドと同様のため、サンプルコードのみ紹介しておく。

// android.graphics.Point クラスのエイリアス定義
DEFINE_JCLASS_ALIAS(Point, android/graphics/Point);

// コンストラクタ Point(int x, int y) を取得する。
auto newPoint = uc::jni::make_constructor<Point(int,int)>();

// フィールドを取得する。
auto x = uc::jni::make_field<Point, int>("x");
auto y = uc::jni::make_field<Point, int>("y");


auto p = newPoint(12, 34);    // Point p = new Point(12, 14);

int valueX = x.get(p);        // int valueX = p.x;
int valueY = y.get(p);        // int valueY = p.y;

x.set(p, 100);                // p.x = 100;
y.set(p, 200);                // p.y = 200;

文字列処理

jstringstd::string の相互変換をサポートする。

auto jstr = uc::jni::to_jstring("Hello World!");

std::string str = uc::jni::to_string(jstr); 

C++11から導入されたUTF-16を扱う型もサポートする。こちらの方がJavaとの相性は良い。

auto jstr = uc::jni::to_jstring(u"こんにちは、世界!"); 

std::u16string str = uc::jni::to_u16string(jstr);      // UTF8 への変換コストがかからない。

jstring を作りたいなら uc::jni::join() が便利だ。
jstring, const char*, const char16_t*, std::string, std::u16string を好きなだけ放り込んで結合できる。

jstring getMessage(jstring name)
{
    return uc::jni::join(u"私の名前は", name, u"です。").release(); 
}

配列処理

同様に、jarray 型と std::vector の相互変換もサポートする。

// jintArray -> std::vector<int>
std::vector<int> intValues = uc::jni::to_vector<int>(intArray);

// std::vector<int> -> local_ref<jintArray>
auto jintValues = uc::jni::to_jarray(intValues);
// jobjectArray -> std::vector<std::string>
auto stringValues = uc::jni::to_vector<std::string>(objArray); 

// std::vector<std::string> -> array<jstring> (後述)
auto jstringValues = uc::jni::to_jarray(stringValues);  

uc::jni::array<T>

JNI定義の型で、オブジェクトの配列を表す型は jobjectArray しかない。
これでは型を明確にすることで得られるC++のメリットが活かせない。

uc-jni では jobjectArray の代わりに uc::jni::array<T> を使う
jobjectArray とほぼ同様に使えて、かつ要素の型情報を持たせることができる。

先のメソッド定義において、テンプレートに任意のオブジェクト型配列を指定したい場合にも、このuc::jni::array<T> を使う。3

// String[] Hoge.intsToString(int[]) メソッドを取得する。
auto intsToStrings 
    = uc::jni::make_method<Hoge, uc::jni::array<jstring>(jintArray)>("intsToStrings");

なお、以下のように書いて std::vector でやり取りすることもできる。

auto intsToStrings 
    = uc::jni::make_method<Hoge, std::vector<std::string>(std::vector<int>)>("intsToStrings");

低コストAPI

何度も言うように、変換処理はコストがかかるため、低コストなAPIも用意している。

Get/SetArrayRegion()の薄いラッパー
// jlong でも jdouble でも同じように書ける。指定された型以外の変数を渡すことはできない。

auto array = uc::jni::new_array<jint>(10);

const std::vector<int> values = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
uc::jni::set_region(array, 0, 10, values.data());

std::vector<int> values2(10);
uc::jni::get_region(array, 0, 10, values2.data());
GetArrayElements()の薄いラッパー
for (auto&& v : uc::jni::get_elements(array)) {
    // 要素の読み書きOK
}

for (auto& v : uc::jni::get_const_elements(array)) {
    // 要素は読み取り専用
}

紹介はこのくらいで。興味があれば github も覗いてみてほしい。
次は解説記事を書いてみる予定。



  1. C++14をターゲットとしたのは、Android Studio が標準で対応していたからだ。 

  2. あまり処理を持たず、薄い方が良い。ユーティリティ的な処理はあると良いが、使う・使わないを選べたほうがいい。処理がないのだから、コンパイルなんてさせないでほしい。コンパクトであって欲しい。全体を把握しないと使い始められないというのも困る。自分が使う部分だけでも覚えたらすぐ使えて、使いながら徐々に覚えていけるようなものだとうれしい。 

  3. これは、タイプシグネチャを自動解決するために必須の情報だ。 jobjectArray を指定すると、シグネチャが [Ljava/lang/Object; になってしまう。 

11
8
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8