Zigで条件付きコンパイル (zig 0.13.0)

C/C++でいう、以下のようなものをzigではどうすればいいのかについての調査。
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
//define something for Windows (32-bit and 64-bit, this part is common)
#ifdef _WIN64
//define something for Windows (64-bit only)
#else
//define something for Windows (32-bit only)
#endif
#elif __APPLE__
#include <TargetConditionals.h>
#if TARGET_IPHONE_SIMULATOR
// iOS, tvOS, or watchOS Simulator
#elif TARGET_OS_MACCATALYST
// Mac's Catalyst (ports iOS API into Mac, like UIKit).
#elif TARGET_OS_IPHONE
// iOS, tvOS, or watchOS device
#elif TARGET_OS_MAC
// Other kinds of Apple platforms
#else
# error "Unknown Apple platform"
#endif
#elif __ANDROID__
// Below __linux__ check should be enough to handle Android,
// but something may be unique to Android.
#elif __linux__
// linux
#elif __unix__ // all unices not caught above
// Unix
#elif defined(_POSIX_VERSION)
// POSIX
#else
# error "Unknown compiler"
#endif

方法については以下の記事にあるように、if文の条件がcomptimeに決まる値だとして、もしコンパイル時にfalse
になっていれば、そのif文の中身は解析対象およびコード生成から外れるという特性を活かし、このcomptimeの値やif文をプリプロセッサのマクロ(C言語でいう#define
や#if
等)と同じようにして利用する。
const builtin = @import("builtin");
fn myFunction() void {
if (builtin.os.tag == .macos) {
// This code will only be included if the target OS is macOS.
return;
}
// This code will be included for all other operating systems.
}

実際に自分も実験してみた。@cInclude
等を使うと実際のヘッダファイルを準備しなければいけなくて面倒なので、ここでは、@embedFile
で好きなファイルを埋め込めることを活かし、ビルドオプションによってファイルの存在確認(つまり @embedFile
の行全体)がきちんとスルーされるかを確認した。

READMEに書いている通りだけど、実際に書いたコードは以下の通り。
このコードは zig build run
とすればファイルの埋め込みはされない(かつ埋め込み対象のファイルが存在していなくても問題ない)。そして zig build run -Denable_embed=true
とすればファイル埋め込みされ、もちろんのことながらファイルが存在していなければコンパイル時にエラーを吐く。
main.zig
const std = @import("std");
const options = @import("build_options");
const enable_embed = options.enable_embed;
// Conditionally embed the file based on build option
const embedded_content: ?[]const u8 = if (enable_embed)
@embedFile("embed.txt")
else
null;
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello from main!\n", .{});
if (enable_embed) {
try stdout.print("Content: {s}\n", .{embedded_content.?});
}else{
try stdout.print("Embedding is disabled by build option. If you want to enable it, set the build option `-Denable_embed=true`\n", .{});
}
}
build.zig (抜粋)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Add a build option
const embed_option = b.option(
bool,
"enable_embed",
"Enable file embedding feature",
) orelse false;
const options = b.addOptions();
options.addOption(bool, "enable_embed", embed_option);
const exe = b.addExecutable(.{
.name = "zig_conditional_build_test",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addOptions("build_options", options);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
run_step.dependOn(&run_cmd.step);
}

これによってわかることは、zigはプリプロセッサ・マクロ専用の構文を持っていない代わりに、コンパイル時計算ができ、かつ不必要な箇所は自動で無視するように作られているので、comptimeな値によって自由自在にプリプロセッサのような処理ができることがわかる。
(comptimeは、実用上はC++のテンプレートやRustのマクロ相当だと考えると良いけれど、zigは目に見えないフローの存在を嫌う言語なので、ASTの処理はできないことに注意。その代わりに、制約は多少あるが実行時と同じようにコンパイル時計算ができて値を返すことができ、かつZigは型を至って普通に値として扱うことができるので、それを活かしていくことになる。)
ただ、この特性のために、使われていない関数にエラーがあってもきちんと静的解析されず、エラーが内在するプログラムを書いてしまいがちな点に注意。(この点はコンパイル時ダックタイピングのようなことを許容するがゆえの、諸刃の剣。)