🔨

ZigはCMakeの代替となるか

2022/08/08に公開約8,500字1件のコメント

はじめに

引き続きZigを触っている。

https://zenn.dev/ryoppippi/articles/4fc7570643339d
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のTaget一覧
❯ 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等のツール、場合によってはシェルスクリプトなど複数のツールを駆使しなければならない[1]

CMakeLists.txt
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などの外部ライブラリをリンクをすることはもちろんできるものの、リンクできなかった場合は単にビルドが失敗するだけである)。→ 追記参照

build.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 ".";
}
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
脚注
  1. CMakeLists.txtやMakefile、Configure.shなどいくつもの設定ファイルが含まれたプロジェクトをみたことがあるはずである ↩︎

Discussion

ログインするとコメントできます