解答2: 開発環境のセットアップと基本操作

概要

この解答では、Zigの開発環境構築、基本的なコマンドの使用方法、ファイル入出力、ビルドシステムについて実践的に学びます。

Part 1: インストール確認 - 解答

コマンド実行結果

# 1. Zigのバージョン確認
$ zig version
0.12.0

# 2. 利用可能なコマンドの表示
$ zig help
Usage: zig [command] [options]

Commands:

  build            Build project from build.zig
  init-exe         Initialize a `zig build` application in the cwd
  init-lib         Initialize a `zig build` library in the cwd

  ast-check        Look for simple compile errors in any set of files
  build-exe        Create executable from source or object files
  build-lib        Create library from source or object files
  build-obj        Create object from source or object files
  fmt              Reformat Zig source into canonical form
  run              Create executable and run immediately
  test             Perform unit testing
  translate-c      Convert C code to Zig code

  ar               Use Zig as a drop-in archiver
  cc               Use Zig as a drop-in C compiler
  c++              Use Zig as a drop-in C++ compiler
  dlltool          Use Zig as a drop-in dlltool.exe
  lib              Use Zig as a drop-in lib.exe
  ranlib           Use Zig as a drop-in ranlib
  objcopy          Use Zig as a drop-in objcopy

  env              Print lib path, std path, cache directory, and version
  help             Print this help and exit
  libc             Display native libc paths file or validate one
  targets          List available compilation targets
  version          Print version number and exit
  zen              Print Zen of Zig and exit

# 3. ターゲットプラットフォームの確認
$ zig targets | head -20
Available CPUs for architecture x86_64:
alderlake
amdfam10
athlon
athlon64
athlon64_sse3
athlon_4
athlon_fx
athlon_mp
athlon_tbird
athlon_xp
atom
barcelona
bdver1
bdver2
bdver3
bdver4
bonnell
broadwell
btver1

インストール確認のポイント

  • バージョン番号の確認: Zigは頻繁にアップデートされるため、最新の安定版を使用することが推奨されます
  • コマンド一覧の理解: 各コマンドの役割を把握することで、効率的な開発が可能になります
  • ターゲットプラットフォーム: クロスコンパイルに対応した豊富なターゲットが利用可能

Part 2: Hello World プログラム - 解答

hello.zig の実装

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, Zig!\n", .{});
}

実行方法と結果

1. zig run hello.zig

$ zig run hello.zig
Hello, Zig!

特徴:

  • 最も簡単な実行方法
  • コンパイルと実行を一度に行う
  • 開発中のテストに最適
  • 一時的なバイナリが生成され、実行後に削除される

2. zig build-exe hello.zig && ./hello

$ zig build-exe hello.zig
$ ./hello
Hello, Zig!

$ ls -lh hello
-rwxr-xr-x  1 user  staff   380K Dec 10 00:00 hello

特徴:

  • デバッグビルド(デフォルト)
  • 実行ファイルが残る
  • デバッグ情報が含まれるため、ファイルサイズが大きい
  • 開発中のデバッグに適している

3. zig build-exe -O ReleaseFast hello.zig && ./hello

$ zig build-exe -O ReleaseFast hello.zig
$ ./hello
Hello, Zig!

$ ls -lh hello
-rwxr-xr-x  1 user  staff    45K Dec 10 00:00 hello

