NdkBuildするC/C++コードにdefineマクロの値をProductFlavorで自動的に渡す


前提

cocos2dライブラリを使った結構古めのプロジェクトに、Product Flavor対応をしました。
が、一部C++のコードの中にもFlavorに依存して変えたい部分があり、対応方法を探しました。

条件としては以下があります。

  • cocos2d (version3.17.2)
    • かなり古いプロジェクトで上げることが出来ない
  • NDKのビルドにはndkBuildを使う
    • cmakeではなく、ndkBuildです

また、筆者はC/C++経験は長かったものの、既に離れて10年経過しており、そもそもmakeファイルはちゃんと書いたことがない(IDEに頼ってビルドしてきた)人間です。
その辺しっかり分かっている方には「今更何を」感があるかも知れませんが、ご容赦下さい。

現状

C++コード

Foo.cpp
#define SAMPE_ID=xxxxx


#if SAMMPLE_ID==xxxx
// ID=xxxxの時のコード
#elif SMAPLE_ID=yyyy
// ID=yyyyの時のコード
#elif SAMPLE_ID=zzzz
// ID=zzzzの時のコード
#endif

みたいな感じで、SAMPLE_IDに応じてdefineマクロでコードを変えていました。

Androidプロジェクト

Androidプロジェクト側は、同じくSAMPLE_IDに応じて、リソースファイルが差し替わるなどの対応があり、Product Flavorに対応させました。

build.gradle

    flavorDimensions "demo"

    productFlavors {
        sampleXXXX {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleX"
            versionCode 51

            manifestPlaceholders = [sampleScheme:"fooxxxx",
                                    appName:"XXXX cocosSample"]
        }
        sampleZZZZ {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleZ"
            versionCode 52

            manifestPlaceholders = [sampleScheme:"foozzzz",
                                    appName:"ZZZZ cocosSample"]
        }
    }

以前はリソースファイルをわざわざ別のフォルダからコピペしてきたりして切り替えていたのですが、これだけでも多少切替が楽になりました。

が、xxxxというFlavorでビルドしたとき、C++コードのSAMPLE_IDのdefineをxxxxとし、yyyyでビルドしたいときには、yyyyに書き変えるという手動コード書き換えが発生していました。

結果、Flavorを切り替えてビルドする際、Fioo.cppファイルの書き換え漏れを個人的に多発させてしまい、「アプリがちゃんと動かない!」と勝手に焦っていることが多くありました。

そこで、

「Product Flavorに追随すべき内容なのだから、Product Flavorの設定で出来る方法は無いか?」

と探した結果がこの記事です。

結論

結論から言うと、コンパイル時フラグ(cppFlags)を使うのですが、ここで前提であったcocos2d/ndkBuildを使っている、ということがネックになりハマりました。

cmakeでしたら、cppFlagにそのまま追加すれば良さそうなのですが、それをやるとcocos2dのビルドが失敗するようになりました。

build.gradle
    flavorDimensions "demo"

    productFlavors {
        sampleXXXX {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleX"
            versionCode 51

            manifestPlaceholders = [sampleScheme:"fooxxxx",
                                    appName:"XXXX cocosSample"]

            externalNativeBuild.ndkBuild {
                cppFlags += '-DSAMPLE_APP_ID=xxxx'
            }

        }
        sampleZZZZ {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleZ"
            versionCode 52

            manifestPlaceholders = [sampleScheme:"foozzzz",
                                    appName:"ZZZZ cocosSample"]

            externalNativeBuild.ndkBuild {
                cppFlags += '-DSAMPLE_APP_ID=zzzz'
            }
        }
    }

そう、我々が使っているのはndkBuildであり、Application.mkでコンパイルフラグなどは設定しており、build.gradlecppFlagsを設定すると、どうやらその内容を上書きしてしまうようなのです。

cocos2dでデフォルトで作成されるApplication.mkはこんな感じです。

Applicaton.mk
APP_STL := c++_static

# Uncomment this line to compile to armeabi-v7a, your application will run faster but support less devices
#APP_ABI := armeabi-v7a

APP_CPPFLAGS := -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -std=c++11 -fsigned-char -Wno-extern-c-compat
APP_LDFLAGS := -latomic

APP_ABI := armeabi-v7a
APP_SHORT_COMMANDS := true

USE_ARM_MODE := 1

ifeq ($(NDK_DEBUG),1)
  APP_CPPFLAGS += -DCOCOS2D_DEBUG=1
  APP_OPTIM := debug
