はじめに
Java と C++ を連携させるためには JNI を使うのが基本だけど、あまり使いやすいものではない。これを少し簡単にするために Meta が fbjni というライブラリを公開している。使ってみたところ、次の点が便利に感じた。
- C++ から Java のメソッドを呼ぶときに、長い Java のメソッドシグニチャ文字列を書く必要がない。
- Java から C++ の関数を呼ぶときに Java のパッケージ名、クラス名、メソッド名をくっつけた長い C++ 関数名書く必要がない。
- Java の
String
と C++ のstd::string
の変換を自動的にやってくれる。 - 一部を Java, 一部を C++ で実装した様なハイブリッドクラスというものを作れ Java と C++ の密な連携がやりやすい。
-
std::function
を内包した Java のオブジェクトを C++ で作って Java に渡しておいて、後で Java から C++ にコールバックできる。
ドキュメント はしっかり書いてある部分と全く書いてない部分があるので、何回かに分けて解説してみる。初回は 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++" テンプレートでプロジェクトを作る。
プロジェクト名などは適当に。
"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 を追加する。
--- 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 --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
が使われる。 これを c++_dynamic
に変更する。
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 --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
に実装する。
#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
クラスは完成。
#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
は次のようになる。
#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 にあります。