Zigは2016年にAndrew Kelleyによって開発が始まったシステムプログラミング言語です。C言語の正統な後継を目指しており、CやC++が抱える長年の問題点を根本から解決しようとしています。
最大の特徴は「隠れた制御フロー」の排除だ。Zigには暗黙的な型変換、隠れたアロケータ、例外によるスタック巻き戻しが存在しません。コードを読めば何が起きているか完全に把握できます。これは、安全性が要求されるシステムプログラミングにおいて非常に重要な性質です。
もう一つの大きな特徴は、Cとの相互運用性だ。ZigはCのヘッダファイルを直接インポートでき、既存のCライブラリをバインディングなしで呼び出せます。これにより、数十年にわたって蓄積されたCのエコシステムをそのまま活用できます。
Zigの基本的な文法を見ていきましょう。変数宣言と関数定義から始めます。
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
// 変数宣言(constは不変、varは可変)
const message = "Hello, Zig!";
var counter: u32 = 0;
// whileループ
while (counter < 5) : (counter += 1) {
print("Counter: {}\n", .{counter});
}
print("{s}\n", .{message});
}
Zigでは型が明示的であり、整数型はu32(符号なし32ビット)、i64(符号付き64ビット)のように、ビット幅が名前に含まれています。曖昧さがなく、クロスプラットフォームでの移植性が高いです。
Zigのエラーハンドリングは言語の設計思想を最もよく体現している部分です。例外は存在せず、代わりにエラーユニオン型を使う。
const FileError = error{
NotFound,
PermissionDenied,
OutOfMemory,
};
fn readConfig(path: []const u8) FileError![]const u8 {
const file = std.fs.cwd().openFile(path, .{})
catch return FileError.NotFound;
defer file.close();
const content = file.readToEndAlloc(
std.heap.page_allocator, 1024 * 1024
) catch return FileError.OutOfMemory;
return content;
}
pub fn main() void {
const config = readConfig("config.txt") catch |err| {
print("Error: {}\n", .{err});
return;
};
print("Config loaded: {d} bytes\n", .{config.len});
}
関数の戻り値型FileError![]const u8は、「FileErrorが発生しうるが、成功時は[]const u8を返す」ことを意味します。エラーの可能性がある関数は、呼び出し側で必ずcatchで処理しなければコンパイルエラーになります。エラーを無視できないという設計は、堅牢なシステムソフトウェアの構築に大きく貢献します。
Zigにはガベージコレクタもボローチェッカーもないです。代わりに、アロケータを明示的に渡すという設計を採用しています。
const std = @import("std");
pub fn main() !void {
// アロケータの選択は呼び出し側の責任
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 動的配列の作成
var list = std.ArrayList(u32).init(allocator);
defer list.deinit();
try list.append(42);
try list.append(100);
try list.append(7);
for (list.items) |item| {
std.debug.print("{} ", .{item});
}
}
アロケータを明示的に扱うことで、テスト時にはテスト用アロケータ(メモリリークを検出するもの)に差し替えたり、組み込みシステムではカスタムアロケータを使ったりと、柔軟なメモリ管理が可能になります。
Zigの最も強力な機能の一つがcomptimeだ。コンパイル時に任意の計算を実行でき、C++のテンプレートメタプログラミングに相当するが、はるかに直感的に書けます。
fn fibonacci(comptime n: u32) u32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// コンパイル時に計算される
const fib_10 = fibonacci(10); // 55
// ジェネリクスもcomptimeで実現
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
const result_int = max(u32, 10, 20); // 20
const result_float = max(f64, 3.14, 2.71); // 3.14
ジェネリクスのためだけに特別な構文を導入するのではなく、comptimeという汎用的な仕組みで統一的に実現しているのがZigらしい設計です。
ZigからCライブラリを呼び出すのは驚くほど簡単です。
const c = @cImport({
@cInclude("stdio.h");
@cInclude("stdlib.h");
});
pub fn main() void {
_ = c.printf("Hello from C!\n");
const val = c.atoi("42");
std.debug.print("Parsed: {}\n", .{val});
}
@cImportでCのヘッダファイルを直接取り込めます。バインディングの自動生成やFFIレイヤーは不要です。この相互運用性の高さが、Zigを既存のCコードベースからの段階的な移行に最適な選択肢にしています。
Zigはまだバージョン1.0に到達しておらず、破壊的変更の可能性はあります。しかし、その設計思想の明確さとコミュニティの活発さを見れば、システムプログラミングの未来を担う言語の一つであることは間違いないです。
特に、BunのランタイムやTigerBeetleデータベースなど、本番環境で使われるプロジェクトが増えてきていることは、言語の成熟度を物語っています。C言語に不満を感じているエンジニア、あるいはRustのライフタイム管理に苦戦しているエンジニアには、Zigは非常に魅力的な選択肢となるでしょう。まずは公式のZig Learnガイドから始めてみてほしい。