Haxeから生成したC++コードをiOSプロジェクト(Xcode)で直接ビルドする
Haxeで書いた共通ロジックをiOSアプリに取り込む際、通常はhxcppでスタティックライブラリ(.a)を作成してリンクするのが一般的だ。しかし、これではビルドプロセスが分離され、何よりコードを変更するたびにライブラリをビルドし直す手間が発生する。
さらに、Apple Silicon環境下では非常に厄介な問題に直面する。実機もシミュレータも同じarm64アーキテクチャであるため、hxcppで普通にビルドするとスタティックライブラリのプラットフォームID(LC_BUILD_VERSION)が混同されてしまうのだ。
otool -lV で確認するとわかるが、アーキテクチャが同じであっても、プラットフォームIDが ios なのか ios-simulator なのかが正しく設定されていないと、Xcodeはリンク時に「実機用のバイナリをシミュレータで使おうとしている」と判断してリンクを弾いてしまう。これを正しく作り分けるのは非常に煩雑だ。これらの問題を回避し、コード変更を即座に反映させるため、今回はXcode側でソースコードを直接管理・ビルドする方式を選択した。
しかし、これを実現するのは想像以上に苦難の道だった。丸三日かかってようやくXcode上で直接ビルド・実行できるようになった。その過程で得た知見を、未来の自分のための備忘録として残しておく。
Haxe/hxcppのビルドの仕組み
まず前提として、HaxeからC++ターゲットへ出力する際の挙動を理解しておく必要がある。
build-ios.hxml
Haxeのビルド設定ファイル(.hxml)では、以下のように指定する。
-cp src
-cpp gen/ios
-D no-compilation
-D iphone
-D iphoneos
-D objc
domain.model.Greeting
ここで重要なのは -D no-compilation だ。通常、HaxeはC++コードを生成した後に、内部で hxcpp を呼び出してコンパイルまで行おうとする。しかし、今回はコンパイルをXcode側に委ねたいため、生成のみを行うように設定している。
Build.xml
HaxeがC++コードを生成すると、出力ディレクトリに Build.xml というファイルが作られる。これは hxcpp が使用する独自ビルドシステムの構成ファイルだ。
本来、ライブラリをビルドする場合は haxelib run hxcpp Build.xml のように実行するが、今回はこの中身を読み解き、そこに記載されているコンパイルオプションや依存ライブラリ、プリプロセッサ定義を Xcodeのプロジェクト設定へ手動で移植する という作業が必要になる。
結論:Xcodeへの統合
hxcppが生成したソースコードを単にXcodeに突っ込むだけでは動かない。以下の対応が必要だ。
1. プリプロセッサマクロと検索パスの設定
Xcodeの GCC_PREPROCESSOR_DEFINITIONS に、hxcppのビルドに必要なマクロを網羅する。特に HXCPP_OBJC を忘れると、ARC環境下でのビルドが困難になる。
📌 設定すべき主なマクロ
HXCPP_API_LEVEL=430
HXCPP_ARM64=1
HXCPP_CHECK_POINTER=1
HXCPP_CPP11=1
HXCPP_M64=1
HXCPP_OBJC=1
HXCPP_VISIT_ALLOCS=1
HX_SMART_STRINGS=1
IPHONE=1
IPHONEOS=1
OBJC_ARC=1
また、HEADER_SEARCH_PATHS には以下の3点を含める。
../Haxe/gen/ios/include/**(Haxeが生成したヘッダ)haxelib/hxcpp/X.X.X/include/**(hxcpp自体のヘッダ)haxelib/hxcpp/X.X.X/project/thirdparty/pcre2-X.X.X/src/**(hxcppに同梱されているPCRE2)
2. ファイル管理と同期(Folder Sharing)
大量の生成ファイルを効率よくXcodeで扱うため、今回はプロジェクトファイルの PBXFileSystemSynchronizedRootGroup を活用して、生成ディレクトリを同期対象とした。
しかし、単純に同期するだけでは不都合がある。例えば、hxcppが生成する Build.xml や Options.txt、あるいはエントリーポイント用の __main__.mm など、Xcodeのビルドプロセスには含めたくない(あるいは個別に制御したい)ファイルが存在するからだ。
これらは、PBXFileSystemSynchronizedBuildFileExceptionSet の membershipExceptions に指定することで、同期グループ内にありながらビルド対象から除外するといった細やかな制御が必要になる。
また、既存の PBXGroup との使い分けも重要だ。静的なランタイムソースは通常のグループで管理し、動的に生成されるHaxeコードは同期グループで管理するといった構成に落ち着いた。
3. hxcppランタイムソースと依存ライブラリの同梱
hxcppでライブラリをビルドしない場合、本来そのライブラリに含まれるはずのランタイム(GCや基本型の実装)や、Haxeが依存するPCRE2などのソースを自前でXcodeのビルド対象に含めなければならない。
以下のファイルを haxelib/hxcpp/src/ から探し出し、Xcodeの Sources に追加する。
hx/Anon.cpp,hx/Object.cpp,hx/Boot.cpp,hx/Class.cpphx/Thread.cpp,hx/RunLibs.cpp,hx/Hash.cpp,hx/Interface.cppArray.cpp,String.cpp- GC(Immixなど)の実装ファイル
さらに、PCRE2のソースコードもプロジェクトに含める必要がある。これらは pcre2_8_bundle.c などのバンドルファイルを作成し、そこから必要な .c ファイルを #include することで管理しやすくなる。
⚠️
hxcppのCFFI.cppなどをそのまま含めると、String型のリンク不整合エラーが発生することがある。必要最低限の関数(hxcpp_alloc_kindなど)だけを抽出したスタブファイルを用意して対応した。
振り返り
正直、iOS側でここまでの苦労を強いるとは思わなかった。Haxeの「どこでも動く」という看板の裏には、こうした泥臭いツールチェーンとの戦いがある。
一方、Android側は拍子抜けするほど簡単だった。出力結果のディレクトリをSource Rootに追加するだけで、あとはビルドシステムがよしなにやってくれた。iOS(Xcode)のビルド設定の複雑さと、それに伴うhxcppの職人芸的なマクロ制御を改めて実感することになった。
Xcode上で直接ソースを管理する最大のメリットは、ライブラリの再ビルドを待つ必要がなく、Haxe側のコード変更を即座にアプリに反映できる開発サイクルにある。デバッガで生成されたC++コードを追うのは現実的ではないが、この「変更即反映」の恩恵は非常に大きい。