特徴:

  • 最高速度で最適化
  • 実行ファイルサイズが小さい
  • 安全性チェックが無効化される
  • 本番環境向け
  • 最適化レベルの比較

    最適化レベル ファイルサイズ 実行速度 安全性チェック 用途
    Debug (デフォルト) 遅い 有効 開発・デバッグ
    ReleaseSafe 速い 有効 本番環境(安全性重視)
    ReleaseFast 最速 無効 本番環境(速度重視)
    ReleaseSmall 最小 速い 無効 組み込み環境

    Part 3: コマンドライン引数処理 - 解答

    const std = @import("std");
    
    pub fn main() !void {
        const allocator = std.heap.page_allocator;
    
        // コマンドライン引数を取得
        const args = try std.process.argsAlloc(allocator);
        defer std.process.argsFree(allocator, args);
    
        // プログラム名を表示
        std.debug.print("Program: {s}\n", .{args[0]});
    
        // 引数がない場合の処理
        if (args.len < 2) {
            std.debug.print("Usage: {s} <arg1> <arg2> ...\n", .{args[0]});
            return;
        }
    
        // 引数の個数を表示
        std.debug.print("Arguments: {}\n", .{args.len - 1});
    
        // すべての引数を表示
        for (args[1..], 0..) |arg, i| {
            std.debug.print("{}: {s}\n", .{i + 1, arg});
        }
    }
    

    実行例

    $ zig run args.zig -- hello world test
    Program: args
    Arguments: 3
    1: hello
    2: world
    3: test
    
    $ zig run args.zig
    Program: args
    Usage: args <arg1> <arg2> ...
    

    ポイント解説

  • アロケータの使用: コマンドライン引数は動的にメモリを確保する必要があるため、アロケータを使用します
  • defer文: defer std.process.argsFree(allocator, args); により、関数終了時に自動的にメモリが解放されます
  • スライス操作: args[1..] で最初の要素(プログラム名)をスキップし、実際の引数だけを処理します
  • エラーハンドリング: !void 返り値型により、エラーが発生する可能性を明示します
  • Part 4: ファイル入出力 - 解答

    const std = @import("std");
    
    pub fn writeFile(filename: []const u8, content: []const u8) !void {
        // ファイルを作成(既存の場合は上書き)
        const file = try std.fs.cwd().createFile(filename, .{});
        defer file.close();
    
        // 内容を書き込み
        try file.writeAll(content);
    }
    
    pub fn readFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 {
        // ファイルを開く(読み取り専用)
        const file = try std.fs.cwd().openFile(filename, .{});
        defer file.close();
    
        // ファイルサイズを取得
        const file_size = try file.getEndPos();
    
        // 内容を読み込み(最大10MB)
        const contents = try file.readToEndAlloc(allocator, 10 * 1024 * 1024);
        return contents;
    }
    
    pub fn main() !void {
        const allocator = std.heap.page_allocator;
    
        // テストデータ
        const test_content = "Hello from Zig!\nThis is a test file.\n";
    
        // 書き込み
        try writeFile("test_output.txt", test_content);
        std.debug.print("File written successfully.\n", .{});
    
        // 読み込み
        const content = try readFile(allocator, "test_output.txt");
        defer allocator.free(content);
    
        std.debug.print("File content:\n{s}", .{content});
    }
    

    エラーハンドリングの拡張版

    pub fn safeReadFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 {
        // ファイルを開く
        const file = std.fs.cwd().openFile(filename, .{}) catch |err| {
            std.debug.print("Failed to open file '{s}': {}\n", .{filename, err});
            return err;
        };
        defer file.close();
    
        // 内容を読み込み
        const contents = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch |err| {
            std.debug.print("Failed to read file '{s}': {}\n", .{filename, err});
            return err;
        };
    
        return contents;
    }
    

    ファイル操作のベストプラクティス

  • deferでリソース管理: ファイルハンドルは必ず defer file.close() で閉じる
  • エラーハンドリング: ファイル操作は失敗する可能性があるため、適切にエラーを処理
  • サイズ制限: readToEndAlloc には最大サイズを指定してメモリ枯渇を防ぐ
  • 相対パスと絶対パス: std.fs.cwd() は現在のディレクトリを基準とする
  • ボーナス課題 - 解答

    Bonus 1: build.zig の作成

    プロジェクト構造

    bonus1/
    ├── build.zig
    ├── src/
    │   ├── main.zig
    │   └── utils.zig
    └── test/
        └── utils_test.zig
    

    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,
        });
    
        // ビルド成果物のインストール
        b.installArtifact(exe);
    
        // `zig build run` の実装
        const run_cmd = b.addRunArtifact(exe);
        run_cmd.step.dependOn(b.getInstallStep());
    
        // コマンドライン引数の転送
        if (b.args) |args| {
            run_cmd.addArgs(args);
        }
    
        const run_step = b.step("run", "Run the application");
        run_step.dependOn(&run_cmd.step);
    
        // テストの実装
        const unit_tests = b.addTest(.{
            .root_source_file = .{ .path = "src/utils.zig" },
            .target = target,
            .optimize = optimize,
        });
    
        const run_unit_tests = b.addRunArtifact(unit_tests);
    
        const test_step = b.step("test", "Run unit tests");
        test_step.dependOn(&run_unit_tests.step);
    }
    

    src/main.zig

    const std = @import("std");
    const utils = @import("utils.zig");
    
    pub fn main() !void {
        const result = utils.add(10, 20);
        std.debug.print("10 + 20 = {}\n", .{result});
    
        const prod = utils.multiply(5, 6);
        std.debug.print("5 * 6 = {}\n", .{prod});
    }
    

    src/utils.zig

    pub fn add(a: i32, b: i32) i32 {
        return a + b;
    }
    
    pub fn multiply(a: i32, b: i32) i32 {
        return a * b;
    }
    
    test "add function" {
        const std = @import("std");
        try std.testing.expectEqual(@as(i32, 30), add(10, 20));
        try std.testing.expectEqual(@as(i32, -10), add(-5, -5));
    }
    
    test "multiply function" {
        const std = @import("std");
        try std.testing.expectEqual(@as(i32, 200), multiply(10, 20));
        try std.testing.expectEqual(@as(i32, 25), multiply(-5, -5));
    }
    

    実行方法

    # ビルド
    $ cd bonus1
    $ zig build
    
    # 実行
    $ zig build run
    10 + 20 = 30
    5 * 6 = 30
    
    # テスト実行
    $ zig build test
    All 2 tests passed.
    

    Bonus 2: クロスコンパイル

    calculator.zig

    const std = @import("std");
    
    pub fn main() !void {
        const allocator = std.heap.page_allocator;
        const args = try std.process.argsAlloc(allocator);
        defer std.process.argsFree(allocator, args);
    
        if (args.len != 4) {
            std.debug.print("Usage: {s} <num1> <op> <num2>\n", .{args[0]});
            std.debug.print("Operations: + - * /\n", .{});
            return;
        }
    
        const a = try std.fmt.parseInt(i32, args[1], 10);
        const op = args[2][0];
        const b = try std.fmt.parseInt(i32, args[3], 10);
    
        const result = switch (op) {
            '+' => a + b,
            '-' => a - b,
            '*' => a * b,
            '/' => @divTrunc(a, b),
            else => {
                std.debug.print("Unknown operator: {c}\n", .{op});
                return error.InvalidOperator;
            },
        };
    
        std.debug.print("{} {} {} = {}\n", .{a, op, b, result});
    }
    

    build_all.sh

    #!/bin/bash
    
    echo "Building for multiple targets..."
    
    # Linux x86_64
    zig build-exe -target x86_64-linux calculator.zig -femit-bin=calculator-linux-x64
    
    # Windows x86_64
    zig build-exe -target x86_64-windows calculator.zig -femit-bin=calculator-windows-x64.exe
    
    # Linux ARM64
    zig build-exe -target aarch64-linux calculator.zig -femit-bin=calculator-linux-arm64
    
    # macOS x86_64
    zig build-exe -target x86_64-macos calculator.zig -femit-bin=calculator-macos-x64
    
    # macOS ARM64 (Apple Silicon)
    zig build-exe -target aarch64-macos calculator.zig -femit-bin=calculator-macos-arm64
    
    echo ""
    echo "Binary sizes:"
    ls -lh calculator-*
    
    echo ""
    echo "File types:"
    file calculator-*
    

    実行結果

    $ chmod +x build_all.sh
    $ ./build_all.sh
    Building for multiple targets...
    
    Binary sizes:
    -rwxr-xr-x  1 user  staff   380K Dec 10 00:00 calculator-linux-arm64
    -rwxr-xr-x  1 user  staff   380K Dec 10 00:00 calculator-linux-x64
    -rwxr-xr-x  1 user  staff   380K Dec 10 00:00 calculator-macos-arm64
    -rwxr-xr-x  1 user  staff   380K Dec 10 00:00 calculator-macos-x64
    -rwxr-xr-x  1 user  staff   380K Dec 10 00:00 calculator-windows-x64.exe
    
    File types:
    calculator-linux-arm64:       ELF 64-bit LSB executable, ARM aarch64
    calculator-linux-x64:         ELF 64-bit LSB executable, x86-64
    calculator-macos-arm64:       Mach-O 64-bit executable arm64
    calculator-macos-x64:         Mach-O 64-bit executable x86_64
    calculator-windows-x64.exe:   PE32+ executable (console) x86-64
    

    Bonus 3: ZLS統合とエディタ設定

    ZLSのインストール確認

    $ zls --version
    0.11.0
    

    VS Code設定 (settings.json)

    {
      "zig.path": "/usr/local/bin/zig",
      "zig.zls.path": "/usr/local/bin/zls",
      "zig.initialSetupDone": true,
      "zig.checkForUpdate": false,
      "zig.formattingProvider": "zls",
      "[zig]": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "ziglang.vscode-zig"
      }
    }
    

    Neovim設定 (init.lua)

    -- LSP設定
    require'lspconfig'.zls.setup{
      cmd = {'/usr/local/bin/zls'},
      filetypes = {'zig'},
      root_dir = require'lspconfig'.util.root_pattern('build.zig', '.git'),
      settings = {
        zls = {
          enable_snippets = true,
          enable_ast_check_diagnostics = true,
          enable_autofix = true,
          enable_import_embedfile_argument_completions = true,
        }
      }
    }
    
    -- オートフォーマット
    vim.api.nvim_create_autocmd("BufWritePre", {
      pattern = "*.zig",
      callback = function()
        vim.lsp.buf.format()
      end,
    })
    

    ZLSの機能

  • コード補完: 変数、関数、型の自動補完
  • エラー表示: リアルタイムでコンパイルエラーを表示
  • 定義ジャンプ: 関数や型の定義にジャンプ
  • リファクタリング: 名前変更、インポートの自動追加
  • ホバー情報: 型情報やドキュメントを表示

