医療DXを加速させる愉快な技術集団ispec inc.でCloudSailという製品のエンジニアをしております。
この投稿はispec-inc Advent Calendar 2025への投稿です。
1週間ぶりの投稿ですが、みなさん、Zigってご存知ですか?
2016年に産声をあげた、まだ小学生くらいのプログラミング言語ですが、
その実績がすごいのです。
- Bun (JavaScript All-in-One パッケージ/2023年)
- Tigerbeele (金融系特化DBエンジン/2024年)
(年数は初期プロダクションリリースを参照)
これらの製品は異次元の性能で世間を賑わせました。
この記事を見ている方々であればBunのほうが馴染みが深いかもしれません。私たちも環境の一部でBunを使っていたりします。
この二つの製品がキラーコンテンツとなり、Zigは一躍注目を集めました(一部で)。
Zigは「隠れた動作が一切存在しないこと」を目指した言語であり、その本質は薄いランタイムとエンジニアこそがコードの神であるべき、という考えと見えます。
今回はそんなZigに魅せられてしまった私と一緒に最新版であるZig 0.15.2を利用して入門をしようという記事です。
先週投稿したRustと違い、Zigは私自身この記事を書くタイミングで初めて触り始めたので、間違いがあれば是非とも指摘いただけると嬉しいです!
Zigの開発環境を整える
Zigはバージョンを見ても分かる通り、まだ1.0.0に達していない言語です。
破壊的変更が頻繁に入るため、バージョン管理には注意してください。今回記載している内容は0.16.0や0.14.0では利用できない可能性が高いです。
Zigのインストール
インストールについては公式のGetting Startedを参照いただきたいですが、Macを利用されている方の場合は以下のbrewコマンドで記事作成時点(2025/12/23)では0.15.2がインストールされます。
brew install zig
それぞれの方法でZigをインストールしてzig versionでバージョンでバージョンが確認できれば完了です。
% zig version
0.15.2
コーディング環境
ZigはさまざまなIDE/TextEditorでPluginを公式に紹介しています。
JetBrains系でもZigBrainsが記載されていますが、あまりサジェストが働かない(表示が遅かったり)するので、VS Code系か記載されていませんがZedを利用することをお勧めします。
VS Code系はCursorでも動作を確認しました。これらは関数などのサジェストがよく働くので他の言語と同じように書けると思います。
- VS Code: https://marketplace.visualstudio.com/items?itemName=ziglang.vscode-zig
- Zed: https://github.com/zed-extensions/zig
この辺をインストールすれば環境設定は完了です。
Hello World
まぁ言語の始まりと言ったらこれですよね。
とりあえず、zigの始まりです。ここの手順はZig公式のRun Hello Worldにありますのでそれに従いましょう。
mkdir hello-worldcd hello-worldzig init
これで以下のようにファイルなどが作られたはずです。
├── build.zig
├── build.zig.zon
└── src
├── main.zig
└── root.zig
このsrc/root.zigはライブラリを作るケースで利用するものなので、今回は使いません。
削除しましょう。
rm src/root.zig
では初めて行きましょう。
初期コードの整理
src/main.zigを開くと以下のようになっているはずです。
const std = @import("std");
const hello_world = @import("hello_world");
pub fn main() !void {
// Prints to stderr, ignoring potential errors.
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
try hello_world.bufferedPrint();
}
test "simple test" {
const gpa = std.testing.allocator;
var list: std.ArrayList(i32) = .empty;
defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
try list.append(gpa, 42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
}
test "fuzz example" {
const Context = struct {
fn testOne(context: @This(), input: []const u8) anyerror!void {
_ = context;
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
}
};
try std.testing.fuzz(Context{}, Context.testOne, .{});
}
testとプレフィックスがついているものはテストコードなので無視しましょう。
一番上にconst std = @import("std")はライブラリのインポートです。これは言語の標準ライブラリをimportしている形です。
Rustでいうところのuse std::XXXみたいなイメージですね。
そしてmainは他の言語同様にエントリーポイントになります。
const hello_world = @import("hello_world");は先ほど削除したroot.zigをインポートしているものなので削除しましょう。
なのでmain.zigは以下のようになります。
const std = @import("std");
pub fn main() !void {
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
}
一旦ここで実行してみましょう。
zig build run
すると以下のようなエラーになると思います。
% zig build run
run
└─ run exe hello-world
└─ install
└─ install hello-world
└─ compile exe hello-world Debug native 1 errors
error: failed to check cache: 'src/root.zig' file_hash FileNotFound
error: the following command failed with 1 compilation errors:
/opt/homebrew/Cellar/zig/0.15.2/bin/zig build-exe -ODebug --dep hello-world -Mroot=/Users/qiita/src/main.zig -Mhello-world=/Users/qiita/src/root.zig --cache-dir .zig-cache --global-cache-dir /Users/.cache/zig --name hello-world --zig-lib-dir /opt/homebrew/Cellar/zig/0.15.2/lib/zig/ --listen=-
Build Summary: 0/5 steps succeeded; 1 failed
run transitive failure
└─ run exe hello-world transitive failure
├─ compile exe hello-world Debug native 1 errors
└─ install transitive failure
└─ install hello-world transitive failure
└─ compile exe hello-world Debug native (reused)
どうやらビルドプロセスに先ほど削除したsrc/root.zigが含まれてしまっているようです。
Zigのビルドプロセスはbuild.zigで管理されています。build.zigを見てみましょう。
手元で見るとたくさんのコメントが書かれていますが、コメントを削除してしまうと以下のようになっていると思います。
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("hello-world", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
});
const exe = b.addExecutable(.{
.name = "hello-world",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "hello-world", .module = mod },
},
}),
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
}
src/root.zigが使われている部分を探しましょう。
const mod = b.addModule("hello-world", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
});
ここですね。b: *std.Buildのb.addModuleでモジュールをどうやら追加しているようです。
Module名はhello-worldとなっていますね。これが@import("hello-world")の正体だったようです。
ということでこれを削除したいのですが、
// addExecutable内
.imports = &.{
.{ .name = "hello-world", .module = mod },
},
...
const mod_tests = b.addTest(.{
.root_module = mod,
});
...
const run_mod_tests = b.addRunArtifact(mod_tests);
...
test_step.dependOn(&run_mod_tests.step);
とmodも削除しましょう。するとbuild.zigは以下のようになると思います。
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello_world",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
}
これで再度実行してみましょう。
$ zig build run
All your codebase are belong to us.
と表示されましたね!これでmain.zigのコードを動かすことができました!
Hello Worldの表示
さて、ではようやくHello, Worldが表示できそうですね。
src/main.zigを見ると
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
があり、これが先ほどの表示だったわけですから、この文字列を変更すれば良さそうですね。
なお、std.debug.printは引数を必ず2つ取ります。
fn print(comptime fmt: []const u8, args: anytype) void
と定義されています。したがって、argsは空の.{}を入力してあげましょう。(僕はRustとかの感覚でそのまま書いたらエラーになって引っかかりました。)
std.debug.print("Hello, World!\n", .{});
つまり、今のsrc/main.zigは
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello, World!\n", .{});
}
となっているかと思います。
これで実行してみると、
$ zig build run
Hello, World!
出力されました!これで完了でも良いのですが、std.debug.printは公式のドキュメントをみると
Print to stderr, silently returning on failure. Intended for use in "printf debugging". Use std.log functions for proper logging.
Uses a 64-byte buffer for formatted printing which is flushed before this function returns.
となっています。重要なのはPrint to stderrです。
Zigのstd.debug.print()は標準エラー出力なんですよね。
まぁ単純なデバッグ目的であれば標準エラー出力でも良いですが、なんか気持ち悪いと僕は感じてしまったので標準出力に表示する方法も調べました!
次はこれを標準出力に出してみましょう。
Zigではstdoutはstd.fs.File.stdout()という形で存在しています。したがって、これを利用して記載しましょう。
const std = @import("std");
pub fn main() !void {
const stdout = std.fs.File.stdout();
try stdout.writeAll("Hello, World!\n");
}
としましょう。Fileとして扱う形なのでwriterAllで書き込みます。
これを実行すると
% zig build run
Hello, World!
表示自体は先ほどと変わりませんが、これで標準出力で表示することができました!
まとめ
今回はZigの環境を作ってHello, World!を表示してみるところまでを一緒に見てきました。
Hello, World!を表示しただけですが、ビルドプロセス設定の更新やstdoutを利用する方法も見ましたね。
設定が複雑で難しい、と感じた方もいたかもしれませんが、それでも現代的なC言語と言われるだけあり、低レイヤーの操作やメモリ管理という点にも非常に強みがあります。
ZigはRust同様にメモリ安全に意識を持っているという話も世の中で言われています。事実としてある程度のメモリ安全のための機能を言語としても持っています。
そうなると、Rustでよくね?という声も聞こえてきそうですが、思想が異なると僕は書いていて思いました。
Rustは
- プログラマは基本的にミスを犯す。人間は信用するべきではない。
- だからこそ、メモリ安全はコンパイラが数学的に証明可能な形で保証する。
という方針であり、これはC/C++などでメモリ管理に苦しみ、またメモリによる脆弱性に涙を飲んできた人たちの血の結晶であり、「unsafeを使わない限りは未定義処理は発生しない」というのは素晴らしいものです。
一方でZigは全く異なる視点で
- メモリ管理を含めてコードの全てはあくまでも人間が管理するべきである
- 隠れたコンパイラなどによるフローは悪である
- 言語は人間が書きやすいようにサポートすることに徹する
- DebugAllocator(旧GeneralPorpuseAllocator)などではメモリリークも検出が可能
というようにあくまで全ての制御を人間に渡すことで言語が行う隠れた処理を無くす、そして隠れたフローがないということはコードを見れば全ての動作が書かれており、それがパフォーマンスにも、安全性にもつながる。
といったところでしょうか。
個人的には万人受けはもちろんRustですが、一部の捻くれ者や狂人(いい意味で)はZigに魅せられてしまうんだろうな、とこれを考えた時に思いました。
そして全てを自分が管理する、というのはロマンでもありますよね。
個人的にもZigを利用した狂気的なプロジェクトが大好物なので僕自身もいつかZigで何か作ってみたいな、とも思っています。
最後に
今回の内容の延長としてランダムな文字列でパスワードを生成するプログラムを書いてみました。
またZigのアロケータ管理を利用して最適化をsrc/generatorsの実装では行っています。
ぜひ見てみて、Zigの使い方の参考にでもしていただければと思います!
今回の記事はそろそろ終わりにしようと思います。
それではまたいつか〜!