既存のC/C++プロジェクトを、別のCPUアーキテクチャやOS向けへ持っていく作業をある程度自動化できないかなと思い、Miruriというツールを作っています。
GitHubはこちらです。
まだalpha版ですが、現在は、
- x86_64 → ARM64
- Linux → macOS
- macOS / Linux / Windows向けのビルド
- CMake
- Meson
- Autotools
- Make
- CPU固有Intrinsicの検出
- Linuxクロスコンパイル用sysrootの自動取得
- AIを使ったソースコードの移植・修正
あたりまで動くようになりました。
最近かなり面白いところまで動くようになってきたので、一度まとめてみます。
Miruriとは
一言で言うと、
既存のソフトウェアプロジェクトを解析して、別のCPUやOS向けにビルド可能な成果物へ変換するツール
です。
たとえば、
x86_64 Linux向けC/C++プロジェクト
↓
Miruri
↓
ARM64 Linux
や、
Linux向けソフトウェア
↓
Miruri
↓
Apple Silicon macOS
のようなことをやります。
単純に、
clang --target=aarch64-linux-gnu ...
を実行するだけのクロスコンパイルwrapperではありません。
実際のプロジェクトには、
- CPU固有Intrinsic
- OS固有API
- GUI framework
- graphics API
- shader
- audio
- input
- plugin
- architecture依存のcompiler flag
- build system固有処理
- 外部ライブラリ
などが混在しています。
Miruriではまずプロジェクト全体を解析して、
どこがarchitecture依存なのか
どこがOS依存なのか
そのまま再コンパイル可能なのか
source rewriteが必要なのか
compatibility layerが必要なのか
新しいplatform backendが必要なのか
を分離して考えるようにしています。
とりあえず使ってみる
Miruri本体はGoで書いています。
Go標準ライブラリだけでビルドでき、cgoは不要です。
git clone https://github.com/yuna-r/miruri.git
cd miruri
go build -o bin/miruri ./cmd/miruri
まず、
./bin/miruri doctor
で環境を確認できます。
利用可能なtargetは、
./bin/miruri targets
で確認できます。
簡単なプロジェクトなら、
./bin/miruri build \
--target macos-arm64 \
path/to/project
のように実行します。
AIによる移植もできる
単純なクロスビルドだけでは通らないプロジェクトの場合、Codex CLIを使ってソースコードやbuild scriptを修正できます。
たとえば、
./bin/miruri port \
--target macos-arm64 \
path/to/project
のようにします。
portは単純なコンパイルエラー修正だけではなく、
新しいplatform backend
GUI adapter
target-native entry point
build system branch
resource定義
compatibility code
などを作ることも許可するモードです。
内部的には、
既存ソース
↓
静的解析
↓
Target Contract生成
↓
移植strategy決定
↓
isolated workspace
↓
ビルド
↓
失敗
↓
診断情報をAIへ渡す
↓
source/build script修正
↓
再ビルド
↓
artifact検査
という流れになっています。
AIに全部丸投げするのではなく、
AIは修正案を作る
compiler/linker/artifact inspectorが結果を判定する
という役割分担にしています。
元のrepositoryは直接変更しない
このあたりはかなり気をつけています。
Miruriは元のrepositoryを直接編集せず、一度isolated workspaceへコピーしてから作業します。
つまり、
original repository
↓
isolated source overlay
↓
AI修正
↓
build
です。
修正内容はpatchとして保存できます。
また、
- compiler output
- object file
- executable
- cache
- generated build artifact
などがAIのsource patchに混ざらないようにもしています。
Linuxクロスコンパイル用sysrootも自動取得する
クロスコンパイルをやっていて面倒なのがsysrootです。
たとえばApple Silicon Macから、
Linux ARM64
のプログラムを作りたい場合、
/usr/include
/usr/lib
crt1.o
libc
libgcc
など、target側の開発環境が必要になります。
MiruriではLinux targetの場合、--sysrootを省略すると自動的に用意します。
./bin/miruri build \
--target linux-arm64 \
path/to/project
初回だけOCI Registryから対象architectureのrootfs layerを取得します。
ただしcontainerを実行するわけではありません。
OCI image
↓
manifest取得
↓
architecture選択
↓
layer download
↓
SHA-256検証
↓
tarとして展開
↓
sysrootとして利用
です。
そのため、
- Docker daemon
- Docker Desktop
- QEMU
- target CPUのエミュレーション
- target側package managerの実行
は不要です。
取得したsysrootはcontent-addressed cacheへ保存されます。
たとえば、
./bin/miruri sysroot ensure --target linux-arm64
で事前取得できます。
一覧は、
./bin/miruri sysroot list
です。
offlineでも、
./bin/miruri build \
--target linux-arm64 \
--offline \
path/to/project
のように、cache済みsysrootだけを使えます。
CMakeだけではなくMesonやAutotoolsにも対応した
実際のOSSを試していると、当然CMake以外も大量に出てきます。
現在は、
CMake
Meson
Autotools
Make
を自動判定します。
たとえば、
CMakeLists.txt
があればCMake、
meson.build
ならMeson、
configure.ac
configure.in
ならAutotools、
という具合です。
Autotoolsの場合、Git checkoutにはconfigureが入っていないこともあります。
その場合は、
configure.ac
↓
autoreconf -fi
↓
configure
↓
make
まで自動的に行います。
Mesonも、ホストにMesonが入っていない場合はMiruri管理下のruntimeを利用できるようにしています。
実際にOSSで試してみた
自作fixtureだけで成功してもあまり面白くないので、既存のOSSをいくつかMiruriへ投げています。
Autotools製のnudokuをmacOS ARM64へ
たとえば、
というterminal上で遊べる数独があります。
これはAutotoolsを使ったプロジェクトです。
最初はMiruriもAutotoolsに対応していなかったので、
no supported build system detected
detected: autotools
で止まりました。
そこでAutotools adapterを実装しました。
その後、
./bin/miruri port \
--target macos-arm64 \
~/src/nudoku
を実行したところ、Apple Silicon Mac上で普通に起動しました。
Linux/Unix向けAutotools project
↓
Miruri
↓
macOS arm64
↓
実行
まで確認できました。
terminal UIとはいえ、実際のOSSがそのまま動いたときは少し驚きました。
Linux/GNOME向けGUIアプリもMacで起動した
さらに面白かったのがこちらです。
DrawingというGNOME向けのペイントアプリがあります。
Python + GTK + Mesonで作られているLinuxデスクトップ向けアプリです。
これを、
./bin/miruri port \
--target macos-arm64 \
--codex-mode port \
~/src/drawing
としてみました。
当然、最初からすんなり動いたわけではありません。
実際には、
Meson未対応
↓
Meson adapter追加
Mesonがhostに無い
↓
managed Meson runtime追加
native binaryが生成されない
↓
interpreted Meson application packaging追加
macOS .appではない
↓
.app bundler追加
Python gettext差異
↓
macOS compatibility rewrite
PyGObjectが見つからない
↓
Homebrew Python runtime探索
Python package名がlauncherと衝突
↓
bundle entry point名を変更
GTK GSettings schema不足
↓
macOS GTK runtime側を調整
という感じで、実際のOSSを使いながらMiruri側の不足機能をかなり増やしました。
そして最終的に、
Drawing.app
としてmacOS上で起動しました。
しかもウィンドウが出るだけではなく、
- GTK UI
- 日本語表示
- キャンバス描画
- テキストツール
- フォント選択
- メニュー
- checkbox
- radio button
- マウス操作
まで普通に動きました。
Linux/GNOME向けGUIアプリがMacでそのまま開いたときは、さすがに自分でもちょっと笑いました。
Intel SSE IntrinsicもARM NEONへ変換してみた
もう一つ試したかったのがCPU固有Intrinsicです。
Intel SSEを直接使ったCコードを作りました。
たとえば、
#include <xmmintrin.h>
void simd_add4(
const float a[4],
const float b[4],
float out[4])
{
const __m128 va = _mm_loadu_ps(a);
const __m128 vb = _mm_loadu_ps(b);
const __m128 vr = _mm_add_ps(va, vb);
_mm_storeu_ps(out, vr);
}
です。
当然このままではARMでは使えません。
これをMiruriへ、
./bin/miruri port \
--target linux-arm64 \
--codex-mode port \
./intel-intrinsic-demo
としてみました。
するとARM側には、
const float32x4_t va = vld1q_f32(a);
const float32x4_t vb = vld1q_f32(b);
vst1q_f32(
out,
vaddq_f32(va, vb));
が生成されました。
つまり、
_mm_loadu_ps
↓
vld1q_f32
_mm_add_ps
↓
vaddq_f32
_mm_storeu_ps
↓
vst1q_f32
です。
さらに元のSSE pathは消さず、
#if defined(__SSE__)
/* x86 SSE implementation */
#elif defined(__aarch64__)
/* ARM NEON implementation */
#else
/* portable scalar implementation */
#endif
という構造になりました。
結果として、
Intel専用コード
↓
Miruri
↓
SSE + NEON + scalar
というportableな構造になりました。
これはかなり面白かったです。
単純なIntrinsic置換だけではない
特に面白かったのが浮動小数点演算です。
元コードに、
const __m128 product = _mm_mul_ps(va, vb);
const __m128 result = _mm_add_ps(product, vc);
という処理を入れていました。
ARMにはFMAがあるので、
vfmaq_f32(...)
へまとめることもできます。
ただしFMAにすると丸め回数が変わるため、元のSSEとbit単位で結果が変わる可能性があります。
生成されたNEON版では、
const float32x4_t product =
vmulq_f32(va, vb);
vst1q_f32(
out,
vaddq_f32(product, vc));
となり、multiplyとaddが分離されたままになりました。
horizontal reductionについても、単純に別のreduction instructionへ置換するのではなく、元のpairwise additionに近い順序を維持する形になりました。
こういうところを見ると、
CPU Intrinsicの移植は単純な関数名変換ではない
ということがよく分かります。
Project GraphとTarget Contract
Miruri内部では、プロジェクトをいきなり書き換えるのではなく、一度構造化します。
だいたい次のような考え方です。
Existing Project
│
▼
Project Graph
│
├─ source
├─ build steps
├─ resources
├─ shaders
├─ plugins
└─ capability requirements
│
▼
Target Contract
│
▼
Strategy Planner
│
├─ native rebuild
├─ source rewrite
├─ generated adapter
├─ compatibility runtime
├─ API translation
└─ unresolved blocker
│
▼
Isolated Artifact Builder
│
▼
Static Artifact Inspector
という感じです。
たとえば、
#include <xmmintrin.h>
を見つけたら、
cpu.x86.intrinsics
というCapabilityとして扱います。
Windows専用GUIなら、
platform.windows.gui
のような依存として扱えます。
こうしておくと、
コード中にif文を大量に増やす
だけではなく、
このプロジェクトがtargetで成立するために何が必要なのか
を別レイヤーで考えられます。
ビルドできたらartifactを検査する
Miruriでは「compilerが0で終了した」だけでは成功扱いにしません。
生成物について、
ELF
Mach-O
PE
static archive
などの形式を調べます。
さらに、
x86_64なのか
ARM64なのか
RISC-Vなのか
も確認します。
たとえばmacOS ARM64 targetなのにx86_64 Mach-Oが出てきた場合は、ビルド成功とはみなしません。
成果物と一緒に、
analysis.json
plan.json
build.log
manifest.json
も保存します。
managed sysrootを使った場合は、
sysroot.lock.json
も追加されます。
target artifactは勝手に実行しない
Miruri v0.1では、クロスターゲットとして生成したバイナリを自動実行しません。
たとえばM1 Mac上で、
Linux ARM64 ELF
を生成しても、そのELFをQEMUで実行してテストすることはしません。
現段階では、
解析
↓
移植
↓
compile
↓
link
↓
artifact architecture検証
までを担当します。
これは意図的です。
クロス環境で「実行できたから正しい」と判断すると、
- emulator差異
- syscall差異
- kernel差異
- driver差異
- GUI環境差異
などが一気に混ざるためです。
まずartifact生成までを独立して成立させています。
現在対応しているtarget
現在はtarget profileとして、
macOS
Linux
Windows
x86_64
ARM64
RISC-V
POWER
などを扱える構造になっています。
ただし、
profileが存在すること
実際にその組み合わせの全softwareを移植できること
は別です。
特に、
- Direct3D
- Vulkan
- Metal
- audio backend
- platform-specific GUI
- proprietary SDK
などは、まだ実装途中です。
まだできないこと
alpha版なので、当然まだできないこともかなりあります。
現在特に大きいのは、
- 生成成果物の実機動作保証
- GPU API間の完全自動translation
- shader bindingの完全変換
- proprietary SDKの自動取得
- あらゆるdependencyの自動解決
- 元実装と移植実装の意味論的等価性の証明
などです。
特に最後は難しい問題です。
たとえば、
SSE実装
と、
NEON実装
が数学的に近い結果を返すだけで良いのか、
bit exact
でなければいけないのかは、ソフトウェアによって違います。
HPCやcodecではかなり重要になると思います。
なぜ作ったのか
CPUやOSを跨ぐ移植作業をしていると、いつも同じような問題に遭遇します。
このヘッダはx86専用
このcompiler flagはARMでは使えない
このAPIはWindows専用
このGUI backendはLinux専用
このライブラリはtarget側にない
このbuild scriptはhost architectureを前提にしている
一つ一つは解決できることが多いです。
問題は、それを大きなproject全体について人間が追い続けるのがかなり面倒なことです。
そこで、
compilerやAIにコードを渡す前に、移植という問題そのものを構造化したい
と思ってMiruriを作り始めました。
最近のAIはコード修正能力がかなり高いですが、
コンパイルエラーを全部AIへ貼る
だけでは、大きな移植ではなかなか安定しません。
なのでMiruriでは、
architecture
OS
ABI
SDK
GUI
graphics
shader
audio
input
plugin
asset
などを先に分離し、その上で必要な部分だけをAIへ任せる方向にしています。
実際のOSSを投げるとツール側のバグが次々見つかる
開発していて一番面白かったのはこれです。
最初は、
CMakeのC projectがcross compileできた
ところから始まりました。
その後、実際のOSSを順番に入れていった結果、
Autotoolsがない
↓
追加
Mesonがない
↓
追加
Meson自体がhostにない
↓
managed runtime追加
Python appにはlinked binaryがない
↓
install-tree artifact対応
macOS .appにならない
↓
bundler追加
Linux Python launcherがmacOSで落ちる
↓
compatibility処理追加
GTKのruntime integrationが足りない
↓
macOS環境対応
という感じで、実物を入れるたびにMiruri自身の不足が見つかりました。
個人的にはこういう開発がかなり好きです。
fixtureだけだと想定した問題しか起きませんが、実際のOSSを入れると全然予想していないところで壊れます。
そして、その壊れ方のほうがだいたい重要だったりします。
今後やりたいこと
今後は特に、
Windows GUI
↓
macOS / Linux GUI
のようなfull platform portをもっと試したいと思っています。
あとは、
SSE / AVX / AVX2 / AVX-512
↓
NEON / SVE / SVE2
や、
x86
↓
RISC-V
x86
↓
POWER
のようなCPU backend移植も増やしていきたいです。
さらに、
- Direct3D → Vulkan / Metal
- shader translation
- audio backend translation
- input backend translation
- dependency resolver
- remote build worker
- semantic equivalence test
あたりも面白そうです。
まとめ
Miruriという、既存ソフトウェアを別architecture・OS向けへ移植して成果物を生成するツールを作っています。
GitHub:
現在は、
CMake
Meson
Autotools
Make
x86_64
ARM64
RISC-V
POWER
Linux
macOS
Windows
などを扱える基盤ができています。
実験では、
Intel SSE C code
↓
ARM NEON
や、
Autotools製terminal application
↓
Apple Silicon macOS
さらに、
Linux/GNOME向けPython + GTK GUI
↓
macOS .app
↓
実際にGUI起動
まで確認できました。
まだalpha版で、完成にはかなり遠いですが、
既存softwareを別platformへ持っていく作業を、どこまで機械化できるのか
を試しているプロジェクトです。
実際の古いC/C++プロジェクトや、変なarchitecture依存コードをMiruriへ投げるとかなり面白いので、興味があればぜひ遊んでみてください。