else
  APP_CPPFLAGS += -DNDEBUG
  APP_OPTIM := release
endif

これで言うところの、APP_CPPFLAGSなんかをまるっと奪ってしまうようなんですね、build.gradlecppFlagsに追加すると。
そのためcocos2dのライブラリのビルドに失敗するということのようです。

解決策は、argumentsを使う、というものでした。

build.gradle
    flavorDimensions "demo"

    productFlavors {
        sampleXXXX {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleX"
            versionCode 51

            manifestPlaceholders = [sampleScheme:"fooxxxx",
                                    appName:"XXXX cocosSample"]

            externalNativeBuild.ndkBuild {
                arguments 'SAMPLE_APP_ID=xxxx'
            }

        }
        sampleZZZZ {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleZ"
            versionCode 52

            manifestPlaceholders = [sampleScheme:"foozzzz",
                                    appName:"ZZZZ cocosSample"]

            externalNativeBuild.ndkBuild {
                arguments 'SAMPLE_APP_ID=zzzz'
            }
        }
    }

argumentsで渡したものは、Application.mkで次のように参照してAPP_CPPFLAGSに追加します。

Applictaion.mk
ifneq ($(SAMPLE_ID),)
  APP_CPPFLAGS += -DSAMPLE_ID=$(SAMPLE_ID)
endif

なんかちょっと2度手間になってはいる気はしますが、argumentsで受け取った$(SAMPLE_ID)の値を、未設定でなければ、-DオプションでSAMPLE_IDのdefine値として渡しています。

で、Foo.cppの方からは、SAMPLE_IDの定義部分は削除してしまいます。

Foo.cpp
// #define SAMPE_ID=xxxxx // 削除

#if SAMMPLE_ID==xxxx
...

これで、ProductFlavorにFoo.cpp内のコードが一緒に連動して変わるようになりました。

恐らく最初から設計できるのであればもっとスマートな方法(targetsを使う方法とか)あるのでしょうが、現状のコードや仕組みを大きく変えること無く、コンパイルオプションで解決できたので、まずまずと思っています。

【おまけ】iOSアプリも同じことをしている場合

実は当該プロジェクトはFoo.cppファイルはAndroidとiOSとで共有しているコードです。
なので、iOSでも同じようにやりたくなります。

NDKでコンパイルオプションで解決できたので、iOSというかXcodeでも同じようにコンパイルオプションを指定すれば良いのは分かりますね。

Build SettingsとUser-Defined settingを使ってこんな風に実現しました。

  • コンパイルフラグ

Apple Clang - Custom Compiler Flags ※flagsで絞り込んだ方が探しやすいです

Other C++ Flags-DSAMPLE_ID=$(SAMPLE_ID)を追加する

  • ユーザー定義設定

User-Defined setting ※Build Settingsの一番下の方にあります

key=SAMPLE_ID, value=xxxxで作成する

あとはinfo.plistも実はこのSAMPLE_IDを使いたい部分があったのでこんな感じにしました。

info.plist
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <string>com.example.cocos2d.sample</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>foo${SAMPLE_ID}</string>
            </array>
        </dict>
    </array>

これで、User-DefinedSAMPLE_IDを書き変えるだけでFoo.cppの実装やinfo.plistの値を切り替えることが出来ました。

が、これだといちいちSAMPLE_IDの値を書き変えなければなりません。個人的に、この設定部分が使いづらくて良く意図しない場所を編集してしまうため、出来ればXcode上で触りたくありません。

Android StudioでBuild Variantsを切り替えるのと同じような、UIで切り替えられると便利です。

※本当はコマンドラインビルドのことも考えなければならないのですが、いったん担当者が違うので無視しています。

そこで気付いたのが"Target"です。

Targetごとに、User-Defindeの値だけを変えればいいんじゃない?

ということで、SAMPLE_IDの数だけ、Targetを作りました。

  • 10000の場合
  • 20000の場合

まとめ

  • AndroidのndkBuildでnative(C/C++)コードをProduct Flavorに追随させたい場合は、argumentsを使う。
  • cmakeならcppFlagsでやれるんじゃないかな?
    • cocos2dがデフォルトで持ってるmakeファイルをよく見てね
  • XcodeならTargetとBuild SettingsのOptional Custom Compiler Flags/User-Defined Settingを使うと同じような感じにできる

参考サイト