ZigはCMakeの代替となるか
はじめに
引き続き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回目以降のビルドは高速化される。
ビルドシステム全体を置き換えることができる?
さて、より依存関係の多いプロジェクトについて考えてみよう。
現代の標準的なプロジェクトではmake
やCMake
、bazel
といったビルドシステムがよく用いられる。
ここでは筆者が普段よく触れているCMake
と比較する。
比較のために、EgienとSpectraを用いた簡単なC++プロジェクトを作成した。
CMake
現代のCMake
はさまざまなコマンドをCMakeLists.txt
に記述することで依存するファイルやライブラリを解決することができる。
また、find_package
やExternalProject_Add
,execute_process
を駆使することでライブラリを探したり外部コマンドを実行することができる。
C/C++プロジェクトには必須のツールと言って良いだろう。
ただしいくつか問題はある。
例えば、CMakeLists.txt
は設定ファイルである都合上、小回りが利かなかったり独自の記法、御作法に戸惑うことも多い。
またビルドプロセスを完遂するためにはCMake
だけではなく、コンパイラを別途導入する必要があることはもちろん、Make
やNinja
等のツール、場合によってはシェルスクリプトなど複数のツールを駆使しなければならない[1]。
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のトランスコンパイルも容易である。
詳しくは以下の記事を参照してほしい。
実用例
実際UberではZigをC/C++ toolchainとして使っているようだ。
CGOをクロスコンパイルする環境として利用しているようである。
(Zig言語自体はまだ利用していないとのこと)
またDenoの拡張をクロスビルドするのにZig CCを使った例もある。
個人的所感
- ZigのエコシステムにC/C++のプロジェクトを組み込むことで、さまざまな恩恵を得られる予感がする。
- 個人的には設定が書きやすく感じた。個人プロジェクトではCMakeを置き換えていくかもしれない。
- クロスコンパイルは便利。例えばラズパイ用のビルドをqemuを経由しないで母艦で行うことができるのはとても良い。
- OpenMPやBLAS等のlibcに含まれないかつOSやハードに強く依存するライブラリはクロスコンパイルと相性が悪い。今後に期待(追記:ネイティブビルドは大丈夫そう)。
追記
以前この記事を書いた時に、find_package
が動かないと判断した理由は、OpenMPがうまくリンクできなかったためである。
しかし、これはincludeパスが通っていなかったせいであり、きちんとパスを通せばコンパイルできた。
ただ依然として、ライブラリが見つからなかった場合に何か条件分岐を実行することは現時点ではできなそうである。
-
CMakeLists.txtやMakefile、Configure.shなどいくつもの設定ファイルが含まれたプロジェクトをみたことがあるはずである ↩︎
Discussion
find_packageについては議論がいくつかある