Closed12

zig言語を触ってみる

nuskeynuskey

最近Zig言語が気になっているので、色々触りながら学んでみる。

執筆時点でのバージョン

  • zig 0.15.1
nuskeynuskey

環境構築

まずはzig本体をインストール。macOSなのでHomebrewから。

$ brew install zig

VSCodeで書いていくので、拡張機能も入れる。

https://marketplace.visualstudio.com/items?itemName=ziglang.vscode-zig

ZLS(zigのLSP)を追加で入れる必要があるという記事もみたが、今試した限りではVSCode拡張側でインストールしてくれる感じになっていた。

nuskeynuskey

Hello, World

まずはHello, Worldから。

const std = @import("std");

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

これをmain.zigに書いてzig runで実行。

$ zig run src/main.zig
Hello, World!
nuskeynuskey

FizzBuzz

ついでにFizzBuzzもやってみる。

const std = @import("std");

pub fn main() !void {
    var i: i32 = 1;

    while (i < 100) {
        if (i % 15 == 0) {
            std.debug.print("fizz buzz", .{});
        } else if (i % 3 == 0) {
            std.debug.print("fizz", .{});
        } else if (i % 3 == 0) {
            std.debug.print("buzz", .{});
        } else {
            std.debug.print("{}", .{i});
        }

        i += 1;
    }
}
  • 変数はvar、定数はconstで定義する
  • whileifは他の言語と大体一緒。括弧は必須っぽい

comptime_intについて

引っかかったところとして、整数はcomptime_intとして推論されるが、これはvarで宣言されたmutableな変数に対しては使えない。

$ zig run src/main.zig
src/main.zig:4:9: error: variable of type 'comptime_int' must be const or comptime
    var i = 1;
        ^
src/main.zig:4:9: note: to modify this variable at runtime, it must be given an explicit fixed-size number type

そのため、明示的にi32などを指定する必要がある。

nuskeynuskey

Allocator

Zigの最大の特徴であるAllocatorを使ってみる。ZigにはGCやARC、所有権モデルなどは存在せず、全てのメモリ管理を手動で行う必要がある。

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Allocatorでメモリを確保
    const buffer = try allocator.alloc(u8, 10);

    // deferでスコープを抜けたらbufferを解放する
    defer allocator.free(buffer);

    // bufferに値を書き込む
    for (buffer, 0..) |*byte, i| {
        byte.* = @intCast(i);
    }

    // bufferの内容を出力
    for (buffer) |byte| {
        std.debug.print("{d}\n", .{byte});
    }
}
  • std.heap以下のallocatorからヒープにメモリを確保できる
    • 他にもc_allocatorとかwasm_allocatorとか色々ありそう
  • エラーを送出する可能性のあるコードはtryで実行する。好き
  • deferがあるので、解放処理はこれで書く
  • forの構文が少し独特だが、慣れるとわかりやすくて良い
nuskeynuskey

Cの関数を呼ぶ

zig側からCの関数を呼んでみる。

Cの実装

まずは整数の足し算を行うだけのadd.cadd.hを作成する。

// add.c
int add(int a, int b)
{
    return a + b;
}
// add.h
int add(int a, int b);

これらのファイルを以下のように配置。

├── include/
│   └── add.h
├── src/
│   ├── add.c
│   └── main.zig
├── build.zig
└── build.zig.zon

build.zigの設定

次にbuild.zigで設定を行う。以下の2行をbuild関数の最後に追記。

exe.addCSourceFiles(.{ .files = &[_][]const u8{"src/add.c"} });
exe.addIncludePath(b.path("include"));

main.zig

最後に、main.zig@cImportからimportする。

const std = @import("std");

// Cライブラリをimport
const c = @cImport(
    @cInclude("add.h"),
);

pub fn main() !void {
    // addを呼び出す
    std.debug.print("{}", .{c.add(10, 20)});
}

実行時はzig src/main.zigだと@cImportが上手く動かず、zig build runを使ったら動いてくれた。ファイルパスを指定した実行だとbuild.zigの設定が反映されてなさそう。