Bonus 4: パフォーマンス比較

fibonacci.zig

const std = @import("std");

fn fibonacci(n: u64) u64 {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

pub fn main() !void {
    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);

    const n = if (args.len > 1)
        try std.fmt.parseInt(u64, args[1], 10)
    else
        40;

    const start = std.time.nanoTimestamp();
    const result = fibonacci(n);
    const end = std.time.nanoTimestamp();

    const duration = @as(f64, @floatFromInt(end - start)) / 1_000_000.0;

    std.debug.print("fibonacci({}) = {}\n", .{n, result});
    std.debug.print("Time: {d:.2} ms\n", .{duration});
}

benchmark.sh

#!/bin/bash

echo "Building with different optimization levels..."
echo ""

# Debug
echo "=== Debug Build ==="
zig build-exe -O Debug fibonacci.zig -femit-bin=fib-debug
time ./fib-debug 40
echo ""

# ReleaseSafe
echo "=== ReleaseSafe Build ==="
zig build-exe -O ReleaseSafe fibonacci.zig -femit-bin=fib-safe
time ./fib-safe 40
echo ""

# ReleaseFast
echo "=== ReleaseFast Build ==="
zig build-exe -O ReleaseFast fibonacci.zig -femit-bin=fib-fast
time ./fib-fast 40
echo ""

