JNIでKotlinからC++のクラスを利用する
AndroidアプリでC++のコードを利用したい。C++のクラスをJavaやKotlinのクラスと同じようにKotlinから生成、呼び出しをしたかった。
JNI
Java Native Interfaceといい、JVMからCPU上で直接実行されるコードを呼び出すインターフェースで、これを使うことでJavaやKotlinからCやC++のコードを呼び出すことができます。
とりあえずJNIを利用してCやC++の関数を呼び出す方法は調べたら沢山出てくるので割愛。既にKotlinからC++の関数を呼び出せることを前提とし、ここではC++のインスタンスを保持する方法を説明します。
ポインタ = 整数
JNI (Java Native Interface)ネタは、この記事で一応最後です。 前々回は、Javaから渡された配列や文字列を簡単に受け取る方法について述べました。配列の受け渡しに関しては、最悪だと呼び出し毎にネイティブヒープへのコピーが発生する可能性があるので、ネイティブ側とVM側のどちらにデータを配置するのが正しいのかを考慮して設計する必要があります。 前回は、C++をJNI経由で呼び出す際の例外処理について述べました。STLに例外が必要となるわけですし、Android等でも基本的には-fexceptionsを有効にしてビルドした方がいいと思います。 さて、ここまでの知識だと、Jav…
参考にしたのはこの記事。ポインタ=整数だからJava側のフィールド変数に格納して、必要な際はC++からJNI経由で呼び出せばいいって考えが分かればあとは楽。
参考サイトではC++のどんなクラスでも利用できるようにジェネリクスを使ったラッパークラスを用意していたが、僕の場合使いたいC++のクラスが限られていたため、ポインタの値を取得してキャストする関数を用意するだけに留めた。
サンプル
Use C++ class from kotlin. Contribute to Hiroki-Kawakami/Kotlin-CppClassTest development by creating an account on GitHub.
サンプルで作ったプロジェクトをgitに上げておきます
C++クラス
今回はサンプルとして以下のC++クラスをKotlinから利用しようと思います。
#include <string>
using std::string;
class counter {
public:
counter();
int count;
string description();
void increment();
};
#include "counter.hpp"
counter::counter() {
count = 0;
}
string counter::description() {
return std::to_string(count) + "回実行されました";
}
void counter::increment() {
count++;
}
簡単なコードなので説明は飛ばします。
Kotlinのラッパークラス
package com.example.cppclasstest
class Counter {
private var pointer: Long = 0
var count: Int
get() = getCountCpp()
set(value) = setCountCpp(value)
private external fun getCountCpp(): Int
private external fun setCountCpp(value: Int)
external fun description(): String
external fun increment()
external fun delete()
protected fun finalize() = delete()
}
pointerという名前で、C++インスタンスのポインタを保持するLong型のフィールドを用意します。
external funを呼ぶとC++で書かれたJNI関数が呼び出される仕組み。count変数はgetter、setterメソッドを直接externalにすることができなかったので、別途getCountCpp、setCountCppというを用意して、それをget、setから呼び出す形になってます。
deleteはC++のインスタンスを削除してメモリを解放するもの。finalizeでdeleteを呼び出してGCが働いた際にC++のインスタンスが解放されるようにする。
JNIで橋渡し
Kotlinのexternal funを呼び出すとC++のJNI関数が呼ばれるので、そこから対応するC++の関数を呼び出してやればいいのですが、今回C++のクラスを利用するので関数を呼び出すためにはインスタンスのポインタをKotlin側から取得する必要があります。
ポインタを取得してキャストする処理は毎回使うので関数化し、あとはそれぞれメソッドに対応するJNI関数を用意してやりましょう。
#include <jni.h>
#include <string>
#include "counter.hpp"
counter* getCounterInstance(JNIEnv* env, jobject thisj) {
static jclass classj = env->GetObjectClass(thisj);
static jfieldID fieldId = env->GetFieldID(classj, "pointer", "J");
long pointer = env->GetLongField(thisj, fieldId);
if (!pointer) {
pointer = (jlong)(new counter());
env->SetLongField(thisj, fieldId, (jlong)pointer);
}
return (counter*)pointer;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_cppclasstest_Counter_getCountCpp(JNIEnv* env, jobject thisj) {
return getCounterInstance(env, thisj)->count;
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_cppclasstest_Counter_setCountCpp(JNIEnv* env, jobject thisj, jint value) {
getCounterInstance(env, thisj)->count = value;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cppclasstest_Counter_description(JNIEnv* env, jobject thisj) {
string res = getCounterInstance(env, thisj)->description();
return env->NewStringUTF(res.c_str());
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_cppclasstest_Counter_increment(JNIEnv* env, jobject thisj) {
getCounterInstance(env, thisj)->increment();
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_cppclasstest_Counter_delete(JNIEnv* env, jobject thisj) {
jclass classj = env->GetObjectClass(thisj);
jfieldID fieldId = env->GetFieldID(classj, "pointer", "J");
long pointer = env->GetLongField(thisj, fieldId);
if (!pointer) return;
delete (counter*)pointer;
env->SetLongField(thisj, fieldId, (jlong)0);
}
JNI関数の記述量が多いので難しそうに見えますが、やってることは至極単純です。
特に今回のメインはgetCounterInstance関数。
6~8行目でKotlinのCounterクラスにあったpointer変数の値を取得しています。ちなみにjclassとjfieldIDをstaticにしているのは、GetObjectClassやGetFieldIDの呼び出しコストが高く、一度取得すれば使い回しができるからです。
9~12行目のif文では、もし取得したポインタの値がセットされていなかったらC++のクラスインスタンスを生成してpointer変数にセットする処理を行なっています。
あとの13行目はキャストして結果を返してるだけですね。
getCountCpp、setCountCpp、description、increment関数はgetCounterInstanceで取得したインスタンスから変数や関数を呼び出しているだけ。
38行目以降のdelete関数は、pointerを取得してdelete、pointerに0をセットしているだけです。
利用
class MainActivity : AppCompatActivity() {
val counter = Counter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = counter.description()
}
fun increment(view: View) {
counter.increment()
sample_text.text = counter.description()
}
fun reset(view: View) {
counter.count = 0
sample_text.text = counter.description()
}
override fun onDestroy() {
super.onDestroy()
counter.delete()
}
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
あとは普通にKotlinから呼び出すだけです。
ちなみに、オブジェクトを使い終わったときにdeleteを呼び出すかなのですが、呼び出せるなら呼び出した方がいいです。ファイナライザが呼び出されるのはGCが走ってオブジェクトが不要だとJVMが判断した時なので、オブジェクトが不要になった後いつ呼び出されるか分かりません。
それこそ、ファイナライザを実装するとオブジェクトの生成と解放が遅くなるので、必ず明示的にdeleteを呼び出すようにしてfinalizeを実装しないのもありです。
ただ、メモリが不足すればGCが走りますし、その際は多少の負荷はかかりますが、その程度も切り詰めて速度を要求するようなアプリでない限り別にdeleteを自分で呼び出さずにファイナライザに任せてしまってもいいと思います。
finalizeが呼び出されずにアプリが終了したとして、確保したメモリが解放されずにメモリリークすることはないですし。最近のOSで使われている仮想メモリは終了したプロセスが使用していたメモリ領域が分からないほどバカではないので。
コメント