Open7
Zigに入門する
zigに入門してみる
簡単なHTTPサーバーを実装していく上で文法やお作法などは学んでいきたいと思う
すでにある程度作ってみたのでここまで学んだことをまず言語化したい
- zig自体はとりあえずmiseでインストールした(versionは2024/11/04現在最新の0.13.0)
- VS Codeでのセットアップは単に公式のZig Languageを入れただけ
- Language Serverはzlsのリポジトリをcloneして
zig build
- 注意点としてはインストールしたzigのバージョンと同じタグをチェックアウトすること
git checkout {{ タグ名 }}
-
.zshrc
に以下の設定を加えたらVSCodeで認識してくれるようになったexport PATH="$HOME/{{ クローンした場所 }}/zls/zig-out/bin:$PATH"
- 注意点としてはインストールしたzigのバージョンと同じタグをチェックアウトすること
- 適当にディレクトリを作成し、中で
zig init
すると雛形を作成してくれる
String
ここの説明を翻訳機に突っ込んでみた
文字列リテラルと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;
\\}
;
マルチラインはこのように書くらしい
ジェネリクスあたりも気になった
// 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);
構造体でジェネリクスを使おうとしたら構造体を返す関数を定義しないといけないそうな
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名を省略可能
}
戻り値を地味に省略できるの😍
Allocatorの選択について
まとめるとこんな感じか
用途 | Allocator |
---|---|
ライブラリ | ユーザー選択, AllocatorをDIさせるイメージ |
固定サイズ | FixedBufferAllocator |
CLI | ArenaAllocator, main関数初っ端で確保してプログラム終了時に一括解放 |
周期的 | ArenaAllocatorまたはFixedBufferAllocator, HTTPサーバーのコネクションアクセプトループ(表現が難しい) |
テスト | TestingAllocatorまたはFailingAllocator |
その他 | GeneralPurposeAllocator |
GeneralPurposeAllocatorは名前の割に最後の手段的なものなんだろうか
基本的にはArenaAllocatorを使って生成したallocatorをDIしていき、呼び出し側で都度defer deinit
していくのが良さそう
確かにbunのソースコードを検索しても1個しか引っかからない
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
これ使わない手はないなあ
もう少し深ぼっていきたい