# ReleaseSmall
echo "=== ReleaseSmall Build ==="
zig build-exe -O ReleaseSmall fibonacci.zig -femit-bin=fib-small
time ./fib-small 40
echo ""

echo "Binary sizes:"
ls -lh fib-*

ベンチマーク結果

最適化レベル 実行時間 バイナリサイズ メモリ使用量
Debug 2800ms 380KB 1.2MB
ReleaseSafe 1200ms 180KB 1.0MB
ReleaseFast 850ms 45KB 1.0MB
ReleaseSmall 1100ms 38KB 1.0MB

benchmark_report.md

# パフォーマンス比較レポート

## テスト環境
- CPU: Intel Core i7-9750H @ 2.60GHz
- RAM: 16GB
- OS: macOS 14.0
- Zig: 0.12.0

## 測定結果

### 実行時間

Debug < ReleaseSafe < ReleaseSmall < ReleaseFast

- Debug: 2800ms (基準)
- ReleaseSafe: 1200ms (2.3倍高速)
- ReleaseFast: 850ms (3.3倍高速)
- ReleaseSmall: 1100ms (2.5倍高速)

### バイナリサイズ

ReleaseSmall < ReleaseFast < ReleaseSafe < Debug

- ReleaseSmall: 38KB
- ReleaseFast: 45KB
- ReleaseSafe: 180KB
- Debug: 380KB

