fbjni を使ってみる (1): ビルド設定と簡単なメソッド

# はじめに

Java と C++ を連携させるためには JNI を使うのが基本だけど、あまり使いやすいものではない。これを少し簡単にするために Meta が [fbjni](https://github.com/facebookincubator/fbjni) というライブラリを公開している。使ってみたところ、次の点が便利に感じた。

* C++ から Java のメソッドを呼ぶときに、長い Java のメソッドシグニチャ文字列を書く必要がない。
* Java から C++ の関数を呼ぶときに Java のパッケージ名、クラス名、メソッド名をくっつけた長い C++ 関数名書く必要がない。
* Java の `String` と C++ の `std::string` の変換を自動的にやってくれる。
* 一部を Java, 一部を C++ で実装した様なハイブリッドクラスというものを作れ Java と C++ の密な連携がやりやすい。
* `std::function` を内包した Java のオブジェクトを C++ で作って Java に渡しておいて、後で Java から C++ にコールバックできる。

[ドキュメント](https://github.com/facebookincubator/fbjni/blob/main/docs/quickref.md) はしっかり書いてある部分と全く書いてない部分があるので、何回かに分けて解説してみる。初回は Android で使う場合のビルド設定をして文字列を返す関数を C++ で書いて Java から呼んでみる。以後、Android で使う場合の話しかしないが fbjni 自体に Android への依存はない。

なお Meta 製というと、Meta の都合ですぐに deprecated になったりするのではと心配になるかもしれないが Android 版 ReactNative の実装にしっかり使われているのでしばらくは大丈夫そう。

# 環境

* Android Studio 2022.1.1
* fbjni 0.3.0
* macOS Ventura 13.1

# プロジェクト作成

NDK を使って Java(Kotlin) から C++ を呼び出すので "Native C++" テンプレートでプロジェクトを作る。

![スクリーンショット 2023-01-29 17.19.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110491/74e087cb-51c6-dab4-9f9f-f6f95edc6c51.png)

プロジェクト名などは適当に。

![スクリーンショット 2023-01-29 17.21.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110491/a95e8cd5-f35b-9e02-9a97-718c783486ae.png)

![スクリーンショット 2023-01-29 17.22.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110491/5f279f7a-574a-6aa8-3452-f6b6b40a5dd0.png)

"Native C++" テンプレートで作ったプロジェクトは NDK を使うための設定が完了していて JNI を使って Java(Kotlin) から C++ を呼び出す簡単なサンプルになっている。ここでその内容を確認しておく。

* `app/build.gradle` の `externalNativeBuild` ブロックによって CMake が起動される。
* CMake は `app/src/main/cpp/CMakeLists.txt` の内容に従って `app/src/main/cpp/native-lib.cpp` をコンパイルして `libfbjniapplicatioin1.so` 共有ライブラリをビルドする。
* `native-lib.cpp` は `jstring Java_com_example_fbjniapplicatioin1_MainActivity_stringFromJNI(JNIEnv*,jobject)` という関数を定義している。この関数は Java の文字列オブジェクトを生成して返す。この関数は Java(Kotlin) からは `MainActivity` クラスの `stringFromJNI` として呼び出せる。
* `MainActivity` クラスは Kotlin のコンパニオンオブジェクトを使ってクラスのロード時に `libfbjniapplicatioin1.so` をロードする。そして `onCreate` 時に `stringFromJNI` を呼んで C++ から受け取った文字列を表示する。

今回は JNI を直接使用している C++ の `stringFromJNI` の実装を fbjni を使ったものに置き換えていく。

# ビルド設定

## fbjni の追加

まずは依存先に fbjni を追加する。

```diff
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -39,6 +39,9 @@ android {
     buildFeatures {
         viewBinding true
     }
+    dependencies {
+        implementation 'com.facebook.fbjni:fbjni:0.3.0'
+    }
 }

 dependencies {
```

## prefab の有効化

fbjni を使う場合は C++ の共有オブジェクト（今回の場合は `libfbjniapplicatioin1.so` のこと) に fbjni の C++ のライブラリをリンクする必要がある。その設定を簡単に行うための prefab という仕組みを有効にする。

```diff
diff --git a/app/build.gradle b/app/build.gradle
index ec46e05..3e5e64b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -17,6 +17,9 @@ android {
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }

+    buildFeatures {
+        prefab true
+    }
     buildTypes {
         release {
             minifyEnabled false
```


## C++ ランタイムの変更

[CMake のデフォルト設定では C++ ランタイムとして `c++_static` が使われる](https://developer.android.com/ndk/guides/cpp-support#selecting_a_c_runtime)。 これを `c++_dynamic` に変更する。

```diff
diff --git a/app/build.gradle b/app/build.gradle
index 3e5e64b..e76c4f3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,6 +15,12 @@ android {
         versionName "1.0"

         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+        externalNativeBuild {
+            cmake {
+                arguments "-DANDROID_STL=c++_shared"
+            }
+        }
     }

     buildFeatures {
```

## fbjni の C++ のライブラリのリンク

CMake の設定を変更して C++ の共有ライブラリに fbjni の C++ のライブラリをリンクする。prefab を有効にしたおかげでこのステップが簡単になっている。

```diff
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index f4f7ac0..3a9f03c 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -36,6 +36,8 @@ find_library( # Sets the name of the path variable.
         # you want CMake to locate.
         log)

+find_package(fbjni REQUIRED CONFIG)
+
 # Specifies libraries CMake should link to your target library. You
 # can link multiple libraries, such as libraries you define in this
 # build script, prebuilt third-party libraries, or system libraries.
@@ -45,4 +47,5 @@ target_link_libraries( # Specifies the target library.

         # Links the target library to the log library
         # included in the NDK.
-        ${log-lib})
\ No newline at end of file
+        ${log-lib}
+        fbjni::fbjni)

```

# fbjni への移行

## stringFromJNI の書き換え

JNI では Java の `static native` (Kotlin だと `external`) メソッドを実装する C++ 側の関数をパッケージ名とクラス名とメソッド名を含んだ長い名前の `external "C"` リンケージの C 言語の関数として実装していた。fbjni ではこれを次の様なクラスの `static` メンバ関数として `native-lib.cpp` に実装する。

```cpp:native-lib.cpp
#include <fbjni/fbjni.h>

class MainActivity : public facebook::jni::JavaClass<MainActivity> {
public:
    static constexpr auto kJavaDescriptor = "Lcom/example/fbjniapplicatioin1/MainActivity;";

    static std::string stringFromJNI(facebook::jni::alias_ref<facebook::jni::JClass> clazz) {
        return std::string{"Hello from fbjni"};
    }
};
```

fbjni は `kJavaDescriptor` 定数によってこのクラスが Java の `com.example.fbjniapplicatioin1.MainActivity` クラスの `static native` メソッドを実装することを知る。C++ のクラス名は分かりやすさのため Java のクラス名と揃えてあるが必ずしもそうする必要はない。そして `jstring` ではなく `std::string` を返すことができる！

## stringFromJNI を Java VM へ登録する

ここまでではまだ fbjni は Java の `MainActivigy#stringFromJNI` と C++ の `MainActivity::stringFromJNI` が対応していることを知らない。同じ名前のものを自動的に対応づけてくれたりはしない。fbjni では明示的に登録して関連付けをする必要がある。そのための関数を追加して、C++ の `MainActivity` クラスは完成。

```cpp:native-lib.cpp
#include <fbjni/fbjni.h>

class MainActivity : public facebook::jni::JavaClass<MainActivity> {
public:
    static constexpr auto kJavaDescriptor = "Lcom/example/fbjniapplicatioin1/MainActivity;";

    static std::string stringFromJNI(facebook::jni::alias_ref<facebook::jni::JClass> clazz) {
        return std::string{"Hello from fbjni"};
    }

    static void registerNatives() {
        javaClassStatic()->registerNatives({
            makeNativeMethod("stringFromJNI", MainActivity::stringFromJNI)
        });
    }
};
```

`javaClassStatic` は親クラスの関数、`makeNativeMethod` はマクロなので名前空間修飾はいらない。

この `MainActivity::registerNatives` は `JNI_Onload` を利用してこの共有ライブラリがロードされるとき、すなわち Java(Kotlin) の `MainActivity` クラスがロードされ、そのコンパニオンオブジェクトによって `libfbjniapplicatioin1.so` がロードされるときに実行されるようにする。これを加えて、もともとあった `jstring Java_com_example_fbjniapplicatioin1_MainActivity_stringFromJNI(JNIEnv*,jobject)` を削除して `native-lib.cpp` は次のようになる。

```cpp:native-lib.cpp
#include <jni.h>
#include <string>
#include <fbjni/fbjni.h>

class MainActivity : public facebook::jni::JavaClass<MainActivity> {
public:
    static constexpr auto kJavaDescriptor = "Lcom/example/fbjniapplicatioin1/MainActivity;";

    static std::string stringFromJNI(facebook::jni::alias_ref<facebook::jni::JClass> clazz) {
        return std::string{"Hello from fbjni"};
    }

    static void registerNatives() {
        javaClassStatic()->registerNatives({
            makeNativeMethod("stringFromJNI", MainActivity::stringFromJNI)
        });
    }
};

extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    return facebook::jni::initialize(vm, [] {
        MainActivity::registerNatives();
    });
}
```

これを実行して `MainActivity` に "Hello from fbjni" と表示されれば成功。`native-lib.cpp` 自体は fbjni 導入前より大きくなってしまったが、`stringFromJNI` の実装はずっとシンプルになった。また簡単にするため C++ の `MainActivity` クラスはこのファイルの中に実装したが、もちろん別ファイル(.cpp/.h)に分けても良い。

プロジェクトファイルは https://github.com/yamoridon/FBJNIApplicatioin1 にあります。