はじめに:Zig ってどんな言語?
Zig は、C に代わる次世代のシステムプログラミング言語として設計された、静的型付け・コンパイル型の 低レベル 言語です。 メモリ管理を自動化するガベージコレクタを持たず、開発者が明示的にリソースを扱えるため、OS、ゲームエンジン、組込みなど性能重視の分野で注目されています。 ziglang
第1章 Zig のインストール(Windows / Unix) ziglang
この記事を読み進める前に、まず Zig コンパイラをインストールして、コマンドラインから zig を実行できる状態にしておきましょう。 ここでは、公式バイナリを使ったシンプルな方法を中心に、Windows と Unix 系(Linux / macOS など)の両方で、初心者でもつまずきにくい手順を順を追って説明します。 ziglang
Windows に Zig をインストールする手順
- ブラウザで公式サイトのダウンロードページ(
https://ziglang.org/download/)を開き、Windows用の最新バージョン(通常はzig-x86_64-windows-*.zipのようなファイル)をダウンロードします。 ziglang - ダウンロードした ZIP ファイルを右クリックし、「すべて展開」から解凍し、
C:\zigなど自分がわかりやすい場所にフォルダごと配置します(途中で移動するとパスが変わるので注意してください)。 reddit - スタートメニューで「環境変数」と検索し、「システム環境変数の編集」→「環境変数」と進み、「Path」を選んで「編集」をクリックします。 ziglang
- 「新規」を押して、解凍した Zig フォルダ内の
zig.exeがあるパス(例:C:\zigまたはC:\zig\bin)を追加し、ダイアログをすべて「OK」で閉じます。 zig - 新しく「コマンドプロンプト」や「PowerShell」を開き、
zig versionと入力して実行し、バージョン番号が表示されればインストールは成功です。 reddit
この方法は ZIP を展開してパスを通すだけなので、お試しで使いたいときにも向いています。 もし将来的にアップデート管理を簡単にしたい場合は、scoop などのパッケージマネージャからインストールする方法もあり、ziglang/zig の Wiki に各種パッケージマネージャのコマンド例がまとめられています。 github
Unix 系(Linux / macOS など)に Zig をインストールする手順
- 公式ダウンロードページから、自分の OS とアーキテクチャ(例:
Linux x86_64やmacOS aarch64)に対応したアーカイブファイル(.tar.xzなど)を選んでダウンロードします。 ziglang - ターミナルを開き、ダウンロードディレクトリに移動してから
tar xf zig-*.tar.xzのように解凍し、解凍されたディレクトリを~/zigや/opt/zigなど任意の場所に移動します(管理者権限がある場合は/usr/local/zigなどもよく使われます)。 gist.github - シェルの設定ファイル(
~/.bashrc、~/.zshrcなど)に次の 1 行を追記して、PATHに Zig を追加します。ここでexport PATH="$PATH:/path/to/zig"/path/to/zigは、zig実行ファイルが置かれているディレクトリに置き換えてください(解凍したディレクトリそのものか、配下のbinディレクトリであることが多いです)。 zig - 設定を反映するために
source ~/.bashrcなどを実行するか、新しくターミナルを開き直してから、zig versionを実行し、バージョンが表示されれば完了です。 gist.github
macOS の場合は、Homebrew を使って brew install zig とする方法もあり、こちらはアップデート管理が簡単で、環境変数の設定も自動で行われます。 Linux でも各ディストリビューション向けにパッケージが用意されていることがあるため、「ディストリのパッケージ」→「公式バイナリ」の順に試してみると、メンテナンスと最新性のバランスを取りやすくなります。 formulae.brew
インストール手順が完了し、zig version の結果が表示できるようになったら、続く章で紹介するサンプルコードを実際にコンパイル・実行しながら学んでいきましょう。 ziglang
第2章 Zig の特徴と設計思想
Zig の最大の特徴は「安全性」「予測可能な性能」「シンプルな文法」を同時に目指している点です。 例外機構をなくし、エラーを値として扱うことで、制御フローが見えやすく、コンパイル時に多くの問題を検出できます。 dev
Zig は C と高い互換性を持ちつつ、未定義動作を極力減らし、「違法な振る舞い(illegal behavior)」として明示することで、最適化と安全性を両立しています。 またコンパイル時実行(comptime)や、単一コンパイルユニット最適化などにより、高速で一貫したバイナリを生成しやすい設計になっています。 en.wikipedia
第3章 最初の Zig プログラム:Hello, World
ここでは Zig の基本構造をつかむために、最小のプログラムを書いてみます。 ziglang
const std = @import("std");
// エントリポイント。!void は「エラーを返すかもしれないが値は返さない」という意味。
pub fn main() !void {
// stdout(標準出力)へのハンドルを取得
const stdout = std.io.getStdOut().writer();
// print で文字列を出力。{s} は「UTF-8 文字列スライス」のフォーマット指定子
try stdout.print("Hello, Zig World!\n", .{});
}
Zig のエントリポイントは pub fn main() !void という形がよく使われ、「pub=公開」「fn=関数」「!void=エラーを返す可能性のある関数」という意味を持ちます。 try は「エラーならそのまま呼び出し元に返す」という構文で、Zig のエラー処理スタイルを象徴するキーワードです。 dev
第4章 基本文法:型と変数と定数
Zig では変数と定数を明示的に宣言し、型も基本的には明示します。 変数は var、定数は const を使うのが基本です。 ziglang
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// 整数型 i32 の可変変数
var count: i32 = 10;
// 不変の定数。型推論により i32 と解釈される
const max = 100;
// ブール値
const is_active: bool = true;
// 浮動小数点数
var rate: f64 = 0.15;
// 文字列スライス(UTF-8)
const message: []const u8 = "Zig basics";
try stdout.print(
"count={d}, max={d}, active={any}, rate={d:.2}, msg={s}\n",
.{ count, max, is_active, rate, message },
);
// 変数の更新
count += 5;
rate *= 2.0;
try stdout.print(
"updated count={d}, rate={d:.2}\n",
.{ count, rate },
);
}
Zig の整数型は i32(符号付き32ビット)、u8(符号なし8ビット)など、ビット幅を明示する名前になっていて、サイズや符号を意識したプログラミングがしやすくなっています。 文字列は「ヌル終端文字列」ではなく、長さ付きのバイト列スライスとして扱われるため、UTF-8 文字列処理でも安全に取り回しができます。 ziglang
第5章 制御構文:if・while・for
Zig は C に似た制御構文を持ちながら、より安全で表現力のある記法を備えています。 if は式としても利用でき、while や for ループも強力なパターンをサポートしています。 ziglang
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var x: i32 = 7;
// if は式として値を返せる
const parity = if (x % 2 == 0) "even" else "odd";
try stdout.print("x={d} is {s}\n", .{ x, parity });
// while ループ
var i: usize = 0;
while (i < 5) : (i += 1) {
try stdout.print("while i={d}\n", .{ i });
}
// for ループ(配列の要素を列挙)
const values = [_]i32{ 3, 1, 4, 1, 5 };
var sum: i32 = 0;
for (values) |v, idx| {
sum += v;
try stdout.print("index={d}, value={d}, partial_sum={d}\n", .{ idx, v, sum });
}
// switch 式
const grade: u8 = 82;
const rank = switch (grade) {
90...100 => "A",
80...89 => "B",
70...79 => "C",
60...69 => "D",
else => "F",
};
try stdout.print("grade={d} => rank={s}\n", .{ grade, rank });
}
Zig の while (cond) : (post) という構文は、「条件判定」と「ループ後処理」を明確に分けた表現で、インクリメントの位置が明示されるため可読性が高くなります。 また switch 式は網羅性チェックがあり、パターンを漏らすとコンパイルエラーになるため、安全な分岐が書きやすいのも利点です。 ziglang
第6章 関数定義と戻り値・エラー型
Zig の関数は fn キーワードで定義し、戻り値の型を後ろに書きます。 戻り値にエラーを含めたい場合は「エラー・ユニオン型」と呼ばれる error{...}!T や !T を使うのが特徴です。 dev
const std = @import("std");
// 独自のエラー集合を定義
const MathError = error{
NegativeInput,
};
fn squareRoot(x: f64) MathError!f64 {
if (x < 0) {
return MathError.NegativeInput;
}
// Zig では標準ライブラリの数学関数を利用
return std.math.sqrt(x);
}
// エラーを返さない通常の関数
fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const s = add(3, 4);
try stdout.print("3 + 4 = {d}\n", .{ s });
// 正常ケース
const r1 = try squareRoot(9.0);
try stdout.print("sqrt(9.0) = {d:.3}\n", .{ r1 });
// エラーケースを catch で処理
const r2 = squareRoot(-1.0) catch |err| {
try stdout.print("squareRoot error: {any}\n", .{ err });
// デフォルト値を返す
return;
};
// ここには到達しない(上で return しているため)
_ = r2;
}
ここで MathError は、関数で発生しうるエラー値の集合を型として定義したものです。 squareRoot の戻り値 MathError!f64 は「MathError のいずれかのエラー」か「f64 の値」のどちらかを返すことを表し、呼び出し側は try や catch を使って扱います。 dev
第7章 標準ライブラリと入出力
Zig の標準ライブラリ(std)は、文字列処理、ファイル I/O、メモリ割り当て、データ構造など、システムプログラミングに必要な機能を幅広くカバーしています。 特徴的なのは、すべてが明示的な API として提供され、暗黙のグローバル状態や例外に依存しない構成になっている点です。 ziglang
const std = @import("std");
pub fn main() !void {
const stdin = std.io.getStdIn().reader();
const stdout = std.io.getStdOut().writer();
try stdout.print("Your name? ", .{});
// 行バッファ
var buf: [128]u8 = undefined;
// 1 行読み込み(末尾の改行を含む)
const line = try stdin.readUntilDelimiterOrEof(&buf, '\n');
// line は ?[]u8(ヌル可能スライス)なので、null チェック
if (line) |name_slice| {
try stdout.print("Hello, {s}!\n", .{ name_slice });
} else {
try stdout.print("No input.\n", .{});
}
}
ここでは std.io.getStdIn()/getStdOut() で標準入出力ストリームにアクセスし、readUntilDelimiterOrEof で 1 行入力を受け取っています。 Zig は「ヌル可能型」や「オプション型」を通じて、値が存在しないケースを型レベルで表現できるため、入出力のような失敗しやすい処理でも安全なコードを書きやすくなっています。 en.wikipedia
第8章 配列・スライス・文字列の扱い
Zig では配列(固定長)とスライス(可変長ビュー)を区別して扱います。 文字列も実体はバイト列なので、「[]const u8(読み取り専用バイトスライス)」として扱われるのが基本です。 ziglang
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// 固定長配列
var nums = [ziglang](https://ziglang.org/download/)i32{ 1, 2, 3, 4, 5 };
// スライスを作成
var slice: []i32 = nums[1..4]; // 要素 2,3,4
try stdout.print("slice len={d}\n", .{ slice.len });
// スライスの要素を更新
for (slice) |*p| {
p.* *= 2;
}
// 全体を表示
for (nums, 0..) |v, idx| {
try stdout.print("nums[{d}] = {d}\n", .{ idx, v });
}
// 文字列(UTF-8)
const msg: []const u8 = "こんにちは Zig";
// バイトとして列挙
for (msg, 0..) |b, i| {
try stdout.print("byte[{d}] = {d}\n", .{ i, b });
}
}
[N]T は長さ N の配列、[]T は長さ情報を持つスライスで、[start..end] のように範囲指定することで配列からスライスを切り出せます。 文字列は実際には u8 の配列なので、1 文字=1 バイトとは限らず、日本語のようなマルチバイト文字では UTF-8 の扱いに注意が必要です。 ziglang
第9章 構造体 struct とメソッド的関数
Zig の構造体(struct)は複数のフィールドをまとめる基本的なユーザー定義型で、メソッドに相当する関数も「第一引数に self を取る通常の関数」として記述します。 オブジェクト指向言語のようなクラスはありませんが、同様の表現力をシンプルな仕組みで実現しています。 en.wikipedia
const std = @import("std");
const Vec2 = struct {
x: f64,
y: f64,
// “メソッド”的な関数。self は &Vec2 型のポインタ
fn length(self: *const Vec2) f64 {
return std.math.sqrt(self.x * self.x + self.y * self.y);
}
fn add(self: *const Vec2, other: Vec2) Vec2 {
return Vec2{
.x = self.x + other.x,
.y = self.y + other.y,
};
}
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var a = Vec2{ .x = 3.0, .y = 4.0 };
const b = Vec2{ .x = -1.0, .y = 2.0 };
const len = a.length();
try stdout.print("len(a) = {d:.3}\n", .{ len });
const c = a.add(b);
try stdout.print("c = ({d:.2}, {d:.2})\n", .{ c.x, c.y });
}
ここでは Vec2 構造体に対して length と add という関数を定義しており、第一引数の self により、オブジェクトに紐づく操作のように振る舞います。 Zig はクラスや継承を持ちませんが、構造体と関数、コンポジションを組み合わせることで、より明示的で柔軟な設計スタイルを推奨しています。 ziglang
第10章 コンパイル時計算 comptime の活用
Zig の大きな魅力のひとつが comptime によるコンパイル時計算で、これによりテンプレート的なコード生成や静的検証を簡潔に実現できます。 comptime は「この値やブロックはコンパイル時に評価されるべき」とコンパイラに指示するキーワードです。 ziglang
const std = @import("std");
// 配列の長さをコンパイル時に決める関数
fn makeArray(comptime N: usize, value: i32) [N]i32 {
var arr: [N]i32 = undefined;
var i: usize = 0;
while (i < N) : (i += 1) {
arr[i] = value;
}
return arr;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// N は comptime 引数なので定数でなければならない
const arr5 = makeArray(5, 42);
const arr3 = makeArray(3, -1);
try stdout.print("arr5: ", .{});
for (arr5) |v| {
try stdout.print("{d} ", .{ v });
}
try stdout.print("\n", .{});
try stdout.print("arr3: ", .{});
for (arr3) |v| {
try stdout.print("{d} ", .{ v });
}
try stdout.print("\n", .{});
}
この例では配列長 N を comptime 引数として受け取り、コンパイル時に異なる長さの配列型 [N]i32 を生成しています。 C++ のテンプレートに近い表現力を、より単純で直感的な仕組みで提供しているのが Zig の comptime の特徴で、静的反射(@TypeOf など)とも組み合わせると強力です。 en.wikipedia
第11章 エラー処理:try・catch・error set
Zig のエラー処理は、例外ではなく「エラー値」を返す形で行われ、コンパイル時に網羅性がチェックされるため、見通しのよい堅牢なコードを書きやすくなっています。 try は「エラーなら即座に呼び出し元に伝播」、catch は「その場でエラーを扱う」ための構文です。 dev
const std = @import("std");
// ファイル関連エラーをまとめたエラー集合
const AppError = error{
ConfigNotFound,
InvalidConfig,
} || std.fs.File.OpenError || std.fs.File.ReadError;
fn readConfig(path: []const u8) AppError![]u8 {
const allocator = std.heap.page_allocator;
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
// ここでは単純化のためバリデーションは省略
return content;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const cfg = readConfig("config.toml") catch |err| switch (err) {
error.FileNotFound => {
try stdout.print("Config file not found.\n", .{});
return;
},
error.InvalidConfig => {
try stdout.print("Config file is invalid.\n", .{});
return;
},
else => {
try stdout.print("Unexpected error: {any}\n", .{ err });
return;
},
};
defer std.heap.page_allocator.free(cfg);
try stdout.print("Config loaded, size={d} bytes\n", .{ cfg.len });
}
AppError のように error{...} 同士や標準ライブラリ定義のエラー型を || で合成できるため、関数単位で「起こりうるエラーの集合」を明確に表現できます。 dev また、Zig は「エラーリターントレース」という仕組みにより、リリースビルドでもオーバーヘッド少なく、どこでエラーが発生したかを追跡しやすい設計になっています。 dev
第12章 メモリ管理とアロケータ
Zig にはガベージコレクタがなく、メモリ管理はアロケータ(Allocator)インターフェースを通じて明示的に行います。 これにより、スタック・ヒープ・固定バッファなど、用途に応じた最適なメモリ戦略を選択できるのが特徴です。 ziglang
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// 汎用アロケータを生成
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 可変長配列 ArrayList を利用
var list = std.ArrayList(u32).init(allocator);
defer list.deinit();
try list.append(10);
try list.append(20);
try list.append(30);
try stdout.print("len={d}, cap={d}\n", .{ list.items.len, list.capacity });
for (list.items, 0..) |v, i| {
try stdout.print("list[{d}] = {d}\n", .{ i, v });
}
// 手動でバッファを確保
const buf = try allocator.alloc(u8, 256);
defer allocator.free(buf);
try stdout.print("allocated buffer size={d}\n", .{ buf.len });
}
ここで使っている GeneralPurposeAllocator は、デバッグや汎用用途に便利なアロケータで、内部でアロケーションの検証なども行います。 Zig のデータ構造(ArrayList、HashMap など)はアロケータを外から注入する設計になっており、同じコードを異なるメモリ戦略で再利用しやすいのが大きな利点です。 ziglang
第13章 テストとビルドシステム:zig test / zig build
Zig には言語レベルでテスト機構が組み込まれており、test ブロックと zig test コマンドで簡単にユニットテストを実行できます。 さらに、専用のビルドシステム(build.zig)により、C/C++ のような複雑なビルド設定をコードで記述できるのも特徴です。 ziglang
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
// ユニットテスト
test "add works" {
try std.testing.expect(add(2, 3) == 5);
try std.testing.expect(add(-1, 1) == 0);
}
// エントリポイント(通常実行用)
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const r = add(10, 20);
try stdout.print("10 + 20 = {d}\n", .{ r });
}
このファイルに対して zig test filename.zig を実行すると test ブロックだけが実行され、main は呼ばれません。 zig build 用の build.zig では、依存ライブラリやターゲット、最適化レベル、クロスコンパイル設定などを Zig コードとして表現でき、スクリプト的に柔軟なビルドフローを構築できます。 ziglang
第14章 C との相互運用:@cImport とヘッダ
Zig は C 互換を強く意識しており、既存の C ライブラリを容易に再利用できるよう @cImport 機能を提供しています。 これにより、C のヘッダファイルから関数や構造体を直接インポートし、ラッパーを書かずにそのまま Zig から呼び出すことが可能です。 en.wikipedia
const std = @import("std");
// C の標準ライブラリをインポート(例として puts を使用)
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// C 関数の呼び出し
_ = c.puts("Hello from C via Zig!");
try stdout.print("Zig and C interop.\n", .{});
}
このように @cInclude でヘッダファイルを指定すると、Zig コンパイラが C の宣言を解析し、Zig の型として利用できるようにします。 Zig のビルドシステムを使えば、C 源コードのコンパイルやリンクも一括で行えるため、「既存 C コード + 新規 Zig コード」という構成で少しずつ移行していくスタイルも取りやすくなっています。 ziglang
第15章 Zig らしい小さなアプリケーション
ここでは、標準入力から数値を読み取り、簡単な統計情報を表示する小さな CLI ツールを作り、Zig らしい記述パターンをまとめて体験してみます。 ziglang
const std = @import("std");
const AppError = error{
InvalidNumber,
};
fn readNumbers(allocator: std.mem.Allocator) AppError![]f64 {
const stdin = std.io.getStdIn().reader();
var list = std.ArrayList(f64).init(allocator);
errdefer list.deinit();
var buf: [128]u8 = undefined;
while (true) {
const line = try stdin.readUntilDelimiterOrEof(&buf, '\n');
if (line == null) break;
const slice = line.?;
if (slice.len == 0) continue;
const value = std.fmt.parseFloat(f64, slice) catch {
return AppError.InvalidNumber;
};
try list.append(value);
}
return list.toOwnedSlice();
}
fn calcStats(values: []const f64) ?struct {
mean: f64,
min: f64,
max: f64,
} {
if (values.len == 0) return null;
var sum: f64 = 0;
var mn = values[0];
var mx = values[0];
for (values) |v| {
sum += v;
if (v < mn) mn = v;
if (v > mx) mx = v;
}
return .{
.mean = sum / @as(f64, @floatFromInt(values.len)),
.min = mn,
.max = mx,
};
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try stdout.print("Enter numbers, one per line. Ctrl+D to finish.\n", .{});
const nums = readNumbers(allocator) catch |err| switch (err) {
error.InvalidNumber => {
try stdout.print("Error: invalid number input.\n", .{});
return;
},
else => return err,
};
defer allocator.free(nums);
const stats = calcStats(nums) orelse {
try stdout.print("No numbers.\n", .{});
return;
};
try stdout.print(
"count={d}, mean={d:.3}, min={d:.3}, max={d:.3}\n",
.{ nums.len, stats.mean, stats.min, stats.max },
);
}
このプログラムでは、エラー集合 AppError を定義し、標準入力から可変長の数値列を読み込む処理を ArrayList とアロケータで実装しています。 calcStats では「値がない場合は null を返す」というオプション型パターンを使い、呼び出し側が orelse で「なかった場合の振る舞い」を明示している点も Zig らしいスタイルです。 ziglang
第16章 これから Zig を学ぶために
Zig はまだ開発中の若い言語ですが、システムプログラミング分野での需要や、C/C++ からの移行先として、コミュニティが急速に成長しています。 公式サイトには言語リファレンスやチュートリアル、標準ライブラリ API などが充実しており、実際に手を動かしながら学ぶのに適した環境が整っています。 ziglang
学習を進める際は、まず本記事で紹介した基本文法と標準ライブラリに慣れ、その後 comptime やエラー処理、アロケータといった Zig 特有の概念を少しずつ取り入れていくと理解しやすくなります。 実プロジェクトでは、既存の C ライブラリと連携する小さなツールから始めてみると、Zig の強みを自然に体験できるでしょう。 ziglang