nuskeynuskey

raylibを使ってみる

raylib@cImportで動かしてみる。

結構苦労して試行錯誤した結果、@Juhaさんのおかげで動かせました。感謝

raylibのインストール

今回はhomebrewからインストールした。

$ brew install raylib

build.zig

build.zigに以下の一行を追加。

exe.linkSystemLibrary("raylib");

main.zig

const std = @import("std");
const raylib = @cImport(
    @cInclude("raylib.h")
);

pub fn main() !void {
    raylib.InitWindow(800, 600, "Hello, Raylib!");
    defer raylib.CloseWindow();

    while (!raylib.WindowShouldClose()) {
        raylib.BeginDrawing();
        defer raylib.EndDrawing();

        raylib.ClearBackground(raylib.RAYWHITE);
        raylib.DrawText("Hello, Raylib!", 190, 200, 20, raylib.LIGHTGRAY);
    }
}

これをzig build runで実行すると、以下のウィンドウが表示される。

nuskeynuskey

comptime

Allocatorと並ぶもう一つの目玉機能、comptimeも試してみる。

const std = @import("std");

pub fn main() !void {
    // comptime addはコンパイル時に評価される
    std.debug.print("{}", .{comptime add(1, 2)});
}

fn add(a: i32, b: i32) i32 {
    return a + b;
}

zigでは型を値(type型)として扱えるので、comptimeと合わせてGenericsのようなものを実現できる。

const std = @import("std");

pub fn main() !void {
    std.debug.print("{}", .{add(i32, 1, 2)});
}

fn add(comptime T: type, a: T, b: T) T {
    return a + b;
}

控えめに言ってめちゃくちゃ良い。他言語にも欲しい。

nuskeynuskey

エラーハンドリング

エラーはRustのResult<T, E>に相当するものが言語機能として組み込まれている。

例として、自作したエラー型を返す関数を定義してみる。

const std = @import("std");

// error型の実体はほぼenum
const ExampleError = error{Example};

// エラーを返す関数
// 失敗する可能性のある関数の戻り値は{ErrorType}!{ResultType}で指定
// エラー型を省略した場合は推論される
fn foo() !u8 {
    return ExampleError.Example;
}

// anyerrorは全てのErrorを含有する型
// Rustのanyhowっぽい
pub fn main() anyerror!void {
    // 失敗する可能性のある関数の呼び出しにはtryが必要
    // Rustの?演算子のイメージ
    _ = try foo();

    // catchで失敗時のデフォルト値を指定できる(unwrap_orのイメージ)
    const result = foo() catch 10;
    std.debug.print("{}", .{result});
}
  • trycatchなどのキーワードを使うが、例外のような機構があるわけではなく、中身は実質的にResult
  • 言語機能として組み込まれている分、Resultを使うよりわかりやすく書けて良い
  • error型はunionを定義することもできるので、複数のエラーの合成も容易
  • エラー時専用にdefer処理を書けるerrdeferがある
nuskeynuskey

build.zig.zon

zig 0.11-0.13あたりで、build.zig.zonを用いたパッケージ管理のシステムが追加された。zig fetchコマンドで依存関係を追加できる。

例として、raylibのzigラッパーであるライブラリraylib-zigを追加してみる。

$ zig fetch --save git+https://github.com/raylib-zig/raylib-zig#devel

これを実行すると、build.zig.zonに以下が追加される。

.{
    // 省略
    .dependencies = .{
        .raylib_zig = .{
            .url = "git+https://github.com/raylib-zig/raylib-zig?ref=devel#d64fc43f38949231dc7d6f1c016db8fcae858b8c",
            .hash = "raylib_zig-5.6.0-dev-KE8REDc2BQCri1t11guC1tZA-Luc7NuVeml_59LSELLe",
        },
    },
}

続いて、build.zigにもいくつか追加する。

