久しぶりにJNIを触ってみたらだいぶやりやすくなっていたので感動しました
メモがてらツラツラ書いてみました。
そもそもJNIとは?
Java Native Interface (JNI) Javaプラットフォームにおいて、Javaで記述されたプログラムと、他のプログラミング言語(たとえばCやC++など)で書かれた、実際のCPU上で動作するコード(ネイティブコード)とを連携するためのインタフェース仕様である
Wikipedia より
環境構築
大まかには以下の手順になります。
- jni用のディレクトリ作成
- Kotlin/Java側のネイティブ呼び出し用のクラスを作る
- CMakeLists.txtを作成
- ネイティブ側実装
-
app/build.gradle
に設定追加
CMakeLists.txt を作成してbuild.gradleに記述するだけで
コンパイルして使えるとは。。おじさん感動しました
(昔はAndroid.mk作ってだねえ。。)
1. jni用のディレクトリ作成
デフォルトだと src/main/cpp
が使われているっぽいが、
分かりやすければどこでも良いかと個人的には思います。
今回は src/main/java/xxxxx/jni
を作成しました。
2. Kotlin/Java側のネイティブ呼び出し用のクラスを作る
例としてネイティブから hello
の文字列を返すメソッドを持ったクラスを作成してみます。
class SampleNative {
external fun hello(): String
companion object {
init {
System.loadLibrary("sample-native")
}
}
}
↑で指定した sample-native
は作成される共有ライブラリ名を指定してます。
3. CMakeLists.txtを作成
cmake_minimum_required(VERSION 3.4.1)
add_library(
sample-native
SHARED
sample-native.cpp)
find_library(
log-lib
log)
target_link_libraries(
sample-native
${log-lib})
4. ネイティブ側実装
# ifndef __SAMPLE_NATIVE_H_
# define __SAMPLE_NATIVE_H_
# include <jni.h>
# include <android/log.h>
# include <string>
# ifdef __cplusplus
extern "C" {
# endif
JNIEXPORT jstring JNICALL
Java_xxx_xxx_xxx_xxx_jni_SampleNative_hello(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
# ifdef __cplusplus
}
# endif
# endif // __SAMPLE_NATIVE_H_
Java_xxx_xxx_xxx_xxx_jni_SampleNative_hello
の xxx
にはパッケージ名が入ります。
5. app/build.gradle
に設定追加
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake { path "src/main/java/xxx/jni/CMakeLists.txt" }
}
}
これで晴れてビルドできるようになりました
使う時には普通のメソッドを呼び出す様に使えます。
val sample = SampleNative()
sample.hello() // Hello from C++
Tips
実装中に個人的に便利だと思ったものをピックアップしました。
-
Log出力に関して
#define TAG "SampleNative" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) // LOGD("numbers %d %d", num1, num2);
↑の様にdefineしておくと便利
-
オブジェクト生成
#if defined(DEBUG) || defined(_DEBUG) #define __ASSERT__(e) assert(e) #else #define __ASSERT__(e) #endif // DEBUG /** * 引数なしコンストラクタを呼び出しクラスオブジェクトを生成<br> * * @param env JNIEnv * @param classPath クラスパス * @return jobject */ jobject newObject(JNIEnv *env, const char *classPath) { jclass cls = env->FindClass(classPath); __ASSERT__(cls != NULL); // コンストラクタ取得 jmethodID mid = env->GetMethodID(cls, "<init>", "()V"); __ASSERT__(mid != NULL); jobject jobj = env->NewObject(cls, mid); // 後片付け env->DeleteLocalRef(cls); return jobj; } /** * java.util.ArrayListクラスのインスタンス取得 * * @param env JNIEnv * @return インスタンス */ jobject newArrayList(JNIEnv *env) { return newObject(env, "java/util/ArrayList"); }
JNI側でJava/Kotlin側オブジェクト生成はよく使うので↑の様にメソッド作っておくと便利
バッドノウハウ
JUnitができない!
-
Android JUnit Testing for Native Libraries
どうしてもJUnitだとホストのPC環境のアーキテクチャでビルドするので
UnsatisfiedLinkError
が発生してしまう。頑張って↑のリンク先の様にビルドするか、instrumentedテストでやる