Open7

Zigに入門する

FKeisukeFKeisuke

zigに入門してみる
簡単なHTTPサーバーを実装していく上で文法やお作法などは学んでいきたいと思う

すでにある程度作ってみたのでここまで学んだことをまず言語化したい

https://github.com/kf3225/zbelto/tree/main

FKeisukeFKeisuke
  • zig自体はとりあえずmiseでインストールした(versionは2024/11/04現在最新の0.13.0)
  • VS Codeでのセットアップは単に公式のZig Languageを入れただけ
    extension
  • Language Serverはzlsのリポジトリをcloneしてzig build
    • 注意点としてはインストールしたzigのバージョンと同じタグをチェックアウトすること
      git checkout {{ タグ名 }}
      
    • .zshrcに以下の設定を加えたらVSCodeで認識してくれるようになった
      export PATH="$HOME/{{ クローンした場所 }}/zls/zig-out/bin:$PATH"
      
  • 適当にディレクトリを作成し、中でzig initすると雛形を作成してくれる
FKeisukeFKeisuke

String

https://ziglang.org/documentation/0.13.0/#toc-String-Literals-and-Unicode-Code-Point-Literals

ここの説明を翻訳機に突っ込んでみた

文字列リテラルとUnicodeコードポイントリテラル
文字列リテラルは、NULL終端バイト配列を指す単一項目の定数ポインタです。文字列リテラルの型は、長さとNULL終端であることの両方を符号化しており、そのためスライスとNULL終端ポインタの両方に型強制できます。文字列リテラルの参照外しを行うと、配列に変換されます。
ZigのソースコードはUTF-8でエンコードされているため、ソースコード内の文字列リテラルに現れる非ASCII文字バイトは、そのUTF-8の意味をそのままZigプログラム内の文字列の内容に持ち込みます。コンパイラによってバイトが変更されることはありません。\xNN表記を使用することで、非UTF-8バイトを文字列リテラルに埋め込むことが可能です。
非ASCII文字を含む文字列のインデックス参照は、UTF-8として有効かどうかに関係なく、個々のバイトを返します。
Unicodeコードポイントリテラルは、整数リテラルと同じcomptime_int型を持ちます。すべてのエスケープシーケンスは、文字列リテラルとUnicodeコードポイントリテラルの両方で有効です。

文字列リテラルは

  • NULL終端されたbyte配列の定数ポインタ
  • Dereferenceするとbyte配列になる
const bytes = "Hello";
const arr = bytes.*; // Dereference
std.debug.print("bytes: {any}\narr: {any}\n{d}", .{ @TypeOf(bytes), @TypeOf(arr), bytes[5] });
bytes: *const [5:0]u8
arr: [5:0]u8
0
  • *: ポインタ
  • const: 定数の
  • [5:0]: オフセット5で0(NULL終端、bytes[5]でアクセスした時の0)

byte配列の先頭アドレスとオフセットや型情報というメタデータを持った型を文字列リテラルとして扱うということか(?)

ちなみに

const hello_world_in_c =
    \\#include <stdio.h>
    \\
    \\int main(int argc, char **argv) {
    \\    printf("hello world\n");
    \\    return 0;
    \\}
;

マルチラインはこのように書くらしい

FKeisukeFKeisuke

ジェネリクスあたりも気になった

// You can return a struct from a function. This is how we do generics
// in Zig:
fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            prev: ?*Node,
            next: ?*Node,
            data: T,
        };

        first: ?*Node,
        last: ?*Node,
        len: usize,
    };
}

// omit

const list = LinkedList(i32){
    .first = null,
    .last = null,
    .len = 0,
};
try expect(list.len == 0);

構造体でジェネリクスを使おうとしたら構造体を返す関数を定義しないといけないそうな

FKeisukeFKeisuke

Tagged Unionも使えるので↑のジェネリクスと組み合わせればResult型のようなことができる模様
代数的データ型による実装も簡単にできそう

fn Result(comptime T: type, comptime E: type) type {
    return union(enum) { success: T, err: E };
}

