--- title: "ZigはCMakeの代替となるか" date: '2022-08-08' isPublished: true lang: 'ja' --- # はじめに 引き続き[Zig](https://ziglang.org/)を触っている。 /blog/2022-07-20-zenn-4fc7570643339d-ja https://github.com/ryoppippi/nyancat.zig 今回はC/C++ toolchainとしてのZigについて書いていく。 # Zig as a C/C++ Toolchain まず、ZigはCコンパイラでもある。 これが何を意味するかといえば - ZigプロジェクトでC/C++を利用できる - C/C++プロジェクトをZigでコンパイルできる というわけで、C/C++プロジェクトにZigを導入すると嬉しいことを列挙していく。 ## コンパクトかつ何でも屋のToolchain Zig Toolchainはコンパイラ、ビルドシステム、リンカ、標準ライブラリを含んでいる。 また、標準でクロスコンパイルにも対応している。 これらのツールが全部含まれているのにも関わらず、容量なんと40MBほど。とても小さい。 このように、Zig toolchainはビルド環境としては極めて導入が簡単であると言える。 ## gcc/clangコマンドを置き換えてみる ```sh ❯ gcc main.c -o main ❯ zig cc main.c -o main ❯ ./main Hello world ``` 既存のプロジェクトで使用しているコンパイラを置き換えるだけで、Zigに付属しているCコンパイラを利用できる。 ## クロスビルドが標準で可能 上でも述べた通り、Zigは標準でクロスコンパイルが可能である。
Zig libcのTarget一覧 ```sh ❯ zig targets | jq ".libc" [ "aarch64_be-linux-gnu", "aarch64_be-linux-musl", "aarch64_be-windows-gnu", "aarch64-linux-gnu", "aarch64-linux-musl", "aarch64-windows-gnu", "aarch64-macos-none", "aarch64-macos-none", "armeb-linux-gnueabi", "armeb-linux-gnueabihf", "armeb-linux-musleabi", "armeb-linux-musleabihf", "armeb-windows-gnu", "arm-linux-gnueabi", "arm-linux-gnueabihf", "arm-linux-musleabi", "arm-linux-musleabihf", "thumb-linux-gnueabi", "thumb-linux-gnueabihf", "thumb-linux-musleabi", "thumb-linux-musleabihf", "arm-windows-gnu", "csky-linux-gnueabi", "csky-linux-gnueabihf", "i386-linux-gnu", "i386-linux-musl", "i386-windows-gnu", "m68k-linux-gnu", "m68k-linux-musl", "mips64el-linux-gnuabi64", "mips64el-linux-gnuabin32", "mips64el-linux-musl", "mips64-linux-gnuabi64", "mips64-linux-gnuabin32", "mips64-linux-musl", "mipsel-linux-gnueabi", "mipsel-linux-gnueabihf", "mipsel-linux-musl", "mips-linux-gnueabi", "mips-linux-gnueabihf", "mips-linux-musl", "powerpc64le-linux-gnu", "powerpc64le-linux-musl", "powerpc64-linux-gnu", "powerpc64-linux-musl", "powerpc-linux-gnueabi", "powerpc-linux-gnueabihf", "powerpc-linux-musl", "riscv64-linux-gnu", "riscv64-linux-musl", "s390x-linux-gnu", "s390x-linux-musl", "sparc-linux-gnu", "sparc64-linux-gnu", "wasm32-freestanding-musl", "wasm32-wasi-musl", "x86_64-linux-gnu", "x86_64-linux-gnux32", "x86_64-linux-musl", "x86_64-windows-gnu", "x86_64-macos-none", "x86_64-macos-none", "x86_64-macos-none" ] ```
C言語をコンパイルするときには、コンパイラは生成するバイナリを標準ライブラリであるlibcとリンクする必要がある。 Zig toolchainには複数ターゲットのlibcが含まれていて、生成するバイナリにそれらが埋め込まれるため(静的ビルドされるため)、ターゲット先で依存ライブラリを導入する必要がない。 とてもポータブルかつクロスプラットフォームなバイナリを生成することができる。 ```sh ❯ zig cc main.c -o main --target=aarch64-linux-musl ❯ docker run -it --rm -v $(pwd):/data -w /data alpine:3.16 ./main Hello world ``` 先に述べた通り、C/C++を使うプロジェクトならコンパイラはどこかで使っているはずなので、既存のコンパイラを置き換えるだけでクロスビルドが可能なのはとても便利。 ## キャッシュシステムが優秀 Zigコンパイラは一度ビルドを行うと、その結果をキャッシュに保存する。 そのため、2回目以降のビルドは高速化される。 ## ビルドシステム全体を置き換えることができる? さて、より依存関係の多いプロジェクトについて考えてみよう。 現代の標準的なプロジェクトでは`make`や`CMake`、`bazel`といったビルドシステムがよく用いられる。 ここでは筆者が普段よく触れている`CMake`と比較する。 比較のために、[Egien](https://eigen.tuxfamily.org/index.php?title=Main_Page)と[Spectra](https://spectralib.org)を用いた簡単なC++プロジェクトを作成した。 https://github.com/ryoppippi/cpp-zig-build-system-demo/tree/9820e84775f0d11da81999712b88f4e5522cd371 ### CMake 現代の`CMake`はさまざまなコマンドを`CMakeLists.txt`に記述することで依存するファイルやライブラリを解決することができる。 また、`find_package`や`ExternalProject_Add`,`execute_process`を駆使することでライブラリを探したり外部コマンドを実行することができる。 C/C++プロジェクトには必須のツールと言って良いだろう。 ただしいくつか問題はある。 例えば、`CMakeLists.txt`は設定ファイルである都合上、小回りが利かなかったり独自の記法、御作法に戸惑うことも多い。 またビルドプロセスを完遂するためには`CMake`だけではなく、コンパイラを別途導入する必要があることはもちろん、`Make`や`Ninja`等のツール、場合によってはシェルスクリプトなど複数のツールを駆使しなければならない^[CMakeLists.txtやMakefile、Configure.shなどいくつもの設定ファイルが含まれたプロジェクトをみたことがあるはずである]。 ```cmake cmake_minimum_required(VERSION 3.5) include(ExternalProject) enable_language(Fortran) set(CMAKE_CXX_STANDARD 14) set(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}") find_package(Git QUIET) if(GIT_FOUND AND EXISTS "${PROJECT_ROOT}/.git") execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive WORKING_DIRECTORY ${PROJECT_ROOT} RESULT_VARIABLE GIT_SUBMOD_RESULT) if(NOT GIT_SUBMOD_RESULT EQUAL "0") message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") endif() endif() if(APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -framework Accelerate") endif() message(${CMAKE_HOST_SYSTEM_NAME}) message(${CMAKE_SOURCE_DIR}) message("${cmake_current_source_dir}") find_package(BLAS) if(BLAS_FOUND) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${BLAS_INCLUDE_DIR} -DEIGEN_USE_BLAS") endif() set(third_PARTY_DIR "${PROJECT_ROOT}/third_party") set(EIGEN3_INCLUDE_DIRS "${third_PARTY_DIR}/eigen") set(SPECTRA_INCLUDE_DIRS "${third_PARTY_DIR}/spectra/include") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEIGEN_FAST_MATH=1 -DEIGEN_NO_DEBUG -DTHREAD_SAFE") project(sample CXX ) add_executable(main ${PROJECT_ROOT}/src/main.cpp) target_include_directories(main PUBLIC ${EIGEN3_INCLUDE_DIRS} ${SPECTRA_INCLUDE_DIRS} ) target_link_libraries(main m blas ${BLAS_LIBRARIES}) message("${CMAKE_CXX_FLAGS}") ``` ```sh mkdir build cd build cmake .. make ./build/main ``` ### build.zig Zigではビルドの設定を`build.zig`に書くことができる。 `build.zig`ではZigだけでなく、C/C++のビルド設定を記述できる。 中身はZigのコードなので、関数を用いて操作を分割したり、外部コマンド実行や条件分岐を記述することで読みやすく記述しやすいビルドファイルが出来上がる(個人の感想)。 また、実行も1つのコマンド実行で済む。 さらに、クロスコンパイルも可能である。 ただし、CMakeの`find_package`に相当する機能が未実装なため、全てを置き換えることはできなかった(BLASなどの外部ライブラリをリンクをすることはもちろんできるものの、リンクできなかった場合は単にビルドが失敗するだけである)。→ [追記](#追記)参照 ```zig const std = @import("std"); const Builder = std.build.Builder; pub fn build(b: *Builder) void { const target = b.standardTargetOptions(.{}); const mode = b.standardReleaseOptions(); ensureSubmodules(b.allocator) catch |err| @panic(@errorName(err)); const exe = b.addExecutable("main", null); exe.setTarget(target); exe.setBuildMode(mode); exe.addCSourceFile("src/main.cpp", &[_][]const u8{}); exe.addIncludeDir("third_party/eigen"); exe.addIncludeDir("third_party/spectra/include"); exe.defineCMacro("EIGEN_FAST_MATH", "1"); exe.defineCMacro("THREAD_SAFE", ""); exe.linkSystemLibrary("m"); if (target.isNative()) { exe.defineCMacro("EIGEN_USE_BLAS", ""); exe.linkSystemLibrary("blas"); if (target.isDarwin()) { exe.linkFramework("Accelerate"); } } if (b.is_release) { exe.defineCMacro("EIGEN_NO_DEBUG", ""); } exe.linkLibCpp(); exe.install(); const run_cmd = exe.run(); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); } fn ensureSubmodules(allocator: std.mem.Allocator) !void { if (std.process.getEnvVarOwned(allocator, "NO_ENSURE_SUBMODULES")) |no_ensure_submodules| { if (std.mem.eql(u8, no_ensure_submodules, "true")) return; } else |_| {} var child = std.ChildProcess.init(&.{ "git", "submodule", "update", "--init", "--recursive" }, allocator); child.cwd = (comptime thisDir()); child.stderr = std.io.getStdErr(); child.stdout = std.io.getStdOut(); _ = try child.spawnAndWait(); } fn thisDir() []const u8 { return std.fs.path.dirname(@src().file) orelse "."; } ``` ```sh zig build run ``` ## Zigとの連携 この記事では触れないが、ここまで来れば既存のC/C++のコードをZigのコードから呼び出すこと、あるいはその逆も簡単にできる。 またZig←→Cのトランスコンパイルも容易である。 詳しくは以下の記事を参照してほしい。 https://ziglang.org/ja/learn/overview/#c言語コードに依存する関数変数型のエクスポート # 実用例 実際UberではZigをC/C++ toolchainとして使っているようだ。 CGOをクロスコンパイルする環境として利用しているようである。 (Zig言語自体はまだ利用していないとのこと) https://jakstys.lt/2022/how-uber-uses-zig/ またDenoの拡張をクロスビルドするのにZig CCを使った例もある。 https://github.com/mattn/deno-expandhome/blob/main/.github/workflows/release.yaml#L9-L29 https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html # 個人的所感 - ZigのエコシステムにC/C++のプロジェクトを組み込むことで、さまざまな恩恵を得られる予感がする。 - 個人的には設定が書きやすく感じた。個人プロジェクトではCMakeを置き換えていくかもしれない。 - クロスコンパイルは便利。例えばラズパイ用のビルドをqemuを経由しないで母艦で行うことができるのはとても良い。 - OpenMPやBLAS等のlibcに含まれないかつOSやハードに強く依存するライブラリはクロスコンパイルと相性が悪い。今後に期待(追記:ネイティブビルドは大丈夫そう)。 # 追記 以前この記事を書いた時に、`find_package`が動かないと判断した理由は、OpenMPがうまくリンクできなかったためである。 しかし、これはincludeパスが通っていなかったせいであり、きちんとパスを通せばコンパイルできた。 ただ依然として、ライブラリが見つからなかった場合に何か条件分岐を実行することは現時点ではできなそうである。 https://github.com/ryoppippi/cpp-zig-build-system-demo/compare/9820e84775f0d11da81999712b88f4e5522cd371...0b9b19eb35d175f3a7d31f05a051c9127bff1d51#diff-f87bb3596894756629bc39d595fb18d479dc4edf168d93a911cadcb060f10fcc