const raylib_dep = b.dependency("raylib_zig", .{
    .target = target,
    .optimize = optimize,
});

const raylib = raylib_dep.module("raylib"); // main raylib module
const raygui = raylib_dep.module("raygui"); // raygui module
const raylib_artifact = raylib_dep.artifact("raylib"); // raylib C library

exe.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib);
exe.root_module.addImport("raygui", raygui);

これでraylib-zigがimportできるようになる。

const raylib = @import("raylib");

pub fn main() anyerror!void {
    const screenWidth = 800;
    const screenHeight = 450;

    raylib.initWindow(screenWidth, screenHeight, "raylib-zig [core] example - basic window");
    defer raylib.closeWindow();

    raylib.setTargetFPS(60);
    
    while (!raylib.windowShouldClose()) { 
        raylib.beginDrawing();
        defer raylib.endDrawing();

        raylib.clearBackground(.white);

        raylib.drawText("Congrats! You created your first window!", 190, 200, 20, .light_gray);
    }
}

cargodotnetnpmなどに慣れている身としては、build.zig周りの設定が必要なのがちょっと辛い。とはいえ、現実的に運用できる範囲にはなっていそう。

nuskeynuskey

テスト

テスト専用の構文があり、インラインでテストが書ける。最近の言語っぽい感じ。

const std = @import("std");

pub fn main() anyerror!void {}

test "example" {
    try std.testing.expectEqual(3, 1 + 2);
}

test "example fail" {
    try std.testing.expectEqual(3, 1 + 1);
}

書いたテストはzig testで実行できる。

$ zig test src/main.zig
expected 3, found 2
2/2 main.test.example fail...FAIL (TestExpectedEqual)
/opt/homebrew/Cellar/zig/0.15.1/lib/zig/std/testing.zig:110:17: 0x1042847b3 in expectEqualInner__anon_510 (test)
                return error.TestExpectedEqual;
                ^
/Users/yusuke/Desktop/Projects/Sandbox/zig-sandbox/src/main.zig:10:5: 0x104284887 in test.example fail (test)
    try std.testing.expectEqual(3, 1 + 1);
    ^
1 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
.zig-cache/o/ef39a59f5151a7bb7eb77c45d7dd3c4e/test --seed=0xa5c784ca
nuskeynuskey

所感

Pros

  • C言語の正統進化といった趣のわかりやすい言語仕様
  • Rustよりはシンプルで習得しやすい
  • ResultやOptionに相当するものが言語機能として搭載されている
    • 専用の構文があるため、RustやGoに比べてわかりやすく書ける
  • comptimeはめっちゃ良い
    • マクロのようなわかりにくさや、C++のテンプレートのような複雑さがない
    • それでいて柔軟に対応できる設計
  • ポインタも含めて明示的なため、unsafe Rustを書くよりはかなりやりやすい印象

Cons

  • 手動メモリ管理は好みが分かれそう
    • 個人的には割と好きだが、規模が大きくなってきたときに大変そうではある
  • ビルド周りはかなりしんどい
    • 良くも悪くも明示的な仕様であり、自分で面倒を見なければならない範囲がかなり広い
    • CMakeよりはマシだと思うが...
  • LSP/VSCode拡張の完成度がまだ微妙
    • 基本的なエラーはエディタ上で見れるが、ほとんどのコンパイルエラーはビルドしてみないとわからない
    • build.zigとかはLSPがほとんど役に立たない
    • エラーメッセージも微妙なことが多い
    • 解析速度はrust-analyzerよりも速いので、補完などはかなり快適
  • まだ1.0に到達していないため、ガンガン破壊的変更が入る
    • サンプルコードやAIの書いたコードが動かないことが多々ある

総評として、better Cとしての方向性は素晴らしいが、ところどころ未完成な部分があり(特にLSP周り)、本格的に導入するには時期尚早な印象。言語自体はかなり良くできているので、1.0がリリースされたら低レイヤー周りを書くのに積極的に使っていきたい。

このスクラップは2日前にクローズされました