Zig言語入門ガイド - C言語を超える次世代システムプログラミング言語
Zigは、C言語の代替を目指すモダンなシステムプログラミング言語です。シンプルさ、パフォーマンス、安全性を重視した設計により、低レイヤーからアプリケーション層まで幅広い用途で利用できます。
本記事では、Zigの基本文法からコンパイル時実行、メモリ安全性、C言語との相互運用まで、Zigの魅力を徹底解説します。
Zigとは?
ZigはAndrew Kelleyにより開発されたシステムプログラミング言語で、以下の特徴を持ちます。
主な特徴
- シンプルな文法: 隠れた制御フローがなく、コードの動作が明確
- コンパイル時実行: コンパイル時にコードを実行して最適化
- メモリ安全性: エラーハンドリングを強制し、未定義動作を排除
- C言語との相互運用: Cライブラリを直接呼び出し可能
- クロスコンパイル: 追加ツール不要で各種プラットフォームをビルド
- パッケージマネージャ内蔵: 依存関係管理が組み込み済み
Zigの位置づけ
高レベル言語 (Python, JavaScript)
↓
システム言語 (Go, Rust)
↓
低レベル言語 (C, C++, Zig)
↓
アセンブリ
ZigはC/C++と同じ低レベル制御が可能ながら、モダンな機能を備えています。
Zig環境のセットアップ
インストール
macOS (Homebrew):
brew install zig
Linux:
# バイナリダウンロード
wget https://ziglang.org/download/0.12.0/zig-linux-x86_64-0.12.0.tar.xz
tar xf zig-linux-x86_64-0.12.0.tar.xz
export PATH=$PATH:$(pwd)/zig-linux-x86_64-0.12.0
Windows:
# Scoopを使用
scoop install zig
バージョン確認
zig version
# 0.12.0
Hello World
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, Zig!\n", .{});
}
実行:
zig run hello.zig
# Hello, Zig!
Zigの基本文法
変数宣言
Zigではconst(不変)とvar(可変)を明示的に区別します。
const x: i32 = 42; // 不変変数
var y: i32 = 10; // 可変変数
y = 20; // OK
// 型推論
const name = "Zig"; // []const u8型
const count = 100; // comptime_int型(コンパイル時整数)
// 未定義の値
var z: i32 = undefined; // 初期化されていない(明示的)
基本的なデータ型
// 整数型
const a: i8 = -128; // 8ビット符号付き整数
const b: u8 = 255; // 8ビット符号なし整数
const c: i32 = -2147483648;
const d: u64 = 18446744073709551615;
// 浮動小数点
const pi: f32 = 3.14159;
const e: f64 = 2.71828;
// 真偽値
const isTrue: bool = true;
// 文字列(スライス)
const str: []const u8 = "Hello";
// 配列
const arr: [5]i32 = [_]i32{ 1, 2, 3, 4, 5 };
関数
// 基本的な関数
fn add(a: i32, b: i32) i32 {
return a + b;
}
// エラーを返す可能性のある関数
fn divide(a: f64, b: f64) !f64 {
if (b == 0.0) {
return error.DivisionByZero;
}
return a / b;
}
// ジェネリック関数
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
// 使用例
pub fn main() !void {
const sum = add(5, 3);
const result = try divide(10.0, 2.0);
const maximum = max(i32, 10, 20);
}
制御フロー
// if式
const x = 10;
const result = if (x > 5) "big" else "small";
// while ループ
var i: u32 = 0;
while (i < 10) : (i += 1) {
// 処理
}
// for ループ
const items = [_]i32{ 1, 2, 3, 4, 5 };
for (items) |item| {
// 各要素を処理
}
// switch文
const value: u8 = 2;
const output = switch (value) {
0 => "zero",
1 => "one",
2 => "two",
else => "other",
};
構造体
const Point = struct {
x: f64,
y: f64,
pub fn init(x: f64, y: f64) Point {
return Point{ .x = x, .y = y };
}
pub fn distance(self: Point) f64 {
return @sqrt(self.x * self.x + self.y * self.y);
}
};
pub fn main() !void {
const p = Point.init(3.0, 4.0);
const dist = p.distance(); // 5.0
}
コンパイル時実行(comptime)
Zigの最も強力な機能の1つがコンパイル時実行です。comptimeキーワードを使うと、コードがコンパイル時に実行されます。
コンパイル時計算
fn fibonacci(n: u32) u32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
pub fn main() void {
// コンパイル時に計算
const fib10 = comptime fibonacci(10);
// コンパイル後のバイナリには結果の55が埋め込まれる
}
ジェネリックプログラミング
fn ArrayList(comptime T: type) type {
return struct {
items: []T,
capacity: usize,
pub fn init(allocator: std.mem.Allocator) !@This() {
return @This(){
.items = &[_]T{},
.capacity = 0,
};
}
pub fn append(self: *@This(), item: T) !void {
// 実装
}
};
}
pub fn main() !void {
const IntList = ArrayList(i32);
const StrList = ArrayList([]const u8);
}
コンパイル時リフレクション
const std = @import("std");
fn printFieldNames(comptime T: type) void {
inline for (@typeInfo(T).Struct.fields) |field| {
std.debug.print("{s}\n", .{field.name});
}
}
const User = struct {
name: []const u8,
age: u32,
email: []const u8,
};
pub fn main() void {
printFieldNames(User);
// 出力:
// name
// age
// email
}
メモリ管理とアロケータ
Zigにはガベージコレクションがありません。代わりに明示的なアロケータを使用します。
アロケータの基本
const std = @import("std");
pub fn main() !void {
// アロケータの作成
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// メモリ確保
const buffer = try allocator.alloc(u8, 100);
defer allocator.free(buffer);
// 使用
buffer[0] = 42;
}
ArenaAllocator
スコープ終了時に一括解放するアロケータ:
const std = @import("std");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// 複数回確保してもdeferは1回だけ
const data1 = try allocator.alloc(u8, 100);
const data2 = try allocator.alloc(u8, 200);
const data3 = try allocator.alloc(u8, 300);
// arena.deinit()で全て解放される
}
FixedBufferAllocator
スタック上の固定サイズバッファを使用:
const std = @import("std");
pub fn main() !void {
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const data = try allocator.alloc(u8, 100);
// ヒープ確保なし、高速
}
エラーハンドリング
Zigのエラーハンドリングは明示的で、例外機構はありません。
エラー定義
const FileError = error{
AccessDenied,
NotFound,
OutOfMemory,
};
fn openFile(path: []const u8) FileError![]const u8 {
if (path.len == 0) {
return error.NotFound;
}
return "file contents";
}
エラーハンドリング
const std = @import("std");
pub fn main() !void {
// tryキーワード: エラーを呼び出し元に伝播
const content = try openFile("test.txt");
// catchでエラーをキャッチ
const content2 = openFile("test.txt") catch |err| {
std.debug.print("Error: {}\n", .{err});
return;
};
// エラーユニオン型の検査
if (openFile("test.txt")) |content3| {
// 成功時の処理
} else |err| {
// エラー時の処理
}
}
エラーの追跡
// エラースタックトレース
const result = openFile("test.txt") catch |err| {
@errorReturnTrace(); // スタックトレースを取得
return err;
};
C言語との相互運用
ZigはC言語との相互運用が非常に簡単です。
Cライブラリのインポート
const c = @cImport({
@cInclude("stdio.h");
@cInclude("stdlib.h");
});
pub fn main() void {
_ = c.printf("Hello from C!\n");
}
C関数の呼び出し
// SQLiteの使用例
const c = @cImport({
@cInclude("sqlite3.h");
});
pub fn main() !void {
var db: ?*c.sqlite3 = null;
const result = c.sqlite3_open(":memory:", &db);
defer _ = c.sqlite3_close(db);
if (result != c.SQLITE_OK) {
return error.DatabaseError;
}
// SQLiteを使用
}
ZigからCへのエクスポート
// Zigコードをライブラリとしてエクスポート
export fn add(a: i32, b: i32) i32 {
return a + b;
}
// ビルド
// zig build-lib mylib.zig -dynamic
パフォーマンス比較
Zigの最適化レベル
# デバッグビルド
zig build-exe main.zig
# リリースビルド(最速)
zig build-exe -O ReleaseFast main.zig
# リリースビルド(サイズ最小)
zig build-exe -O ReleaseSmall main.zig
# リリースビルド(安全性チェック付き)
zig build-exe -O ReleaseSafe main.zig
ベンチマーク例
フィボナッチ数列の計算(n=40):
| 言語 | 実行時間 | メモリ使用量 |
|---|---|---|
| C (gcc -O3) | 0.45s | 1.2MB |
| Zig (ReleaseFast) | 0.44s | 1.1MB |
| Rust (—release) | 0.46s | 1.3MB |
| Go | 1.20s | 2.5MB |
ZigはC言語と同等の速度を実現しています。
実践例: HTTPサーバー
const std = @import("std");
const net = std.net;
pub fn main() !void {
const address = try net.Address.parseIp("127.0.0.1", 8080);
var server = try address.listen(.{
.reuse_address = true,
});
defer server.deinit();
std.debug.print("Server listening on http://127.0.0.1:8080\n", .{});
while (true) {
const conn = try server.accept();
defer conn.stream.close();
var buffer: [1024]u8 = undefined;
const bytes_read = try conn.stream.read(&buffer);
const response =
\\HTTP/1.1 200 OK
\\Content-Type: text/html
\\
\\<h1>Hello from Zig!</h1>
;
_ = try conn.stream.write(response);
}
}
実行:
zig run server.zig
# Server listening on http://127.0.0.1:8080
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 = "myapp",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// 依存関係の追加
exe.linkSystemLibrary("c");
exe.linkSystemLibrary("sqlite3");
b.installArtifact(exe);
// テストステップ
const test_step = b.step("test", "Run tests");
const tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
});
test_step.dependOn(&tests.step);
}
ビルド:
zig build
zig build test
まとめ
Zigは以下のような特徴を持つ強力なシステムプログラミング言語です。
Zigの強み
- シンプルで明確: 隠れた制御フローがなく、コードが読みやすい
- コンパイル時実行: 強力なメタプログラミング機能
- C言語との互換性: 既存のCライブラリを簡単に利用可能
- メモリ安全性: エラーハンドリングを強制
- クロスコンパイル: 追加ツール不要で各種プラットフォームに対応
適用場面
- システムプログラミング
- 組み込みソフトウェア
- ゲームエンジン開発
- コマンドラインツール
- パフォーマンスクリティカルなアプリケーション
学習リソース
Zigは、C言語の代替として、またはRustと並ぶ選択肢として、今後さらに注目を集めるでしょう。シンプルで強力な言語を求める開発者にとって、Zigは最適な選択肢の1つです。