見出し画像

Android ndkbuildしてるC++ライブラリを、CMakeでデバッガブル導入

こんにちは、初代クワマンです。
ナビタイムジャパンの研究開発部門にて、地図フレームワークエンジニアを担当しています。

Android nativeビルドはandroid.mkとndkbuildで行われてきました。この手法はデバッグの資料に乏しく、あくまでデバッグログを出力するに留まっていました。
しかし、2016年4月、Android Studio 2.1 より、CMakeLists.txtとCMakeでのビルドがサポート・推奨され、nativeコードのデバッグがデフォルトで使用できるようになりました。
それから、8年……
みなさん、ndkbuildからCMakeへ移行し、Android Studioを通してのnativeコードのデバッグはできていますでしょうか?

……Androidの地図フレームワークは、つい昨年まではnativeコードのデバッグはできていませんでした。
今回は、このndkbuildしていたC++nativeコードを、CMakeでデバッガブルにした実績についてお話しします。



今、nativeコードをデバッガブルにする理由

  • 開発時間を確保するため

  • 確認・修正を容易にするため

この2点に尽きます。
動いているから良いのでは・・・?と感じられるかもしれませんが、
nativeコード変更・改修からのndkbuild と言うのは、そのnative領域の大きさ、複雑度に応じてbuild時間がすごいことになってしまいます。

どれくらいすごいことになるかというと、ndkbuildする領域の1依存プロジェクトをgit pull して更新するだけで、30分 ~ 1時間、CPUが熱され、ファンが高速回転する音を聞き続ける羽目になります。

これが、native下のバグ調査やれ機能追加やれの度に走ると考えると、
時間的損失は計り知れません・・・。

また、このバグ調査やれ機能追加やれについても困難が待ち受けています。
AndroidStudioでの開発では、ndkbuildしたライブラリ内部のコードを走査して、デバッグすることが非常に難しいです。
(資料が極端に少なく・・・)


エラーを確認するためには、
当たりをつけて、Logを張り、フラグを弄り、ndkbuildして、実行して、ダメで、
当たりをつけて、Logを張り、フラグを弄り、ndkbuildして……
1時間経過しました。
native開発・修正をしている最中はこんな感じで時間が過ぎていきます。

……開発を、修正をしたいはずなのに、なぜ調査で1時間も……
と、度々思い悩むようになっていたため、CMakeでのデバッガブル環境に手を出した次第です。
では、実際に行った移行作業について、言及していきましょう。


容易ならざる移行作業

実際に移行するにあたって行う作業は、大きく分けて5つです。

  1. 依存関係の整理

  2. ファイル、ディレクトリの移行

  3. android.mk から CMakeLists.txtへの移行

  4. build.gradleの構成変更

  5. ndkbuild環境のso等リソース削除

このうち、2~4については、
Android Developersの「Add C and C++ code to your project」のページ
に則った作業をしています。
公式のドキュメントを介して作業したい、という方は上記を参考に行なってください。
それでは、それぞれ、確認していきます。


移行1.依存関係の整理

まず、ndkbuildしてるC++ライブラリが移行できるか?
という点から、依存関係を確認、整理する必要があります。
端的に言うと、移行対象のプロジェクトに依存してるもの全て、2 ~ 5 の移行作業をしなければなりません。
そうしなければ、依存が解決できないので・・・。

幸い、地図フレームワークを参照しているものは1つと少なく、参照しているプロジェクトも移行を行えば問題ないと判断し、以下の作業を行いました。


移行2.ファイル、ディレクトリの移行

個別に入れているにしろ、まとめて入れているにしろ、ndkのビルド、読み込みを実行しているディレクトリパスから、androidプロジェクトがビルドできるパスへと変更する必要があります。
先ほどのAndroid Developersの「Add C and C++ code to your project」のページに従うなら、下記のようにjava ディレクトリのお隣に cppディレクトリを作成して入れるイメージです。

  • [PROJECT_ROOT]/~~~/---/

    • java

    • cpp

      • [Library_root]

      • CMakeList.txt

ディレクトリ構成

移行3.android.mk から CMakeLists.txtへの移行

ディレクトリ構成が変化するため、android.mk をベタ移植することはできません。
CMakeList.txtの作法に従って、書き換えを行います。
実際に移行前後の android.mkとCMakeLists.txtを見比べてみましょう。

// android.mk [Top Diretory]
include $(call all-subdir-makefiles)
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// android.mk [Project Directory]
LOCAL_PATH			:=	$(call my-dir)
SEARCH_PATH	        :=	$(LOCAL_PATH)
include $(call all-subdir-makefiles)
include $(CLEAR_VARS)

LOCAL_PATH			:=	$(SEARCH_PATH)

LOCAL_MODULE    	:=	[NameOfProject] \

LOCAL_SRC_FILES		:=	AnyFile.cpp \ 
						[AnyDir]/OtherFile.cpp

LOCAL_CFLAGS		+=	$(LOCAL_C_INCLUDES:%=-I%)

LOCAL_CPPFLAGS		+= -std=XXXXXX

LOCAL_LDLIBS		:=	-llog -lz

include $(BUILD_SHARED_LIBRARY)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 以下、必要なプロジェクトごとにディレクトリとandroid.mkを構築
// android.mk [some Project Directory]

