LoginSignup
19
16

More than 5 years have passed since last update.

C++からKotlin関数を呼びたいし、呼ばれたい

Last updated at Posted at 2019-04-07

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 側に次のようなファイルを追加します。

KotlinFunction.kt
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 にデコンパイルした結果が次のようになります(冗長な部分を一部省略しています)

KotlinFunction.decompiled.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 について調べると書いてあると思います。

native-lib.cpp
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());
}

結果

スクリーンショット 2019-04-07 16.37.28.png

よさそう。

Kotlin → C++ 呼び出し

自動でC++メソッドを探索してもらうやり方

Native C++ テンプレートには最初から次のような形で Kotlin → C++ の呼び出しが実装されており、見よう見まねでやればとりあえずできます

MainActivity.kt
...
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")
        }
    }
}
native-lib.cpp
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) 側に登録する方法も紹介しておきます

native-lib.cpp
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側の記述は以下です。

MainActivity.kt
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")
        }
    }
}
CppFunction.kt
package com.example.kotlinjni

class CppFunction {
    companion object {
        @JvmStatic
        external fun stringFromJNI(): String
    }
}

register というエントリーポイントを用意し、その中で Kotlin 側の関数に対応する C++ 側の関数を JNIEnv に登録していきます。
個々の関数を一つずつ登録していくのがめんどくさいっちゃめんどくさいです。Kotlin 側で System.loadLibrary を呼ぶ必要はないですが、どちらが扱いやすいかは皆さんの判断にお任せします。

結果

スクリーンショット 2019-04-07 17.05.59.png
よさそう。

Proguard への記述

C++ から Java コードを呼び出す際はメソッドをシンボルで呼ぶため、proguard の影響をもろにうけます。
シンボルを壊されないよう、proguard を使用する際は以下を記述することを忘れないようにしましょう。

proguard-rules.pro
-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 の完全上位互換な印象を受けました。


  1. いつからかJavaでもラムダが使えるようになって最近は便利になりましたね 

19
16
0

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
19
16