const DivideResult = Result(f64, []const u8);

fn divide(a: f64, b: f64) DivideResult {
    if (b == 0) {
        return .{ .err = "division by zero" };
    }
    return .{ .success = a / b };
}
pub fn main() void {
    for ([_]DivideResult{ divide(1.0, 2.0), divide(1.0, 0.0) }, 0..) |result, i| {
        switch (result) {
            .success => |value| {
                std.debug.print("\n{d}: {d}\n", .{ i, value });
            },
            .err => |err| {
                std.debug.print("\n{d}: {s}\n", .{ i, err });
            },
        }
    }
}
0: 0.5

1: division by zero

あとあまり関係ないけど関数で戻り値が構造体の場合

fn divide(a: f64, b: f64) DivideResult {
    if (b == 0) {
        return DivideResult{ .err = "division by zero" }; // 省略せずに記載することもできる
    }
    return DivideResult{ .success = a / b }; // 省略せずに記載することもできる
}

とも書けるけど

fn divide(a: f64, b: f64) DivideResult {
    if (b == 0) {
        return .{ .err = "division by zero" }; // 戻り値のstruct名を省略可能
    }
    return .{ .success = a / b }; // 戻り値のstruct名を省略可能
}

戻り値を地味に省略できるの😍

FKeisukeFKeisuke

Allocatorの選択について

https://ziglang.org/documentation/0.13.0/#Choosing-an-Allocator

まとめるとこんな感じか

用途 Allocator
ライブラリ ユーザー選択, AllocatorをDIさせるイメージ
固定サイズ FixedBufferAllocator
CLI ArenaAllocator, main関数初っ端で確保してプログラム終了時に一括解放
周期的 ArenaAllocatorまたはFixedBufferAllocator, HTTPサーバーのコネクションアクセプトループ(表現が難しい)
テスト TestingAllocatorまたはFailingAllocator
その他 GeneralPurposeAllocator

GeneralPurposeAllocatorは名前の割に最後の手段的なものなんだろうか
基本的にはArenaAllocatorを使って生成したallocatorをDIしていき、呼び出し側で都度defer deinitしていくのが良さそう
確かにbunのソースコードを検索しても1個しか引っかからない

FKeisukeFKeisuke

comptimeが中々強力

https://ziglang.org/documentation/0.13.0/#comptime

これはコンパイル時に評価されてバイナリに直接埋め込まれるという仕組み(定数畳み込み)を利用していて

  • パフォーマンス
    • メモリアロケーションのオーバーヘッドがない
    • 実行時の文字列操作などが不要になる(コンパイル時に決定される)
  • 安全性
    • メモリ確保の失敗がない
    • メモリリークの心配がない
    • コンパイル時にエラーを検出可能
  • 最適化
    • 上で書いた通り、コンパイラが定数畳み込みなどの最適化を行える
    • 同じ文字列リテラルなどの重複を排除できる
pub fn main() !void {
    var arena = ArenaAllocator.init(default_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    // これらはすべて同じメモリ領域を指す可能性がある
    // "可能性がある"というのは最適化の結果であって必ずしも保証されているわけではないっぽい
    const str1 = "hello-world";
    const str2 = comptime "hello" ++ "-" ++ "world";
    const str3 = comptime std.fmt.comptimePrint("{s}-{s}", .{ "hello", "world" });
    // これは別メモリ領域を指す
    const str4 = try std.fmt.allocPrint(allocator, "{s}-{s}", .{ "hello", "world" });

    // アドレスを出力して確認
    std.debug.print("str1: {*}\n", .{str1.ptr});
    std.debug.print("str2: {*}\n", .{str2.ptr});
    std.debug.print("str3: {*}\n", .{str3.ptr});
    std.debug.print("str4: {*}\n", .{str4.ptr});
}
str1: u8@104f9bd5e
str2: u8@104f9bd5e
str3: u8@104f9bd5e
str4: u8@6000013700b0

これ使わない手はないなあ
もう少し深ぼっていきたい