(JUCE on Androidの)JNI_OnLoadのLTO (link time optimization)でハマった話

3 min読了の目安(約2700字TECH技術記事

C++なんもわからんatsushienoです(前フリ)

今回は小ネタ。

Android用のJUCEアプリケーションに自分のC++共有ライブラリを参照して使っているのだけど、最近JNI_OnLoad()が競合してビルドに失敗する、という問題が発生したことがあった

CMakeFiles/juce_jni.dir/home/runner/work/aap-juce-plugin-host/aap-juce-plugin-host/apps/AudioPluginHost/JuceLibraryCode/include_juce_core.cpp.o (symbol from plugin): In function `juce::JNIClassBase::~JNIClassBase()':

(.text+0x0): multiple definition of `JNI_OnLoad'

/home/runner/work/aap-juce-plugin-host/aap-juce-plugin-host/apps/AudioPluginHost/Builds/Android/app/../../../android-audio-plugin-framework/java/androidaudioplugin/build/intermediates/cmake/release/obj/arm64-v8a/libandroidaudioplugin.so::(.text+0x83b0): first defined here

clang++: error: linker command failed with exit code 1 (use -v to see invocation)

自分のライブラリにはJNI_OnLoad()が定義されている。そしてJUCEの中でも定義されている。JUCEライブラリは(Projucerを使っている場合は)libjuce-jni.soとしてビルドされるのだから、これが競合するシンボルとしてビルド時にエラーになるのはおかしいはずだ。しかし何が原因でおかしくなるのかわからなかったので、いろいろ調べることになった。

最初はCIビルドでしか発生しなかったので環境の問題かと思ったが、少し調べたらエラーが出ているのはリリースビルドでのみだった。

Projucerが自動生成したapp/build.gradledebug_release_の差分を調べても(自動生成なので一般的なAndroidアプリに比べて冗長で無駄なオプションが多々含まれている)有意なものは見つからなかったので、app/CMakeLists.txtの中でDEBUG~RELEASE~のビルド構成の差分を調べたら、リリースビルドの時だけLDFLAGSに-fltoが指定されていたので、これを削ってみたらビルドが通るようになった

LTO (link time optimization)って何? というのはこの解説が参考になると思う: https://qiita.com/kaityo256/items/a822fc462a4de6ddd8e7

Projucerの<ANDROIDSTUDIO>ビルドの構成(Debug/Release)別オプションにlinkTimeOptimisation="1"というのがあったので、これを0にすれば対応としては十分。Projucer上もオプションがあるのでGUIだけでも設定できる。

ちなみにJUCE 6.0.7のProjucer上のデフォルト値は0なので、新規プロジェクトなら何も問題なくビルドできるだろう。

正しい仕様上の挙動なのか、JUCEの問題なのか、LLVMの問題なのか

問題の原因と対処方法はわかったのだけど、対処方法が適切なのかどうかは正直よくわからない。今回は外部ライブラリにJNI_OnLoad()が定義済みで、これと別にJUCEが内部的にJNI_OnLoad()を定義するのは問題とはいえないはずだ(そうでなければ「JNI_OnLoad()のシンボルを動的にロードできるが、それはライブラリに参照関係がある限り1つだけ」ということになってしまい、JNIの仕様そのものがかなりイマイチだということになる)。

もっともJUCE (on Android) の設計は、ビルドされるJUCEモジュールとアプリケーション全体でlibjuce-jni.so1つだけになるにもかかわらず、唯一のJNI_OnLoad()を食いつぶしてユーザーに追加処理の余地を与えない設計になっている点でイマイチなところがあるのは否定できない。でもそれはリンカーの振る舞いとは別の話だ。

clang -fltoの実際の処理はLLVMのllvm/lib/LTOで実装されているので、この中でのライブラリ解決のやり方に問題があるか、あるいはclangフロントエンドでのlibLTOのAPIへの引数の渡し方が問題なのであれば大まかにはLLVMの問題だし、-fltoでは不十分で追加のパラメーターなどが必要なのだとしたら、-fltoオプションしか生成していないProjucerの問題だ。

LTOの基本設計についてはドキュメントがまとめられているけど、この内容からは想定される挙動が判断しがたい。外部ライブラリのシンボルをどう扱うのかは書かれていないようだ。 https://www.llvm.org/docs/LinkTimeOptimization.html

試しにデスクトップでLTOによるリンクエラーを再現しようとしてみたのだけど、ビルドが失敗することはなかった。 https://gist.github.com/atsushieno/9116656290e1a028c0074cba1943a7ce

そういうわけで自分のC++力だと行き詰まってきたので、正解が分かる人がいたらコメント等で教えてもらえるとうれしいです。