## 分析

### 速度優先: ReleaseFast
- 最も高速だが、安全性チェックが無効
- 本番環境で速度が最優先の場合に使用

### バランス: ReleaseSafe
- 速度と安全性のバランスが良い
- 本番環境で推奨

### サイズ優先: ReleaseSmall
- 最小のバイナリサイズ
- 組み込み環境に最適

### 開発: Debug
- デバッグ情報が豊富
- エラー検出が有効

ポイント解説

1. ビルドシステムの重要性

build.zig を使用することで:

  • 複雑なビルドプロセスを管理
  • 依存関係の解決
  • テストの自動化
  • クロスコンパイルの簡略化

2. クロスコンパイルの利点

Zigの最大の特徴の一つがクロスコンパイルの容易さです:

  • 追加のツールチェーン不要
  • 単一のコマンドで複数プラットフォーム向けにビルド
  • Cライブラリもクロスコンパイル可能

3. 最適化レベルの選択

用途に応じて適切な最適化レベルを選択:

  • 開発中: Debug(エラー検出優先)
  • テスト: ReleaseSafe(速度と安全性)
  • 本番: ReleaseFast or ReleaseSafe
  • 組み込み: ReleaseSmall(サイズ優先)
  • よくある間違い

    1. deferを忘れる

    // 間違い(メモリリーク)
    const buffer = try allocator.alloc(u8, 1024);
    // bufferを使用...
    // allocator.free を忘れている!
    
    // 正しい
    const buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);
    

    2. エラーハンドリングを省略

    // 間違い
    const file = std.fs.cwd().openFile("test.txt", .{});  // コンパイルエラー!
    
    // 正しい
    const file = try std.fs.cwd().openFile("test.txt", .{});
    

    3. 相対パスの誤解

    // 注意: cwd()は実行時の作業ディレクトリ
    const file = try std.fs.cwd().openFile("data.txt", .{});
    
    // 絶対パスを使う場合
    const file = try std.fs.openFileAbsolute("/tmp/data.txt", .{});
    

    発展課題

    1. 非同期ファイルI/O

    Zigの非同期処理機能を使って、複数のファイルを並行して読み込むプログラムを作成してください。

    2. カスタムアロケータ

    メモリプールを使ったカスタムアロケータを実装し、パフォーマンスを比較してください。

    3. ビルドスクリプトの拡張

    build.zigにカスタムビルドステップを追加し、コード生成や自動テストを実装してください。

    4. Cライブラリとの統合

    既存のCライブラリをリンクし、Zigから呼び出すプロジェクトを作成してください。

    まとめ

    この課題を通じて、Zigの開発環境構築と基本的なツールの使用方法を習得しました。特に重要なポイント:

  • コマンドラインツール: zig run、build-exe、testなどの基本コマンド
  • ファイルI/O: エラーハンドリングとリソース管理
  • ビルドシステム: build.zigによる柔軟なビルド管理
  • クロスコンパイル: 簡単に複数プラットフォーム向けにビルド
  • 最適化: 用途に応じた最適化レベルの選択

これらの基礎を習得することで、より高度なZigプログラミングへの準備が整いました。