Zigで簡単クロスコンパイル 2022

僕は以下の3つのツールを複数プラットフォーム向けにクロスコンパイルしてバイナリ配布しており、以下のように全て異なる言語で開発している。

ロスコンパイルに苦労している話をするとZigを使ってみたらいいんじゃないかと言われることがあり、周りでもZigが何となく流行り始めた気がするので、これらのツールに実際自分で使ってみてどうだったかという事例を紹介したい。

Zigとは

Zigはそもそもプログラミング言語なのだが、C/C++とのinteropがやりやすい言語なようで、おそらくそれに必要でLLVMベースのC/C++ツールチェインが同梱されていて、しかもそれをDrop-In Replacement for GCC/Clangとして売りにしている。

僕はZig言語そのものにはそれほど興味はないのだが、クロスコンパイラとしての zig cc には強い興味があるので、この記事はその話だけする。

使い方

Go

そもそも前提として、Goは普通は以下のようにするだけでクロスコンパイルができる。

CGO_ENABLED=1 GOOS=[GOOS] GOARCH=[GOARCH] go build

問題はCのコードを連携するためにcgoを使う時で、本来cgoは避けるのが賢明なのだが、sqldefでPostgreSQLのパーサーを自分でメンテするのが大変になってきたので本家PostgreSQLのCのコードを連携できるライブラリを使うことにしたため、cgoが必要になってしまった。

一方、Zigを使ってクロスコンパイルするのは簡単で、上記の環境変数の他に、以下の環境変数を足して go build してやれば良い。target名は zig targets で調べられる。

CC="zig cc -target [target]"

GitHub Actions上でのZigのセットアップが簡単で、targetを設定しなくても使えるのは便利 *1

macOSでは動かない

問題なのはmacOSをターゲットにする場合で、MacOSX.sdkを自分で調達 *2 してきた上でこの記事に書いてあるように複雑なフラグを渡さないといけないのだが、そうしてがんばってリンクを成功させても、実行してみるとruntime.cgocallでクラッシュしてしまう。元々macOS向けのZigとcgoの相性はあまり良くなく、色々試したがこれを直すのは無理そうだなと結論づけた。

macOS向けのクロスコンパイルにはosxcrossというツールがあり、MacOSX.sdkを調達した後これを使ってclangクロスコンパイラをビルドしそれをCCに指定するとクラッシュしなくなった。

実はXcode 12.2+は元々クロスコンパイルに対応しているので、GitHub Actionsでmacos-latestを使うと、CCに何も指定しなくてもarm64向けのcgoクロスコンパイルができることをその後vim-jpで教わり、そのようにした。現状macOS向けのクロスコンパイルはこれが一番簡単そう。

Rust

Goはlibcにあたる部分も自前で書いているためcgoを使わない限りはクロスコンパイルが通りやすいのだが、Goとは違ってRustは普通にビルドするとlibcへの依存が発生するため、自分ではネイティブ環境への変な依存を作っていないつもりでもstaticなバイナリの作り方が自明ではなく、クロスコンパイルも通しにくかったりする。

"Just Work" してくれない

Zig Makes Rust Cross-compilation Just Work という記事があり、題名の通りZigを使うとRustのクロスコンパイルがやりやすくなるそうなので、やってみた。しかし、実際にはglibcを使う場合もmusl-libcを使う場合もlibcに関係するシンボルがconflictしてしまう。この問題はRust + zig cc CRT conflict.というスレで解説されているが、先ほどの記事でうまくいってるのはlibcがダイナミックリンクされるmacOSを対象にクロスコンパイルしているからで、LinuxWindowsだとそうならないためZigが定義するシンボルと衝突するらしい。

cross

仕方ないのでcrossという既存のクロスコンパイルツールを使ったらすんなり動いたので、xremapはcrossを使うことにした。以下のようにするだけなので簡単。

cargo install cross
cross build --target=[target]

cargo-zigbuild

その後rust-lang-jpでcargo-zigbuildというのを教わった。これを使えばZigを使ったクロスコンパイルもうまくいくかもしれない。わざわざcrossから移行するモチベーションはないが、次クロスコンパイルを新たに設定する時には試してみようと思っている。

mruby

mrubyはよく使うライブラリが大体Cのコードに依存しており、唯一のクロスコンパイルツールチェインも5年前にメンテがストップしているので、mrubyのクロスコンパイルは地獄が約束されている。クロスコンパイル用の設定のAPIはあるのだが、そこに渡すクロスコンパイラは自分で調達しなければならないからだ。

そこでZigの出番である。良くも悪くもmrubyはCとの親和性が高いというか、単にCのコードとCで書かれたVMで動くmrubyのバイトコードの集まりなので、Cのクロスコンパイラを渡すインターフェースはGoやRustより充実している。以下のように設定すると動く。

MRuby::CrossBuild.new('[target]') do |conf|
  [conf.cc, conf.linker].each do |cc|
    cc.command = 'zig cc -target [target]'
  end
  conf.archiver.command = 'zig ar'

  # mrbgems/mruby-yaml や mattn/mruby-onig-regexp にはこれも必要
  conf.host_target = '[target]'
end

mitamaeでは元々dockcrossを使ってWindowsLinuxのバイナリをクロスコンパイルし、osxcrossを使ってmacOSのバイナリをクロスコンパイルしていたのだが、前者はDockerコンテナのメンテ、後者はMacOSX.sdkの調達が面倒だったので、今回の変更でZigをいれるだけで動くようになったのは便利。GoやRustに比べ、mrubyと一緒に使う方が親和性が高く感じた。

macOSではzig ranlibの指定が必要

一つだけ注意点としては、macOS向けのビルドはranlibをzig ranlibに差し替えないと動かない感じがしており、ranlibを指定するAPIがmrubyになさそうなので、 ENV['RANLIB'] ||= 'zig ranlib' などで無理やり子プロセスに渡るようにしておく必要がある。この設定はどのtargetでも動くので、そこまで困りはしなさそう。

まとめ

動く環境が限られていたり、特定の言語に向けて設計されたツールと違って連携方法が自明ではなかったりするので、現時点ではそれほど使いやすくはないというのが正直なところ。

一方、どれも解決は可能な問題に見えるし、実際過去に他の言語のツールに修正を取り込ませている様子も見えるので、今後便利になると期待している。

*1:デフォルトのバージョンだとcgoと連携する時にバグがあって動かないので、今は明示的に新しいZigのバージョンを指定する必要があることに注意。それから、Zigに対応するためにはGo側も1.18以上にしないといけない https://github.com/golang/go/issues/43078

*2:https://github.com/phracker/MacOSX-SDKs が便利