上記のandroid.mkを下記のように書き換えます

// CMakeLists.txt [Top Directory]
cmake_minimum_required(VERSION X.Y.Z)

// ~~~ プロジェクトごとの設定 ~~~
project(NameOfProject)
find_library(log-lib log)
find_library(z-lib z)

#共通の.soファイル参照パスを指定
link_directories(
        ${PROJECT_SOURCE_DIR}/[SoDirectory]/${ANDROID_ABI}
)

#共通の.hファイル参照パスを指定
include_directories(
        ${PROJECT_SOURCE_DIR}/[HeaderDirectory]/include
)

# NameOfProjectプロジェクト 
# コンパイル対象を指定
add_library(NameOfProject SHARED
        [ProjectDirectory]/AnyFile.cpp
        [ProjectDirectory]/[AnyDir]/OtherFile.cpp)

#コンパイル時の設定を指定
target_compile_features(NameOfProject PUBLIC cxx_std_11)

#コンパイル時のオプション設定を指定
target_compile_options(NameOfProject PUBLIC -O3 -D_PLATFORM_ANDROID)

#対象となる.hディレクトリを指定
target_include_directories(
        NameOfProject
        SYSTEM
        PUBLIC
        [NameOfProjectDir]
        [OtherDir]
)

#対象となる.soファイルを指定(先頭のLibは省略)
target_link_libraries(
        NameOfProject
        PUBLIC
        ${log-lib}
        ${z-lib}
        android
)

# ~~~ 他、必要なプロジェクトごとに設定を追記 ~~~

注意事項
android アーキテクチャ固有の設定 neonのcompileフラグを指定する場合は、下記のように指定してください。

if(CMAKE_ANDROID_ARCH_ABI STREQUAL "armeabi-v7a")
    set_source_files_properties(neon_src/util.cpp PROPERTIES COMPILE_FLAGS "-mfpu=neon")
endif()

移行4.build.gradleの追記

CMakeでのビルドを行うため、build.gradleでの各種指定を行います。
・android.externalNativeBuild.cmake path file() の指定
・android.defaultConfig.ndk.abiFilters の指定
・android.ndkVersionの指定

// build.gradle (Project本体)
android {
    externalNativeBuild {
        cmake {
            path file('~~~/---/CMakeLists.txt')
        }
    }

    defaultConfig {
        ndk {
            abiFilters 'arm64-v8a', 'x86_64'
        }
    }
    ndkVersion "X.Y.ZZZZZZZZ"
}

注意事項
ndkVersionの指定は、トッププロジェクトのbuild.gradleに上書きされます
(記載が無い場合は、デフォルトのndkが使われます……)

他に上位のプロジェクトがある場合は、下記をそのプロジェクトのbuild.gradleに記載すると、ndkVersionを指定することができます。

// build.gragle (トッププロジェクト)
// ~~~~~~~~~~~~~~~~~~
subprojects {
    afterEvaluate {
        if(it.hasProperty('android')){
            android {
                ndkVersion "X.Y.ZZZZZZZZ"
            }
        }
    }
}

移行5.ndkbuild環境のso等リソース削除

移行自体は以上で終わりましたが、移行したプロジェクトのso等がndkbuildの成果物を置くパスに残っていると、競合が起こってしまいます。
もし、soやheader等がまだndkbuildの環境にある場合は、削除しておきましょう。


Android Studio でnativeデバッグできる、喜び

ここまでで移行は完了しました。
実際に動作を見てみましょう。
今までndkbuildをする際にコンソールやターミナル等で流れていたログが、Android StudioのBuildログに掲示されます。

初回は全ビルドが走りますが、次回からは変更を施した箇所と、IF依存する箇所のみがビルドされることでしょう。素晴らしい。

build風景

ビルド後はエラーも表示され、なんならハイライトまでしてくれます。

エラーのハイライト表示

さらには、JNIメソッドの生成もサポートされています。

JNIメソッド作成
JNIメソッド作成場所の選択
作成されたJNIメソッドのテンプレート

これら、ビルド時間にして、数分。快適な開発環境が手に入りました。
実に素晴らしい・・・。


残る課題

とはいえ、ndkbuildがCMakeに変わっても依然変わらぬ点や、
困った点は残ります。

  1. JNIメソッドが無くても通ってしまうコンパイル

    • JNIメソッドの定義が.h/.cpp側になかったとしても、それはそれでコンパイルは通ってしまいます。
      動作確認時にわかることですが、思わぬ地雷になるのでご注意を。

  2. AndroidStudioでのFile RenameがCMakeLists.txtには届かない

    • CMakeLists.txtは独立した概念らしく、.cpp/.h の FileRenameをIDE上でかけた場合はCMakeLists.txtまでは反映されません。
      別途、CMakeLists.txtへの書き換えを忘れずに行う必要があります。

  3. 依存関係の複雑化

    • 一部ライブラリがプロジェクト内部に入り込むため、Headerやsoの参照に、相対パスで複雑な参照を余儀なくされることも・・・。

これらは、地道に対処していくしかなさそう、というのが現状です。


以上になります。
皆様も、良いレガシー環境脱却が実現されんことを。
May fortune be with NEW.