Zig入門——「Cの問題を修正した言語」は何が違うのか?仕組みからクロスコンパイル実践まで
はじめに——「Cの問題を修正した言語」とは何か?なぜ今Zigなのか?
「C言語の代替」と公言するプログラミング言語は数多くありますが、その中でもZig(ジグ)は独特の立場を占めています。公式サイトが掲げるのは、Cの再実装でもC++の後継でもなく、「C but with the problems fixed(問題を修正したC)」——Cが半世紀にわたり蓄積してきた課題を、言語設計の段階で解決し直した言語だ、という主張です [1]。
具体的には、隠れた制御フローを排除し(演算子オーバーロード・例外・マクロなし)、メモリ確保を必ず明示させ(アロケータの引数渡し)、ガベージコレクションもランタイムも持たない。そのうえでCの資産を@cImportでそのまま活かせる [1]。この「見た目はC、しかし罠がない」設計思想が、Zigに注目が集まる最大の理由です。
1.0前なのに、なぜBunやTigerBeetleはZigを採用したのか?
意外に思われるかもしれませんが、Zigは2026年9月上旬時点でもまだバージョン0.16.0(2026年4月13日リリース)という1.0未満の言語です [2]。それにもかかわらず、JavaScriptランタイムのBunや金融データベースのTigerBeetle、そしてUberの座標系ライブラリh3など、Zigを採用したプロダクトの存在が知られています [3]。
理由は大きく2つあると考えられます。第一に、Zigが単なる言語ではなく実用的なツールチェーンであることです。zig ccはclang互換のC/C++コンパイラとして動き、PostgreSQL・libcurl・zlib・libpng・ffmpegといった著名OSSが、そのビルドスクリプトのままzig ccでコンパイルできたと公式に報告されています [1]。さらに公式ターゲット一覧に掲載される多数のターゲット(Windows/macOS/Linux/WASM/RISC-Vなど)へのクロスコンパイルが、単一バイナリの配布物で可能です [1][2]。ただし後述のとおり、Apple純正フレームワークのリンクなど一部の場面では追加のSDKや設定が必要になります。プロダクトの一部にZigを採用するハードルが、他の新言語より低いとされるのです。第二に、開発体制の信頼性です。Zigは2016年2月にAndrew Kelleyが初公開して以降、Zig Software Foundation(ZSF、2020年設立の非営利法人)によって開発が継続されており、スポンサー資金で専任開発者を雇用する持続可能なモデルが築かれているとされます [1][3]。
本記事のゴール——今日からZigを動かせるところまで
この記事では、Zigを言葉だけでなく手で理解することを目指します。読み終える頃には、次の状態になっていることがゴールです。
- 設計思想の理解:「隠れた制御フローなし・隠れたメモリ確保なし・ランタイムなし」という3原則が、具体的にコードでどう現れるかを把握する
- comptimeとエラー処理の習得:コンパイル時にコードを実行する
comptimeと、try/catch/errdeferによるエラー処理の書き方を理解する - C・Rustとの比較:3言語の違いを比較表で整理し、「どの場面でどれを選ぶか」を判断できるようになる
- ハンズオン体験:公式tarballからのインストール、Hello Worldの実行、そして
zig build-exe -target x86_64-windowsによるクロスコンパイルまで、実際に体験する
注意:1.0前の言語であることを忘れずに
Zigに触れる前に、必ず知っておくべき注意点があります。Zigは1.0未満であり、破壊的変更が頻繁に入ります [2][3]。実際、2025年8月リリースの0.15系では標準ライブラリのI/Oインターフェースが全面的に刷新され(std.Io.Reader/Writerの導入、いわゆる「Writergate」)、0.14以前の入門記事に書かれたHello Worldのコードが動かなくなりました [2]。本記事のコードは執筆時点の最新安定版0.16.0(2026年4月13日リリース)のAPI仕様に基づく例ですが、Zigの情報を調べるときは、その記事が対象とするバージョンを必ず確認してください。開発版の0.17.0-devも毎日ビルドされていますが、入門段階では安定版を使うのが安全です [2]。
それではまず、Zigが「問題を修正したC」と呼ばれる由縁である設計思想から見ていきましょう。
Zigの設計思想はどうなっている?「隠れたものをなくす」という原則
Zigを一言で説明すると「明示性(explicitness)を最後まで貫いた言語」です。公式サイトが掲げる特徴の中心にあるのは、「隠れたものをなくす」という原則——隠れた制御フローなし、隠れたメモリ確保なし、ランタイムなし——の3本柱です [1]。一見地味な方針に見えますが、これこそが「問題を修正したC」と呼ばれる理由の核心です。それぞれ順に見ていきましょう。
なぜ「隠れた制御フローなし」が大事?演算子オーバーロード・例外・マクロの排除
C++で次のコードを読むことを考えてください。
auto result = a + b; // この + は本当に足し算か?
+が演算子オーバーロードされていれば、ここで任意の関数が呼ばれている可能性があります。例外が有効なら、どんな関数呼び出しも途中で脱出しうます。マクロなら、見た目と実際のコードが一致している保証すらありません。コードを「読んだだけで挙動を追えない」——これがC++系言語の大きなメンタルコストです。
Zigはこの問題を、該当機能を全部なくすことで解決しました [1]。
- 演算子オーバーロードなし —
+は常に整数か浮動小数点の加算です。ラップ(桁あふれ)させたい場合も専用の演算子+%で明示します。 - 例外なし — 失敗しうる処理はエラーunion型
!Tで型に現れ、伝播にはtry、捕捉にはcatchという目に見えるキーワードが必要です。例外のように呼び出しツリーを暗黙に遡ることはありません(詳細は次セクションで)。 - マクロなし — プリプロセッサという「別言語」を廃止しました。条件コンパイルや定数計算は、コンパイル時にZigそのものを実行する
comptimeで置き換えます [1]。
結果、「Zigのコードは読んだ通りに動く」という性質が設計目標となります。関数呼び出しの裏で何かが起きている可能性を常に疑う、というC++読解の疲労から解放されるのです。
「隠れたメモリ確保なし」とは?アロケータは必ず引数で渡す
Zigでは、メモリを確保する標準ライブラリの関数はアロケータを引数として受け取ります。「暗黙にヒープ確保する関数」が言語内に存在しない、というのが設計原則です [1]。
実際のコードで見てみましょう(0.16.0のAPI仕様に基づく例です)。
const std = @import("std");
pub fn main() !void {
// デバッグ用アロケータ(リーク検出つき)
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// ArrayListの生成にも操作にも、必ずアロケータを渡す
var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);
try list.appendSlice(allocator, "Hello, Zig!");
std.debug.print("{s}\n", .{list.items});
}
ポイントはstd.ArrayListへの追加操作にいちいちallocatorが登場することです。ホットループの中でアロケーションが起きているかどうかは、引数を見るだけで一目でわかります。
この設計には実利が二つあります。第一に、アロケータを差し替えられること。用途に応じて、ページ単位のstd.heap.page_allocator、一括解放できるアリーナ、マルチスレッド向けのSmpAllocatorなどを使い分けられます [2]。第二に、上の例で使ったDebugAllocatorのように、アロケータ側にリーク検出や二重解放検出を仕込めること。言語機能ではなくライブラリで安全装置を作れるのは、アロケータが単一のグローバルな存在でないからです。
Cのmalloc/freeが散らばりやすい問題、C++のnewがコンストラクタや演算子の奥に隠れる問題と対比すると、Zigのアプローチが「C的な手動管理の見通しを良くした」ものだと分かるでしょう。後始末はデストラクタではなくdefer/errdeferで明示的に書きます(これも「隠れた実行なし」の延長です)。
ランタイムなし——GCもlibc依存もないのに、どうやって動くのか?
Zigにはランタイムがありません。ガベージコレクションがないのはもちろん、デフォルトではlibcすらリンクしません。この「サブシステムを持たない(no subsystem)」設計の恩恵は次のとおりです [1]。
- ベアメタルで動く — OSのない環境(
-target wasm32-freestandingや組み込み、カーネル開発)でも、言語機能がそのまま使えます。 - バイナリが小さく依存が明確 — Zigのコードだけならlibc無しでコンパイルでき、何に依存しているかがビルド結果から読み取れます。
- クロスコンパイルが簡単になる — ターゲットごとのランタイムやSDKの多くが不要なので、単一バイナリで公式ターゲット一覧の多数のターゲットに向けたビルドが可能になります(詳細は後のセクションで実践します) [1][2]。
「ランタイムなし」は制約ではなく、メモリ確保も後始末も実行も、すべてプログラマの見える場所で行う——という原則の帰結なのです。
なぜ「問題を修正したC」と呼ばれるのか?
ここまでの原則を踏まえると、Zigの正体が見えてきます。Zigは見た目がほぼC(手動メモリ管理、シンプルな構文、コンパイルの速さ)でありながら、Cが半世紀抱えてきた「落とし穴」を言語レベルで塞いでいます [1][3]。
| Cの問題 | Zigでの修正 |
|---|---|
| NULLポインタによるクラッシュ | オプショナル型?T。unwrap前にnullチェックを強制 |
| errnoやNULL返却によるエラー処理の曖昧さ | エラーunion!T+try/catchで失敗を型に明示 |
| マクロによる型破りなコード生成 | comptime——型のある同じ言語でコンパイル時計算 |
| どこでmallocされるか分からない | アロケータの明示渡しで確保箇所が一目瞭然 |
| gotoによる複雑なクリーンアップ | defer/errdeferによる構造化された後始末 |
つまりZigが目指したのは「Cを置き換える全く別の言語」ではなく、Cの実践的な書き心地を保ちつつ、問題となっていた部分だけを修正した言語です。ヌル安全性と明示的なエラー処理を足し、隠れた動作をすべて排除する——この組み合わせこそが「C but with the problems fixed(問題を修正したC)」という標榜の中身です [1]。
ただし、この設計思想の代償として、言語仕様そのものは1.0を迎える前(2026年9月上旬時点の最新は0.16.0)であり破壊的変更が続いている点には注意してください [2]。思想はブレませんが、APIの形はバージョンで変わり得ます。
次のセクションでは、この思想を支える二つの柱——コンパイル時計算のcomptimeとエラー処理の仕組み——を、実際のコードで詳しく見ていきます。
comptimeとエラー処理の仕組みを理解するとZigが見えてくる
前章では「隠れたものをなくす」というZigの設計原則を見ました。しかし原則だけでは言語の実感は掴めません。ZigをZigたらしめている2つの仕組み——コンパイル時計算とエラー処理——を、コードとともに見ていきましょう。この2つを理解すると、「見た目はCなのに、Cの悩みの種が消えている」という"C but with the problems fixed"の中身が具体的に分かります [1]。
comptimeとは何か?——コンパイル時に動く「同じ言語」のコード
C++でジェネリックプログラミングというと、テンプレートという「テンプレート専用の別言語」を学ぶ必要がありました。Cのマクロはテキスト置換で、型の概念すらありません。
Zigのcomptimeは発想が違います。実行時に書くのとまったく同じZigコードを、コンパイル時に走らせるのです。コンパイル時には「型」も普通の値として扱えるため、テンプレートのような専用機構なしにジェネリクス・条件コンパイル・定数畳み込みが実現します [1]。
まずは一番シンプルな例から。
const std = @import("std");
fn fibonacci(n: u32) u32 {
return switch (n) {
0, 1 => n,
else => fibonacci(n - 1) + fibonacci(n - 2),
};
}
pub fn main() void {
// comptimeを付けると、コンパイル時に計算して定数としてバイナリに埋め込む
const answer = comptime fibonacci(20);
std.debug.print("fib(20) = {d}\n", .{answer});
}
fibonacci自体はどこにも特殊な書き方をしていない、普通の再帰関数です。呼び出し側でcomptimeと指定するだけで、コンパイル時に評価されます。
さらにcomptimeブロックを使うと、コンパイル時の検証を宣言できます。
comptime {
// この前提が崩れたらビルド自体が失敗する
if (@sizeOf(usize) != 8) {
@compileError("このモジュールは64ビット環境を前提としています");
}
}
後の章で登場するビルドシステム(build.zig)も、実はこのcomptimeの上に載った「普通のZigプログラム」です。ビルドスクリプト用の言語を新しく覚える必要がないのは、この設計の大きな恩恵です [2]。
ジェネリクスはどう書く?——型を引数に取る関数 fn List(comptime T: type) type
comptimeの最も代表的な応用がジェネリクスです。Zigでは「任意の型Tを要素に持つリスト型」を、型を受け取り型を返す関数として書きます [1]。
const std = @import("std");
/// 型Tを要素とするリスト型を生成する関数
fn List(comptime T: type) type {
return struct {
items: []T = &.{},
allocator: std.mem.Allocator,
const Self = @This();
pub fn append(self: *Self, item: T) !void {
const old_len = self.items.len;
const new_items = try self.allocator.realloc(self.items, old_len + 1);
new_items[old_len] = item;
self.items = new_items;
}
/// 確保したメモリを解放する
pub fn deinit(self: *Self) void {
self.allocator.free(self.items);
}
};
}
const Point = struct { x: i32, y: i32 };
pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// List(i32) も List(Point) も、同じ関数から生成される
var int_list = List(i32){ .allocator = allocator };
defer int_list.deinit();
try int_list.append(42);
var point_list = List(Point){ .allocator = allocator };
defer point_list.deinit();
try point_list.append(.{ .x = 1, .y = 2 });
std.debug.print("int_list = {any}\n", .{int_list.items});
std.debug.print("point_list = {any}\n", .{point_list.items});
}
ポイントは3つです。
comptime T: type— 引数Tが「コンパイル時に既知の型」であることを宣言しますList(i32)は関数呼び出しの形 — 型の生成がテンプレート展開ではなく、普通の関数呼び出しとして読める- アロケータを明示的に渡し、
deinitで必ず解放する設計(前章の「隠れたメモリ確保なし」がここでも一貫)
C++のテンプレートメタプログラミングのように、ジェネリクスのためだけに新しい構文を覚える必要はありません。実行時のZigと同じ言語で書ける——これが「テンプレートではなく同じ言語で書くジェネリクス」と言われる理由です [1]。
Zigのエラー処理はどうなっている?——!T・try・catch・errdefer
Zigのもう一つの柱がエラー処理です。失敗しうる関数は、戻り値型をエラーunion !T で宣言します。これは「成功時はT、失敗時はエラー値」を表す型で、関数シグネチャを見ただけで失敗しうるかどうかが分かります [1]。
const HexParseError = error{ InvalidLength, InvalidCharacter };
fn parseHexDigit(c: u8) !u8 {
return switch (c) {
'0'...'9' => c - '0',
'a'...'f' => c - 'a' + 10,
'A'...'F' => c - 'A' + 10,
else => error.InvalidCharacter,
};
}
/// "ff8800" のような6桁の16進数をu24の色コードに変換する
fn parseHexColor(input: []const u8) HexParseError!u24 {
if (input.len != 6) return error.InvalidLength;
var value: u24 = 0;
for (input) |c| {
const digit = try parseHexDigit(c); // 失敗したら即座にエラーをreturn
value = value * 16 + digit;
}
return value;
}
この例には、エラー処理の基本が詰まっています。
try——伝播。 try exprは「exprがエラーなら、その場でこの関数からエラーを返す」という意味です。エラーの巻き上げは例外のように暗黙には起きません。呼び出し箇所に必ずtryと書かなければならないため、どこでエラーが伝播しうるかをコードを見た目どおりに追えます(「隠れた制御フローなし」の原則そのものです)。
catch——捕捉。 エラーを伝播させず受け止めたい場合はcatchを使います。フォールバック値を返すだけなら1行で書けます。
// 失敗したら白にフォールバック
const color = parseHexColor("ff8800") catch 0xffffff;
// エラーの種類を受け取って処理することもできる
const color2 = parseHexColor("zzz") catch |err| {
std.debug.print("パース失敗: {}\n", .{err});
return err;
};
エラー値は網羅的に検査できる。 エラーは集合型(enumと同じ仕組み)の値なので、switchで全ケースを列挙できます。エラー集合に新しい種類を追加したのにswitchが取りこぼしていたら、コンパイルエラーになります。
fn describe(err: HexParseError) []const u8 {
return switch (err) {
error.InvalidLength => "桁数が正しくありません",
error.InvalidCharacter => "16進数として不正な文字が含まれています",
};
}
errdefer——エラー時だけの後始末。 deferが関数脱出時に必ず実行されるのに対し、errdeferはエラーでreturnするときだけ実行されます。「途中まで確保したリソースを、失敗したときだけ解放したい」というパターンにぴったりハマります。
C++のRAII(デストラクタ)と違い、後始末のタイミングがコード上で読める点がZigらしい設計です。
Cのエラー処理とどう違う?——errnoとNULL返しの問題
ここまでの機能が、Cの長年の悩みをどう解決するのか見比べてみましょう。まずは典型的なCのコードです。
/* C: 「失敗しうる」ことがシグネチャから分からない */
char *load_config(const char *path) {
FILE *f = fopen(path, "r");
if (f == NULL) {
return NULL; /* 呼び出し側はNULL検査を忘れがち */
}
char *buf = malloc(4096);
if (buf == NULL) {
fclose(f);
return NULL; /* 失敗理由はグローバルのerrnoを確認するしかない */
}
/* ...読み込みと解析... */
return buf;
}
このC流の慣行には、次の問題があります。
- 戻り値だけでは失敗が分からない —
char *という型はNULLを返しうることを教えてくれない - 検査は任意 — 呼び出し側がNULL検査を忘れても、コンパイラは何も言わない
- 失敗理由がグローバル状態 — errnoはスレッドローカルなグローバル変数で、他の呼び出しで上書きされうる
- 後始末が手作業 — どのパスで何を解放するかはプログラマがすべて覚えている必要がある
Zigでは同じロジックがこうなります。
// Zig: 失敗しうることは型に現れ、伝播は明示的、後始末はerrdefer
fn loadConfig(allocator: std.mem.Allocator, path: []const u8) !Config {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const buffer = try allocator.alloc(u8, 4096);
errdefer allocator.free(buffer);
return parseConfig(buffer);
}
| 観点 | C | Zig |
|---|---|---|
| 失敗の表現 | 戻り値NULL/−1+errno | エラーunion !T をシグネチャに明示 |
| 失敗の理由 | グローバルのerrnoを確認 | エラー値そのものを受け取る |
| 検査漏れ | コンパイラは検査しない | 未処理のエラーunionはコンパイルエラー |
| 途中の後始末 | 手作業でfclose/free | defer/errdeferで宣言的に |
重要なのは、例外のような高機能な仕組みを導入したのではなく、失敗を「普通の値」として型に載せただけという点です。エラー値は実行時コストの小さいタグ値であり、スタックを巻き戻す暗黙の仕組みもありません。Cのシンプルさを保ったまま、「検査忘れ」と「理由不明」だけを修正した——これがZigのアプローチです [1]。
この章のまとめ
comptimeはテンプレートでもマクロでもなく、「実行時と同じZig」をコンパイル時に走らせる仕組み。型を値として扱えるため、fn List(comptime T: type) typeのように型を生成する関数でジェネリクスを書く- エラー処理は
!Tエラーunion+try(伝播)+catch(捕捉)+errdefer(エラー時の後始末)で構成。すべて明示的で、未処理のままではコンパイルが通らない - errnoとNULL返しに代表されるCのエラー処理の問題を、「失敗を型に載せる」という最小の仕組みで解決している
これでZig独自の部分は押さえました。次章では、C・Rust・Zigをメモリ安全性・相互運用・学習コストなどの軸で横並びに比較し、「どの言語をどの場面で選ぶべきか」を整理します。
C・Rust・Zigはどこが違う?比較表で違いを整理する
ここまでZig単体の特徴を見てきました。しかし「Zig入門」を読む多くの方が気になるのは、やはり「CやRustと比べて実際どこが違うのか?」という点でしょう。ここでは、低レイヤー言語を選ぶ際に比較されることが多いC・Rust・Zigの3言語を、7つの軸で整理します。
メモリ安全性・GC・相互運用まで、7軸で比較するとこうなる
まずは結論から。3言語の違いを1枚の比較表にまとめました。
| 項目 | C | Zig | Rust |
|---|---|---|---|
| メモリ安全性 | 手動管理(言語側の支援なし) | 手動管理だがdefer/errdefer・Optional型など言語支援あり | 所有権・借用チェッカーによる静的保証 |
| 隠れた実行 | マクロによる暗黙のコード展開あり | なし(設計原則として排除)[1] | 演算子オーバーロード・マクロあり |
| GC(ガベージコレクション) | なし | なし | なし |
| C相互運用 | —(同一言語) | @cImportでCヘッダを直接インポート [1] | bindgen等でバインディングを自動生成 |
| クロスコンパイル | クロスツールチェーンの構築が面倒 | 単一バイナリで多数ターゲットに対応 [1] | rustup target addで比較的容易 |
| 学習曲線 | 低い(ただし危険な使い方もしやすい) | C+α程度(comptimeとエラーunionを覚えれば書ける) | 所有権・借用で最初の壁が高い |
| 成熟度 | 極めて高い(半世紀の実績) | 1.0前(2026年9月上旬時点で最新安定版0.16.0)[2] | 高い(2015年の1.0以降、定期リリース) |
この表から読み取れるのは、Zigが「Cの書きやすさ」と「Rustの手堅さ」の中間に位置する言語だということです。GCが無くランタイムを持たない点は3言語共通ですが、そこに至る思想が大きく異なります。Cは「言語は何も保証しない」、Rustは「コンパイラがメモリ安全性を静的に保証する」、そしてZigは「保証はしないが、危険なコードを見た目で追えるようにする」——このスタンスの違いが、表のすべての項目に表れています [3][8]。
Rustとの最大の違いは「所有権がない」こと——何がメリット?
Rust経験者にとってZigの第一印象は「借用チェッカーがないRustのような言語」になるはずです。Zigには所有権・借用・ライフタイムの概念がありません。メモリの確保と解放はプログラマの責任で、アロケータを明示的に受け渡す設計です [1][8]。
これがメリットになるのは、低レイヤーコードを書く場面だとされます。たとえば二重連結リストやグラフ構造のような「複数箇所から参照が飛ぶ」データ構造は、Rustでは借用規則に引っかかるためRcやRefCell、場合によってはunsafeが必要になりがちです。ZigではポインタをCと同じ感覚で扱えるため、アロケータ自体の実装や組込み開発、カーネル寄りのコードが素直に書けやすいとされます [8]。
一方で、トレードオフも明確です。Rustの借用チェッカーは「データ競合やuse-after-freeをコンパイル時に弾く」という強力な保証を与えますが、Zigにはそれがありません。Zigでは安全性を、明示的なコード・テスト・DebugAllocator(旧GeneralPurposeAllocator)のような実行時検査ツールで補うことになります。つまり「書きやすさを取るか、静的な保証を取るか」が、両言語を選ぶ際の本質的な分水嶺です。
結局どれを選ぶべきか?用途別の判断指針
3言語の違いを踏まえると、選択の指針は次のように整理できます。
- 既存のC資産を段階的に移行したい・共存させたいならZig。
@cImportでCヘッダをそのまま取り込め、zig ccでCプロジェクトをそのままビルドできるため、「一部だけZigに置き換える」という移行戦略が取れます [1]。 - メモリ安全性の静的保証が要件ならRust。大規模チームでの開発や、第三者が書くコードの品質をコンパイラで縛りたい場面では、借用チェッカーの価値は代替できません。成熟したエコシステムと1.0以降の安定性も強みです。
- 低レイヤーの学習や実験、ベアメタル開発にはZigが特に向いているとされます。「Cに近い感覚で書けるのにモダンな機能が乗っている」ため、Cから一歩進んだ言語を学ぶ入口としても優れていると考えられます。
- レガシー環境の保守だけであればCのままという判断も依然として有効です。半世紀分のノウハウと資産は、他言語では代替できない価値を持っています。
なお、Zigを選ぶ際は「1.0前の言語である」ことを必ず考慮してください。2026年9月上旬時点の最新安定版は0.16.0で、マイナーバージョン間でも標準ライブラリのAPIが大きく変わります(詳細はFAQセクションで扱います)[2]。この点を許容できるかどうかが、Zig採用の現実的な判断条件になります。
「どの言語が優れているか」ではなく「何を保証とし、何をプログラマに委ねるか」という設計思想の違い——この視点で3言語を見比べると、それぞれの存在理由がはっきりと見えてくるはずです。
どうやって始める?Zigをインストールして最初のプログラムを動かすまで(ステップバイステップ)
ここからは実際に手を動かします。Zigにはインストーラーも依存関係も必要ありません。公式サイトからtarballを落としてPATHを通すだけで、コンパイラ・ビルドシステム・パッケージマネージャ・クロスコンパイル対応がすべて揃ったツールチェーンが使えるようになります [1][2]。以下、執筆時点の最新安定版 0.16.0(2026-04-13リリース) を前提に進めます [2]。
Step 1: Zigをインストールするには?公式tarballのダウンロードとPATH設定
Zigの推奨インストール方法は、公式ダウンロードページ(https://ziglang.org/download/ )からtarballを取得することです [2]。aptなどOS標準のパッケージマネージャはバージョンが大幅に古いことが多く、1.0前のZigでは破壊的変更が多いため、バージョンの揃った公式バイナリを使うのが安全です。
Linux(x86_64)の場合:
curl -O https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
tar xf zig-x86_64-linux-0.16.0.tar.xz
export PATH="$PWD/zig-x86_64-linux-0.16.0:$PATH"
macOS(Apple Silicon)の場合:
curl -O https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz
tar xf zig-aarch64-macos-0.16.0.tar.xz
export PATH="$PWD/zig-aarch64-macos-0.16.0:$PATH"
Windows(PowerShell)の場合:
curl.exe -LO https://ziglang.org/download/0.16.0/zig-x86_64-windows-0.16.0.zip
Expand-Archive .\zig-x86_64-windows-0.16.0.zip .
$env:Path = "$pwd\zig-x86_64-windows-0.16.0;" + $env:Path
上記のexport/$env:Pathはセッション限りの設定です。継続して使う場合は、展開したディレクトリを~/optなど固定の場所に移動し、シェルの設定ファイル(~/.bashrcや~/.zshrc)にPATH設定を追記しておきましょう。
mv zig-x86_64-linux-0.16.0 ~/opt/zig-0.16.0
echo 'export PATH="$HOME/opt/zig-0.16.0:$PATH"' >> ~/.bashrc
source ~/.bashrc
なお、macOSのbrew install zigやWindowsのwinget install zigでもインストールできますが、配布バージョンが公式より遅れることがあります。バージョンはzig versionで必ず確認してください。
Step 2: インストールできたか確認するには?zig versionで動作確認
ターミナル(新しいシェルを開いた場合は開き直したターミナル)で次のコマンドを実行します。
zig version
次のようにバージョン番号が表示されれば成功です。
0.16.0
command not found: zigと出る場合はPATHが通っていません。Step 1のexport PATH=...を実行したシェルと同じセッションで試しているか、設定ファイルの追記に誤字がないかを確認してください。
Step 3: 最初のプログラムはどう書く?hello.zigを作ってzig runで動かす
作業用ディレクトリを作り、hello.zigを書きます。
mkdir hello-zig && cd hello-zig
hello.zig(0.16.0のAPI仕様に基づく例):
const std = @import("std");
pub fn main() !void {
var stdout_writer = std.fs.File.stdout().writer(&.{});
try stdout_writer.interface.print("Hello, {s}!\n", .{"Zig"});
}
保存したら、コンパイルと実行を一括で行うzig runで動かします。
zig run hello.zig
出力:
Hello, Zig!
コードのポイントを簡単に解説します。
@import("std")— 標準ライブラリを読み込みます。@始まりの識別子は組み込み関数ですpub fn main() !void— エントリポイント。!voidは「エラーを返しうるvoid型」(エラーunion)で、失敗しうることをシグネチャで明示しますtry— エラーが発生したらそこで関数からエラーを伝播させるキーワードです
⚠️ 古い入門記事に注意(Writergate) 0.15で標準ライブラリのI/Oがstd.Io.Reader/Writerを基盤とする新インターフェースに大幅に再編されました(コミュニティでは「Writergate」と呼ばれます)[2]。0.14以前の入門記事に書かれがちな次のコードは、0.16系ではコンパイルエラーになります。
Web上のZig記事を読むときは、まずzig versionで自分の環境と記事のバージョンを照合するのがトラブル防止のコツです。
Step 4: テストはどう書く?zig testでテストブロックを体験する
Zigには言語標準のテスト機能が組み込まれており、testブロックを書くだけで専用フレームワークなしにテストが実行できます。add.zigというファイルを作ってみましょう。
add.zig:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
test "add: 正の数同士の加算" {
try std.testing.expectEqual(@as(i32, 3), add(1, 2));
}
test "add: 負の数を含む加算" {
try std.testing.expectEqual(@as(i32, -1), add(1, -2));
}
実行します。
zig test add.zig
出力:
All 2 tests passed.
try std.testing.expectEqual(期待値, 実測値)が基本のアサーションです。期待値を@as(i32, 3)のように型指定しているのは、expectEqualが引数の型も比較対象にするため(1 + 2はi32なので合わせています)。テストを意図的に失敗させると、どのテストのどの行で期待と違ったかをソース付きで表示してくれるので、一度試してみると感触が掴めるでしょう。
Step 5: zig buildとパッケージ管理はどう使う?build.zigとbuild.zig.zonの概観
1ファイルのプログラムはzig runで十分ですが、規模が大きくなるとビルド定義が欲しくなります。Zigにはビルドシステムとパッケージマネージャが言語ツールチェーンに標準で同梱されており、zig initで雛形を生成できます [2]。
zig init
次のようなファイル群が作られます。
.
├── build.zig … ビルド定義(Zig自身、comptimeで記述)
├── build.zig.zon … パッケージのメタ情報と依存関係の定義
└── src/
├── main.zig … プログラムのエントリーポイント
└── root.zig … ライブラリとして公開する場合のルート
各ファイルの役割は次のとおりです。
- build.zig — MakefileやCMakeLists.txtに相当しますが、専用のDSLではなく普通のZigコード(comptime)で書くのが特徴です。「Cのソースを混ぜたビルド」もここで定義できます
- build.zig.zon — パッケージ名・バージョン・依存パッケージを宣言するファイルです。中央レジストリは無く、依存はURLベースで解決します
主なビルドコマンドは次のとおりです(zig initの雛形にはrunとtestのステップがあらかじめ定義されています)。
zig build # build.zigに従ってビルド(成果物は zig-out/ に格納)
zig build test # testステップを実行
zig build run # runステップを実行(ビルドして起動)
外部パッケージを追加するときは、build.zig.zonに依存を記述し、ソースを取得するzig fetchを使います。
zig fetch --save https://github.com/...(依存パッケージのtarball URL)
--saveを付けると取得したパッケージのハッシュ付きでbuild.zig.zonに依存が追記されます。ここでは概観だけですが、「言語・ビルドシステム・パッケージ管理が単一バイナリに一体化している」点はZigの大きな特徴だと実感できるはずです。
ここまでのまとめと次のステップ
- Zigはtarballを展開してPATHを通すだけで使い始められる(追加のツールチェーン不要)[1][2]
zig runで即実行、zig testで組み込みテスト、zig buildで本格ビルド- 0.15以降はI/O周りのAPIが変わっているため、古い記事のコードには注意 [2]
環境が整ったところで、次はZigが最も得意とする「クロスコンパイル」を実際に体験してみましょう。
クロスコンパイルは本当に簡単か?zig ccでWindows/macOS/WASM向けビルドを実践する
「Zigのクロスコンパイルは簡単」とよく聞くけれど、本当に簡単なのか? このセクションでは、疑問を抱えたまま読み流すのではなく、実際にコマンドを打ちながら検証していきます。
C/C++の世界では、Linux上でWindows向けバイナリを作ろうとするとMinGWの導入、macOS向けならSDKの収集とsysrootの構築、という具合に環境整備だけで半日を溶かすことが珍しくありません。Zigは標準のターゲット範囲ではこの環境構築コストを大きく下げている点で、他の言語と一線を画します [1]。
なぜZigのクロスコンパイルは簡単なのか?——単一バイナリにターゲット定義が同梱されている仕組み
Zigの公式配布物は、ダウンロードして展開しPATHを通すだけの単一バイナリです。この1つのファイルの中に、次の3点がすべて入っています [1]。
- clang/LLVM本体 — C/C++コンパイラとしての機能ごと同梱(後述する
zig ccの正体です)[1] - 多数のターゲット定義 — x86_64/aarch64/RISC-VなどのCPUアーキテクチャと、Windows/Linux/macOS/WASMなどのOS組み合わせ(正確な一覧は
zig targetsで確認できます) - 各ターゲット向けlibc — musl、mingw-w64(Windows)、glibc互換層などをソースコードとして保持 [1]
通常のクロスコンパイルでは「ターゲットごとのツールチェーン+libc(sysroot)」を手で揃える必要がありますが、Zigはクロスビルド時に必要なlibcをその場でソースからコンパイルし、グローバルキャッシュ(~/.cache/zigなど)に保存します。初回のビルドは少し時間がかかりますが、2回目以降はキャッシュが効いて速くなります。
対応ターゲットの一覧は、インストール直後に次のコマンドで確認できます。
zig targets
arch・os・abiのリストがJSONで表示されれば、クロスコンパイルの準備はもう完了です。
実践1:Linux/macOSからWindows向けの.exeを作るには?
前セクションで作成したhello.zigを使います(0.15以降の新しいI/O APIの書き方です)。
const std = @import("std");
pub fn main() !void {
var stdout_writer = std.fs.File.stdout().writer(&.{});
try stdout_writer.interface.print("Hello, {s}!\n", .{"Zig"});
}
LinuxやmacOS上で、Windows向けにビルドしてみましょう。
zig build-exe hello.zig -target x86_64-windows
標準のターゲット範囲内では追加のSDKインストールも設定ファイルも不要で、hello.exeが生成されます。中身を確認してみます。
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows
ちゃんとWindowsの実行形式(PE)です。このファイルをそのまま64bit Windowsにコピーすれば動作します。zig build-exeはターゲットに応じて拡張子(.exe)も自動で付けてくれます。
ターゲット指定は「CPUアーキテクチャ-OS(-ABI)」というトリプル形式です。主な指定例をまとめると次のとおりです。
| ターゲット指定 | 生成物 | 用途例 |
|---|---|---|
x86_64-windows |
PE (.exe) | 64bit Windows用CLIツール |
aarch64-macos |
Mach-O (arm64) | Apple Silicon Mac向けバイナリ |
x86_64-linux-musl |
静的リンクELF | glibcバージョンに依存しない配布バイナリ |
aarch64-linux-gnu |
ELF (ARM64) | Raspberry Pi・ARMサーバー向け |
wasm32-freestanding |
WebAssembly (.wasm) | ブラウザ・エッジ環境 |
実践2:Apple SiliconのmacOS向けバイナリを作るには?
同じ要領で、macOS(Apple Silicon)向けもビルドできます。
zig build-exe hello.zig -target aarch64-macos
file hello
$ file hello
hello: Mach-O 64-bit executable arm64
Linux上でビルドしたのに、arm64のMach-O実行ファイルができあがりました。Xcodeのインストールは不要です。ただし後述のとおり、Apple純正フレームワークを使うアプリのリンクにはApple SDKが別途必要であり、配布時のコード署名・公証もmacOS側で行う必要があります。
実践3:WASM(WebAssembly)向けにビルドするには?
続いてWebAssemblyです。WASMはwasm32-freestanding(OSなし環境)としてターゲット指定します。freestanding環境にはstdoutもlibcも存在しないため、mainで標準出力に書く代わりに、exportで関数を外部に公開する形にします。
// add.zig — OSのない環境(freestanding)向けの最小構成
export fn add(a: i32, b: i32) i32 {
return a + b;
}
zig build-exe add.zig -target wasm32-freestanding -fno-entry -O ReleaseSmall
-fno-entryはエントリーポイント(main)を持たないモジュールであることを指定するフラグ、-O ReleaseSmallはサイズ最適化です。生成されたadd.wasmをNode.jsから呼び出して動作確認できます。
$ node -e "WebAssembly.instantiate(require('fs').readFileSync('add.wasm')).then(r => console.log(r.instance.exports.add(1, 2)))"
3
このように、Zigで書いた1つの関数がブラウザやサーバーサイドWASMランタイムでそのまま動かせます。純粋なライブラリモジュールにするならzig build-lib add.zig -target wasm32-freestandingでも構いません。
実践4:musl静的リンクで配布しやすいLinuxバイナリを作るには?
クロスコンパイルのもう一つの実用シーンが「Linux向け配布バイナリ」です。Linux向けバイナリ配布の頭痛の種といえば、ビルド環境のglibcバージョンに引き払われて古いディストリビューションで動かない問題です。
Zigなら、musl libcを静的リンクした単一バイナリを作ることでこれを回避できます。
zig build-exe hello.zig -target x86_64-linux-musl
ldd hello
$ ldd hello
not a dynamic executable
動的リンクが一切ない、glibcに依存しないバイナリが完成しました。多くのディストリビューションでそのまま動かしやすく、DockerイメージもFROM scratchで数MBに収まり、配布・デプロイがシンプルになります。
FROM scratch
COPY hello /hello
ENTRYPOINT ["/hello"]
CI(x86_64 Linux)からaarch64-linux-gnuやaarch64-linux-musl向けにビルドする運用も、この仕組みの応用でよく見られます。
既存のCプロジェクトをそのままクロスビルドできる?——zig ccのドロップイン利用
ここまでZigコードのビルドを見てきましたが、Zigの真骨頂はC/C++コンパイラとしてもそのまま使えることです。zig cc/zig c++は内蔵clangをgcc/clang互換のドライバーとして動かすサブコマンドで、MakefileやconfigureスクリプトのCCを差し替えるだけで機能します [1]。
まずは単一のCファイルをクロスビルドしてみます。
// hello.c
#include <stdio.h>
int main(void) {
printf("Hello from zig cc!\n");
return 0;
}
zig cc -target aarch64-linux-gnu hello.c -o hello-arm # ARM64 Linux向け
zig cc -target x86_64-windows hello.c -o hello.exe # Windows向け
そして既存プロジェクトのビルドは、慣習的なconfigure & makeに対してCC環境変数を差し替えるだけです。
CC="zig cc" ./configure && make
configureスクリプトを含む著名OSS——PostgreSQL、libcurl、zlib、libpng、ffmpegなど——が、そのビルドスクリプトのままzig ccでコンパイルに成功したことが公式に報告されています [1]。C言語資産のクロスプラットフォーム配布を、ツールチェーン構築なしで実現できるのがZigの大きな強みです。C++プロジェクトならCXX="zig c++"を使います。
クロスコンパイルで注意すべきポイントは?
「簡単」の裏には、いくつか知っておくべき制約もあります。
- macOS向けの制約 — 単純なCLIはそのまま動きますが、Apple純正フレームワーク(GUIなど)のリンクにはApple SDK(ライセンス上の再配布不可)が別途必要です。配布時のコード署名・公証もmacOS側で行う必要があります
- Windows ABIはmingwベース —
x86_64-windowsはmingw-w64(GNU ABI)でビルドされます。MSVC ABIとの完全互換(MSVC製ライブラリとのリンク等)はいまだ発展途上です - クロスリンクの可否はターゲット・リンク対象・OS要件に依存 — 標準ターゲット外のライブラリや、実行して挙動を確かめるタイプのconfigure検査は、クロス環境では原理的に失敗します(Zigに限らないクロスビルド一般の話です)
- Zig自体が1.0前 — バージョン間でフラグや標準ライブラリのAPIが変わることがあります。本記事の内容は0.16.0時点のものです [2]
それでもなお、ツールチェーン構築なしで-target指定だけでWindows・macOS・WASM・musl静的バイナリを揃えられる体験は、C/C++資産のメンテナーにとって一度味わうと戻れなくなるものです。次のセクションでは、入門者がつまずきやすいポイントをFAQ形式でまとめます。
よくある疑問と注意点(FAQ)——Zig入門者のつまずきポイントに答える
Zigを学び始めると、多くの人が似たような疑問やつまずきに遭遇します。ここでは、入門者から特に多い質問にQ&A形式でお答えします。
Q1. Zigは1.0になった?本番環境で使えるの?
2026年9月上旬時点の最新安定版は0.16.0(2026年4月13日リリース)で、まだ1.0には到達していません [2]。Zigは1.0未満であることを公式にも明記している言語で、マイナーバージョン間でも言語仕様や標準ライブラリに破壊的変更が入ることがあります [1][2]。
では本番で使えないのかというと、必ずしもそうとは限りません。Bun(JavaScriptランタイム)やTigerBeetle(分散データベース)、Uberのh3ライブラリなど、Zigを採用したプロダクトが知られています [3]。一方で、これらは言語のバージョンアップに追従する工数を払えるチームであることが共通しているとされます。
実務で検討する場合の目安としては、次のような判断が一つの考え方です。
- バージョンを固定して運用できる — 破壊的変更の影響を受けにくい
- C資産の移行・共存が目的 —
zig ccや@cImport経由なら段階的に試せる [1] - 長期保守が前提の大規模プロジェクト — 1.0後の安定化を待つ選択も有力
「すぐ1.0になるか」については確定的なスケジュールが公表されていないため、公式のロードマップ情報を定期的に確認することをおすすめします。
Q2. 0.14以前の入門記事のコードが動かないのはなぜ?
Zig入門で最もつまずきやすいのがこのポイントです。原因の多くは、0.15で標準ライブラリのI/Oインターフェースが大幅に刷新された(通称「Writergate」と呼ばれる変更)ことにあります [2]。この変更で、std.io.getStdOut().writer()のような旧来の書き方が動かなくなり、std.fs.File.stdout()系の新しいAPIに置き換えられました。
たとえばHello Worldは、0.14以前の記事では次のようなコードとして紹介されています。
// 0.14系以前の書き方(0.15以降では動かない例)
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{"Zig"});
}
0.16系では次のように書きます。
// 0.16系の書き方
const std = @import("std");
pub fn main() !void {
var stdout_writer = std.fs.File.stdout().writer(&.{});
try stdout_writer.interface.print("Hello, {s}!\n", .{"Zig"});
}
I/O以外にも、アロケータまわりの再編(GeneralPurposeAllocatorからDebugAllocatorへの移行など)が進んでいます [2]。学習の際は、参考にする記事やドキュメントがどのバージョンを対象にしているかをまず確認するのが、手間を減らすコツです。エラーが出たら、自分のzig versionの結果と照らし合わせて公式ドキュメントで最新の書き方を確認するとよいでしょう。
Q3. Cのライブラリはそのまま使える?
基本的には使えます。Zigには@cImportというビルトイン関数があり、Cのヘッダファイルをコンパイル時に読み込んで、そのままZigのコードから呼び出せます。clangを内蔵しているため、別途バインディング生成ツールを用意する必要がないのが特徴です [1]。
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
_ = c.printf("Hello from C library!\n");
}
さらに、コンパイラとしての側面も強力です。zig ccはC/C++コンパイラとしてgcc/clangの代わりにそのまま使えるため、PostgreSQL・libcurl・ffmpegなどの著名なOSSが、既存のビルドスクリプトをほぼ変更せずにzig ccでビルドできたという実績が公式に報告されています [1]。
CC="zig cc" ./configure && make
注意点としては、Cのマクロまわりの解釈やプラットフォーム依存の設定で工夫が要るケースがある程度です。ただし「既存のC資産を捨てずに少しずつZigを混ぜていく」という移行戦略は、Zigが公式に想定している使い方の一つです [1]。
Q4. RustとZig、どちらを学ぶべき?
結論から言うと、目的によって適した選択は変わると考えられます。両者の大まかな違いは次のとおりです。
- Zig — 所有権・借用の概念がなく、Cに近い感覚で低レイヤーのコードが書けやすいとされる。C資産との相互運用とクロスコンパイルが強力 [1]。ただし1.0前で破壊的変更が続く [2]
- Rust — 所有権・借用・ライフタイムによる静的なメモリ安全性保証が得られる。エコシステムは成熟しており1.0以降の安定したリリースサイクルを持つ。一方で学習曲線は急になりがち
「低レイヤーのコードをCの延長線上で書きたい」「既存のCプロジェクトを段階的に移行したい」という場合はZigが向いているとされます [8]。「メモリ安全性をコンパイル時に強く保証したい」「安定した言語仕様の下で長期的な開発をしたい」という場合はRustが適している傾向があります。
どちらも実際に触ってみないと判断しにくい部分があるため、まずはHello Worldとクロスコンパイルを両方試してみて、書いていてしっくりくる方を選ぶというのも現実的なアプローチだと思います。学習コストだけで言えば、Cの経験があるならZigの入りは比較的スムーズとされます。
Q5. 学習に使えるリソースはどこにある?
Zigの学習リソースは、言語の若さもあって他のメジャー言語より少なめですが、質の高い公式リソースが揃っています [2]。
- 公式サイト日本語版(ziglang.org/ja-JP) — 言語概要とダウンロードの公式情報。日本語で読める [4]
- Zig LANGUAGE REFERENCE(公式言語リファレンス) — 言語仕様の一次情報。バージョンに対応した内容が掲載されるため、記事と挙動が違うときの確認先として有用
- Ziglings — 「壊れた小さなプログラムを直して進める」形式の演習集。GitHubで公開されており、手を動かしながら文法を学べる
- Zig SHOWTIME / Zig言語バーチャルミーティング — ZSFが毎週水曜に開催しているYouTubeライブ(SHOWTIME)と、隔週で開かれるユーザー向けミーティング。開発の最前線の様子が分かる [3]
日本語の記事もQiitaやZennで増えていますが、前述のとおり対象バージョンの確認が大切です。破壊的変更の情報は公式のリリースノートが最も確実なので、バージョンを上げるときは一読することをおすすめします [2]。
まとめ——「問題を修正したC」の中身は「明示性の徹底」
ここまで見てきたように、Zigが「Cの問題を修正した言語」と呼ばれる理由は、目新しい機能の追加ではなく明示性を徹底することにあります [1]。隠れた制御フローをなくし、メモリ確保をアロケータ引数で明示し、失敗しうる処理をエラーunionで型に表す。この原則は、FAQで扱った1.0前の破壊的変更や旧記事のコード差異といった「今のZigの課題」を理解するうえでも一貫しています。
入門の次の一歩としては、次のような流れがおすすめです。
- 公式tarballから最新安定版(0.16.0)をインストールする [2]
- Hello Worldと
zig testで基本のサイクルを体験する zig build-exe -targetでクロスコンパイルを実際に試すzig ccで既存のCプロジェクトをビルドしてみる [1]- Ziglingsで文法を体系的に固める
1.0前の言語であることを踏まえつつ、まずは自分の環境で動かして体感してみてください。「見た目どおりに動くコード」の手触りは、触って初めて分かる部分が大きいと思います。
参考文献
- Zig公式サイト(英語・ダウンロード/特徴): https://ziglang.org/
- 公式ダウンロードインデックス(バージョン・リリース日の実データ取得元): https://ziglang.org/download/index.json
- Zig (programming language) - Wikipedia: https://en.wikipedia.org/wiki/Zig_(programming_language)
- Zig公式サイト(日本語版): https://ziglang.org/ja-JP/
- Zig (プログラミング言語) - Wikipedia(日本語版): https://ja.wikipedia.org/wiki/Zig_(プログラミング言語)
- Zig Programming Language · GitHub: https://github.com/ziglang
- やさしく学ぶ Zig 入門ガイド - Qiita (2026-02-25): https://qiita.com/automation2025/items/f138062afee32d5b04e6
- Zigのすすめ Rustとの比較 - Qiita (2026-01-05): https://qiita.com/lechatthecat/items/d448727b817b7b03b535
- 5分でざっと理解するZig言語 - Zenn: https://zenn.dev/tetsu_koba/articles/2da58bd66586aa
関連記事
※ 本記事の技術的記述は主に一次資料(公式サイト・公式ダウンロードインデックス [1][2])に基づき、採用事例の記述はWikipedia [3] 等の二次資料に基づきます。実務判断の前に公式ドキュメントでの最新情報の確認をおすすめします。