Android 界隈では Kotlin がすでに十分実用段階として広まっているので、C++ でゲーム開発をしている我々は C++ から Kotlin の関数を呼びたくなるわけですよ。
今まではネイティヴプラットフォームに対して操作するとき Java を使ってきましたが、あの長ったらしい Runnable1 や Null 安全じゃない世界とはできればおさらばしたいわけです
広いインターネットの海を探してみると Kotlin から C++ を呼び出すことについての記事はいくつか見つかるのですが、C++ から Kotlin を呼び出すことについて情報が少なかったので今回調べたことを残しておくことにしました。
準備
Android Studio の New Project から Native C++
のテンプレートを選択して生成されたプロジェクトをベースにやっていきます。
- Android Studio 3.3.2
- NDK 19.2.5345600
- macOS 10.14.4
C++ → Kotlin 呼び出し
Kotlin 側コード
C++ から int を引数にとって string を返す Kotlin の static 関数を呼ぶケースを考えます。
Kotlin 側に次のようなファイルを追加します。
package com.example.kotlinjni
class KotlinFunction {
companion object {
@JvmStatic
fun hello(value: Int) = "Hello from Kotlin. value = ${value}"
}
}
Kotlin には static 関数という概念がないので companion object を使うのですが、その際に @JvmStatic
をつけることが重要みたいでした。これを付与することによって Java インターフェイスからはあたかも今までの static 関数であるかのように振る舞えます。
実際にこの Kotlin コードをバイトコードにしてそれを Java にデコンパイルした結果が次のようになります(冗長な部分を一部省略しています)
...
public final class KotlinFunction {
public static final KotlinFunction.Companion Companion = new KotlinFunction.Companion((DefaultConstructorMarker)null);
@JvmStatic
@NotNull
public static final String hello(int value) {
return Companion.hello(value);
}
public static final class Companion {
@JvmStatic
@NotNull
public final String hello(int value) {
return "Hello from Kotlin. value = " + value;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
KotlinFunction.hello
の形で呼べそうですね!
C++ 側コード
続いて C++側 です。次のコードをテンプレートで存在していた native-lib.cpp
なり好きな場所に配置します。
こちらは既存の JNI 呼び出しの形と一切変化がありません。これまでの Java 用の資産が完全にそのまま使えると思います。さすが100% Java 互換をうたっているだけはあります
#include <jni.h>
#include <string>
static JNIEnv* _env;
std::string callStaticStringMethod(const char* className, const char* methodName, int intArg) {
std::string result {};
auto clazz = _env->FindClass(className);
auto methodID = _env->GetStaticMethodID(clazz, methodName, "(I)Ljava/lang/String;");
if (methodID != nullptr) {
auto jstr = static_cast<jstring>(_env->CallStaticObjectMethod(clazz, methodID, intArg));
const char *chars = _env->GetStringUTFChars(jstr, nullptr);
result = chars;
_env->ReleaseStringUTFChars(jstr, chars);
_env->DeleteLocalRef(jstr);
}
_env->DeleteLocalRef(clazz);
return result;
}
一般的な JNI コールのお作法をそのまま実行します。(簡単のためにenvを雑に扱ってますが。)
初見は "(I)Ljava/lang/String;"
のシグネチャなどが意味不明だと思いますが、これは「intを引数にとってstringを返す関数」という意味をしています。詳しくは JNI について調べると書いてあると思います。
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_kotlinjni_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
_env = env;
std::string result;
result = callStaticStringMethod("com/example/kotlinjni/KotlinFunction", "hello", 5);
return env->NewStringUTF(result.c_str());
}
結果
よさそう。
Kotlin → C++ 呼び出し
自動でC++メソッドを探索してもらうやり方
Native C++
テンプレートには最初から次のような形で Kotlin → C++ の呼び出しが実装されており、見よう見まねでやればとりあえずできます
...
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = stringFromJNI()
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
external fun stringFromJNI(): String
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_kotlinjni_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
...
}
Java でいう
static native void hogehoge();
は Kotlin では
external fun hogehoge()
として表現されるように見えます。
C++メソッドを自分で登録するやり方
このままでも良いのですが、
extern "C" JNIEXPORT jstring JNICALL Java_com_example_kotlinjni_MainActivity_stringFromJNI(
この記述は難しいです。Kotlin 側のパッケージ名やクラス名が変わったときなどに対応が大変そうです。
また System.loadLibrary("native-lib")
も雰囲気でしかよくわかっていません。
そこで、動的にあとから C++ のメソッドを Kotlin(Java) 側に登録する方法も紹介しておきます
void registerNative(const char* className, const char* jmethodName, const char* jsignature, void* methodPtr) {
auto clazz = _env->FindClass(className);
JNINativeMethod methods[] {
{ jmethodName, jsignature, methodPtr },
};
_env->RegisterNatives(clazz, methods, 1);
_env->DeleteLocalRef(clazz);
}
namespace CppFunction {
static jstring nativeStringFromJNI(JNIEnv* env) {
const char* result = "Hello from Cpp";
return env->NewStringUTF(result);
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_kotlinjni_MainActivity_register(
JNIEnv* env,
jobject /* this */) {
_env = env;
registerNative("com/example/kotlinjni/CppFunction",
"stringFromJNI",
"()Ljava/lang/String;",
(void*)CppFunction::nativeStringFromJNI);
}
registerNative
関数で Kotlin 側の関数と C++ 側の関数を紐づけます。この際にもシグネチャが必要になります。
Kotlin側の記述は以下です。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
register() // Cppの関数をKotlinから参照できるように最初にどこかでこれを呼ぶ
sample_text.text = CppFunction.stringFromJNI()
}
external fun register()
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
package com.example.kotlinjni
class CppFunction {
companion object {
@JvmStatic
external fun stringFromJNI(): String
}
}
register
というエントリーポイントを用意し、その中で Kotlin 側の関数に対応する C++ 側の関数を JNIEnv に登録していきます。
個々の関数を一つずつ登録していくのがめんどくさいっちゃめんどくさいです。Kotlin 側で System.loadLibrary
を呼ぶ必要はないですが、どちらが扱いやすいかは皆さんの判断にお任せします。
結果
よさそう。Proguard への記述
C++ から Java コードを呼び出す際はメソッドをシンボルで呼ぶため、proguard の影響をもろにうけます。
シンボルを壊されないよう、proguard を使用する際は以下を記述することを忘れないようにしましょう。
-keep class com.example.kotlinjni.** {
native <methods>;
public static <methods>;
}
所感
C++ から見たときに完全に Java と同じ形式で扱えるのでやりやすいなと思いました。
Kotlin 側でも @JvmStatic
をつけるということさえわかればあとは自然に Kotlin を使用できるので今後はプラットフォーム関数を呼ぶときに積極的に Kotlin を使っていきたいと思います。
iOSのほうはBridging Headerあたりがちょっと難しいのと、Objective-C++ が便利すぎるので C++ から Swift を使うモチベがあまりないのですが、Kotlin に関しては Java の完全上位互換な印象を受けました。
-
いつからかJavaでもラムダが使えるようになって最近は便利になりましたね ↩