@ryoppippi

ZigはCMakeの代替となるか

8 Aug 2022 ・ 10 min read


はじめに

引き​続きZigを​触っている。 /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コマンドを​置き換えてみる

 gcc main.c -o main
 zig cc main.c -o main
 ./main
Hello world

既存の​プロジェクトで​使用している​コンパイラを​置き換えるだけで、​Zigに​付属している​Cコンパイラを​利用できる。

クロスビルドが​標準で​可能

上でも​述べた​通り、​Zigは​標準で​クロスコンパイルが​可能である。

Zig libcのTarget一覧
 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が​含まれていて、​生成する​バイナリに​それらが​埋め込まれる​ため​(静的ビルドされる​ため)、​ターゲット先で​依存ライブラリを​導入する​必要が​ない。 とても​ポータブルかつクロスプラットフォームな​バイナリを​生成する​ことができる。

 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回目以降の​ビルドは​高速化される。

ビルドシステム全体を​置き換える​ことができる?

さて、​より​依存関係の​多い​プロジェクトに​ついて​考えてみよう。 現代の​標準的な​プロジェクトではmakeCMakebazelと​いった​ビルドシステムが​よく​用いられる。 ここでは​筆者が​普段よく​触れているCMakeと​比較する。

比較の​ために、EgienSpectraを​用いた​簡単な​C++プロジェクトを​作成した。

https://github.com/ryoppippi/cpp-zig-build-system-demo/tree/9820e84775f0d11da81999712b88f4e5522cd371

CMake

現代のCMakeは​さまざまな​コマンドをCMakeLists.txtに​記述する​ことで​依存する​ファイルや​ライブラリを​解決する​ことができる。 また、find_packageExternalProject_Add,execute_processを​駆使する​ことで​ライブラリを​探したり外部​コマンドを​実行する​ことができる。 C/C++プロジェクトには​必須の​ツールと​言って​良いだろう。

ただしいく​つか​問題は​ある。 例えば、CMakeLists.txtは​設定ファイルである​都合上、​小回りが​利かなかったり独自の​記法、​御作法に​戸惑うことも​多い。 また​ビルドプロセスを​完遂する​ためにはCMakeだけではなく、​コンパイラを​別途導入する​必要が​ある​ことは​もちろん、MakeNinja等の​ツール、​場合に​よっては​シェルスクリプトなど​複数の​ツールを​駆使しなければならない​^CMakeLists.txtや​Makefile、​Configure.shなどいく​つもの​設定ファイルが​含まれた​プロジェクトを​みたことが​あるはずである

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}")
mkdir build
cd build
cmake ..
make
./build/main

build.zig

Zigでは​ビルドの​設定をbuild.zigに​書く​ことができる。 build.zigでは​Zigだけでなく、​C/C++の​ビルド設定を​記述できる。 中身は​Zigの​コードなので、​関数を​用いて​操作を​分割したり、​外部​コマンド実行や​条件分岐を​記述する​ことで​読みやすく​記述しやすいビルドファイルが​出来上がる​(個人の​感想)。 また、​実行も​1つの​コマンド実行で​済む。 さらに、​クロスコンパイルも​可能である。 ただし、​CMakeのfind_packageに​相当する​機能が​未実装な​ため、​全てを​置き換える​ことは​できなかった​(BLASなどの​外部​ライブラリを​リンクを​する​ことは​もちろんできる​ものの、​リンクできなかった​場合は​単に​ビルドが​失敗するだけである)。​→ 追記参照

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 ".";
}
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

comment on bluesky / twitter
CC BY-NC-SA 4.0 2022-PRESENT © ryoppippi