引き続きZigを触っている。
https://zenn.dev/ryoppippi/articles/4fc7570643339d
https://github.com/ryoppippi/nyancat.zig
今回はC/C++ toolchainとしてのZigについて書いていく。
まず、ZigはCコンパイラでもある。
これが何を意味するかといえば
というわけで、C/C++プロジェクトにZigを導入すると嬉しいことを列挙していく。
Zig Toolchainはコンパイラ、ビルドシステム、リンカ、標準ライブラリを含んでいる。
また、標準でクロスコンパイルにも対応している。
これらのツールが全部含まれているのにも関わらず、容量なんと40MBほど。とても小さい。
このように、Zig toolchainはビルド環境としては極めて導入が簡単であると言える。
❯ gcc main.c-o main❯ zig cc main.c-o main❯ ./mainHello world既存のプロジェクトで使用しているコンパイラを置き換えるだけで、Zigに付属しているCコンパイラを利用できる。
上でも述べた通り、Zigは標準でクロスコンパイルが可能である。
❯ 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 ./mainHello world先に述べた通り、C/C++を使うプロジェクトならコンパイラはどこかで使っているはずなので、既存のコンパイラを置き換えるだけでクロスビルドが可能なのはとても便利。
Zigコンパイラは一度ビルドを行うと、その結果をキャッシュに保存する。
そのため、2回目以降のビルドは高速化される。
さて、より依存関係の多いプロジェクトについて考えてみよう。
現代の標準的なプロジェクトではmakeやCMake、bazelといったビルドシステムがよく用いられる。
ここでは筆者が普段よく触れているCMakeと比較する。
比較のために、EgienとSpectraを用いた簡単なC++プロジェクトを作成した。
https://github.com/ryoppippi/cpp-zig-build-system-demo/tree/9820e84775f0d11da81999712b88f4e5522cd371
現代のCMakeはさまざまなコマンドをCMakeLists.txtに記述することで依存するファイルやライブラリを解決することができる。
また、find_packageやExternalProject_Add,execute_processを駆使することでライブラリを探したり外部コマンドを実行することができる。
C/C++プロジェクトには必須のツールと言って良いだろう。
ただしいくつか問題はある。
例えば、CMakeLists.txtは設定ファイルである都合上、小回りが利かなかったり独自の記法、御作法に戸惑うことも多い。
またビルドプロセスを完遂するためにはCMakeだけではなく、コンパイラを別途導入する必要があることはもちろん、MakeやNinja等のツール、場合によってはシェルスクリプトなど複数のツールを駆使しなければならない[1]。
cmake_minimum_required(VERSION3.5)include(ExternalProject)enable_language(Fortran)set(CMAKE_CXX_STANDARD14)set(PROJECT_ROOT"${CMAKE_CURRENT_LIST_DIR}")find_package(Git QUIET)if(GIT_FOUNDAND EXISTS"${PROJECT_ROOT}/.git")execute_process(COMMAND${GIT_EXECUTABLE} submodule update --init --recursiveWORKING_DIRECTORY${PROJECT_ROOT} RESULT_VARIABLE GIT_SUBMOD_RESULT)if(NOT GIT_SUBMOD_RESULTEQUAL"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(mainPUBLIC${EIGEN3_INCLUDE_DIRS}${SPECTRA_INCLUDE_DIRS})target_link_libraries(main m blas${BLAS_LIBRARIES})message("${CMAKE_CXX_FLAGS}")mkdir buildcd buildcmake..make./build/mainZigではビルドの設定をbuild.zigに書くことができる。build.zigではZigだけでなく、C/C++のビルド設定を記述できる。
中身はZigのコードなので、関数を用いて操作を分割したり、外部コマンド実行や条件分岐を記述することで読みやすく記述しやすいビルドファイルが出来上がる(個人の感想)。
また、実行も1つのコマンド実行で済む。
さらに、クロスコンパイルも可能である。
ただし、CMakeのfind_packageに相当する機能が未実装なため、全てを置き換えることはできなかった(BLASなどの外部ライブラリをリンクをすることはもちろんできるものの、リンクできなかった場合は単にビルドが失敗するだけである)。→追記参照
const std=@import("std");const Builder= std.build.Builder;pubfnbuild(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",&[_][]constu8{}); 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);}fnensureSubmodules(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=(comptimethisDir()); child.stderr= std.io.getStdErr(); child.stdout= std.io.getStdOut(); _=try child.spawnAndWait();}fnthisDir()[]constu8{return std.fs.path.dirname(@src().file)orelse".";}zig build runこの記事では触れないが、ここまで来れば既存の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
以前この記事を書いた時に、find_packageが動かないと判断した理由は、OpenMPがうまくリンクできなかったためである。
しかし、これはincludeパスが通っていなかったせいであり、きちんとパスを通せばコンパイルできた。
ただ依然として、ライブラリが見つからなかった場合に何か条件分岐を実行することは現時点ではできなそうである。
CMakeLists.txtやMakefile、Configure.shなどいくつもの設定ファイルが含まれたプロジェクトをみたことがあるはずである↩︎
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。