Open18

zig study

mizchimizchi

bun を読む前に zig を読むべきだと思ったので、チュートリアルをやっていく

まず zig 言語自体のスタンスを確認。

https://ziglang.org/learn/why_zig_rust_d_cpp/

nostd

標準ライブラリなしでもファーストクラスでサポート
上で述べたように、Zigには全くオプションの標準ライブラリがあります。各標準ライブラリのAPIは、それを使用する場合にのみプログラムにコンパイルされます。Zigはlibcとリンクすることも、リンクしないことも等しくサポートしています。Zigはベアメタル開発にもハイパフォーマンス開発にも適しています。
例えば、Zigでは、WebAssemblyのプログラムは標準ライブラリの通常の機能を使うことができ、かつ、WebAssemblyへのコンパイルをサポートする他のプログラミング言語と比較して、最も小さなバイナリを生成することができるのです。

これは自分が必要がほしかった特性

言語仕様の方向性

C++、Rust、Dは、機能が多すぎて、作業しているアプリケーションの実際の意味から逸脱してしまうことがある。アプリケーションをデバッグするのではなく、プログラミング言語の知識をデバッグしている自分に気がつくのです。
Zigにはマクロもメタプログラミングもありませんが、それでも複雑なプログラムを明確かつ反復的でない方法で表現するには十分強力です。Rustでもformat!のような特殊なケースを想定したマクロがあり、これはコンパイラ自体に実装されています。一方Zigでは、同等の関数が標準ライブラリに実装されており、コンパイラに特殊なケースのコードはありません。

小さい言語仕様は好みなので、実際書いていって確かめる

mizchimizchi

Install

$ brew install zig
$ zig version
0.9.1

https://github.com/ziglang/vscode-zig をとりあえずいれた。

Hello Zig

とりあえずプロジェクトを作ってみる

https://ziglang.org/learn/getting-started/

$ mkdir zig-play
$ cd zig-play
$ zig init-exe

$ tree
.
├── build.zig
├── src
│   └── main.zig
├── zig-cache
│   ├── h
│   │   ├── 0969022837ccfb2d11c9cfc33ec55003.txt
│   │   ├── 59710624d3819effec2064c135b6fc45.txt
│   │   ├── 5e6364b7813fb0fa635b826f962c6b8b.txt
│   │   ├── b4995280bd6b1a0d71abfc3fb57b32f4.txt
│   │   └── timestamp
│   ├── o
│   │   ├── 4bae524aa50883cf87c3ac694d1d9df3
│   │   │   ├── builtin.zig
│   │   │   ├── stage1.id -> 4924eaa057b27d5e23a44ca0a4dece4f20
│   │   │   ├── zig-play
│   │   │   ├── zig-play.o
│   │   │   └── zld.id -> 957658e100f53183e2343c9f77118f4f
│   │   └── 7919845c1d4c352ffca0eb426f117fb0
│   │       ├── build
│   │       ├── build.o
│   │       ├── builtin.zig
│   │       ├── stage1.id -> 7fac22794f3322fea030f8cccc2fa37220
│   │       └── zld.id -> d591ebecdb7b40afc32b5c407ba34888
│   └── z
│       ├── 2d1fdc4cd88730da39ff42daede6a1cb
│       └── 4df82f8b3b717e74de2def4856d06651
└── zig-out
    └── bin
        └── zig-play
$ zig run src/main.zig
info: All your codebase are belong to us.
$ zig build run
$ zig-out/bin/zig-play
info: All your codebase are belong to us.
mizchimizchi

生成されたコード

src/main.zig
const std = @import("std");

pub fn main() anyerror!void {
    std.log.info("All your codebase are belong to us.", .{});
}

test "basic test" {
    try std.testing.expectEqual(10, 3 + 7);
}

commonjs require を思い出す感じのライブラリ読み込み

test "basic test" {
    try std.testing.expectEqual(10, 3 + 7);
}

流行りのインソーステストかな。

$ zig test src/main.zig 
All 1 tests passed.

わざと失敗させてみた

$ zig test src/main.zig
src/main.zig:4:5: error: use of undeclared identifier 'std'
    std.log.info("All your codebase are belong to us.", .{});
    ^
src/main.zig:8:9: error: use of undeclared identifier 'std'
    try std.testing.expectEqual(10, 3 + 7);
        ^
src/main.zig:12:9: error: use of undeclared identifier 'std'
    try std.testing.expectEqual(11, 3 + 7);
mizchimizchi

Getting Started からリンク貼られていた https://ziglearn.org/ を読んでいく

その前に色々 vscode 周りの設定した

.vsocde/settings.json
{
  "editor.defaultFormatter": "tiehuis.zig",
  "editor.formatOnSave": true,
  "zig.zigPath": "~/brew/bin/zig",
  "files.exclude": {
    "**/zig-cache/**": true,
    "**/zig-out/**": true
  }
}

型のキャストと明示的な変換

const constant: i32 = 5;  // signed 32-bit constant
var variable: u32 = 5000; // unsigned 32-bit variable

// @as performs an explicit type coercion
const inferred_constant = @as(i32, 5);
var inferred_variable = @as(u32, 5000);

配列

const a = [5]u8{ 'h', 'e', 'l', 'l', 'o' };
const b = [_]u8{ 'w', 'o', 'r', 'l', 'd' };

if 式

x += if (a) 1 else 2;

try 式。throw する関数はこの中でハンドルすることを強制される?

try expect(x == 1);

while 式

test "while" {
    var i: u8 = 2;
    while (i < 100) {
        i *= 2;
    }
    try expect(i == 128);
}

continue

test "while with continue" {
    var sum: u8 = 0;
    var i: u8 = 0;
    while (i <= 3) : (i += 1) {
        if (i == 2) continue;
        sum += i;
    }
    try expect(sum == 4);
}
mizchimizchi

for

test "for" {
    //character literals are equivalent to integer literals
    const string = [_]u8{ 'a', 'b', 'c' };

    for (string) |character, index| {
        _ = character;
        _ = index;
    }

    for (string) |character| {
        _ = character;
    }

    for (string) |_, index| {
        _ = index;
    }

    for (string) |_| {}
}

キャストがややこしかったので剥がしてみた

    const list = [_]u8{ 1, 2, 3 };
    var x: u8 = 0;
    for (list) |i| x += i;
    try expect(x == 6);
mizchimizchi

関数定義

fn addFive(x: u32) u32 {
    return x + 5;
}

test "function" {
    const y = addFive(0);
    try expect(@TypeOf(y) == u32);
    try expect(y == 5);
}

フィボナッチ

fn fibonacci(n: u16) u16 {
    if (n == 0 or n == 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

test "function recursion" {
    const x = fibonacci(10);
    try expect(x == 55);
}
mizchimizchi

defer ブロックを抜ける時に処理される。

test "defer" {
    var x: i16 = 5;
    {
        defer x += 2;
        try expect(x == 5);
    }
    try expect(x == 7);
}
mizchimizchi

エラー構造体。enum の特殊化っぽい。

const FileOpenError = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};

const AllocationError = error{OutOfMemory};

test "coerce error from a subset to a superset" {
    const err: FileOpenError = AllocationError.OutOfMemory;
    try expect(err == FileOpenError.OutOfMemory);
}

エラーセットは列挙型(Zigの列挙型の詳細は後述)のようなもので、セット内の各エラーが値になります。Zigには例外はなく、エラーは値です。では、エラーセットを作ってみましょう。

エラー周りはちょっと複雑っぽく、今見るより後で見返したほうが良さそうなので飛ばす

mizchimizchi

switch. switch というか実質パターンマッチ相当

test "switch statement" {
    var x: i8 = 10;
    switch (x) {
        -1...1 => {
            x = -x;
        },
        10, 100 => {
            //special considerations must be made
            //when dividing signed integers
            x = @divExact(x, 10);
        },
        else => {},
    }
    try expect(x == 1);
}

switch 文ではなく switch 式なのが嬉しい

test "switch expression" {
    var x: i8 = 10;
    x = switch (x) {
        -1...1 => -x,
        10, 100 => @divExact(x, 10),
        else => x,
    };
    try expect(x == 1);
}
mizchimizchi

ランタイム安全性を自分で外せるやつ。Rust の unsafe {}相当

test "out of bounds" {
    @setRuntimeSafety(false);
    const a = [3]u8{ 1, 2, 3 };
    var index: u8 = 5;
    const b = a[index];
    _ = b;
}
mizchimizchi

参照を取る

fn increment(num: *u8) void {
    num.* += 1;
}

test "pointers" {
    var x: u8 = 1;
    increment(&x);
    try expect(x == 2);
}

Rust みたいな安全性はなさそう。
.* は deref の意味っぽい

mizchimizchi

ちょっと飛ばして enum

const Direction = enum { north, south, east, west };
const Value = enum(u2) { zero, one, two };
test "enum ordinal value" {
    try expect(@enumToInt(Value.zero) == 0);
    try expect(@enumToInt(Value.one) == 1);
    try expect(@enumToInt(Value.two) == 2);
}

enum 構造体がメソッドを持てる

const Suit = enum {
    clubs,
    spades,
    diamonds,
    hearts,
    pub fn isClubs(self: Suit) bool {
        return self == Suit.clubs;
    }
};

test "enum method" {
    try expect(Suit.spades.isClubs() == Suit.isClubs(.spades));
}

.spades が Suit を略せてるのはなぜだろう。enum ネームスペースはこの時自明と言えるのかな

mizchimizchi

enum は実質構造体みたいに振る舞う

const Mode = enum {
    var count: u32 = 0;
    on,
    off,
};

test "hmm" {
    Mode.count += 1;
    try expect(Mode.count == 1);
}
mizchimizchi

構造体

経験上この辺から新しい言語は癖が出てくるのでがんばるぞ。

const Vec3 = struct {
    x: f32, y: f32, z: f32
};

test "struct usage" {
    const my_vector = Vec3{
        .x = 0,
        .y = 100,
        .z = 50,
    };
    _ = my_vector;
}

構造体はTSと違って構造体用の名前空間があるのではなく、同じ const で同じメモリ空間にいるっぽい?

全部揃わないと初期化できない。

const Vec3 = struct { x: f32, y: f32, z: f32 };

test "struct usage" {
    const my_vector = Vec3{
        .x = 0,
        // .y = 100,
        .z = 50,
    };
    _ = my_vector;
}
./src/main.zig:156:27: error: missing field: 'y'
    const my_vector = Vec3{
mizchimizchi

Union

const Result = union {
    int: i64,
    float: f64,
    bool: bool,
};

test "simple union" {
    var result = Result{ .int = 1234 };
    result.float = 12.34;
}
const Tag = enum { a, b, c };

const Tagged = union(Tag) { a: u8, b: f32, c: bool };

test "switch on tagged union" {
    var value = Tagged{ .b = 1.5 };
    switch (value) {
        .a => |*byte| byte.* += 1,
        .b => |*float| float.* *= 2,
        .c => |*b| b.* = !b.*,
    }
    try expect(value.b == 3);
}
mizchimizchi

zig の wasm ビルドをしたい

src/main.zig
export fn add(a: i32, b: i32) i32 {
    return a + b;
}

なんか色々試した感じ、 export 宣言が必要だった(pub とは違うのか?)

deno でwasm実行するコードを用意する。

run.ts
const m = await WebAssembly.instantiate(await Deno.readFile('main.wasm'));
console.log(m.instance.exports.add(1, 2));

コンパイルして実行

$ zig build-lib src/main.zig -target wasm32-freestanding-musl -dynamic -O ReleaseSmall --export=add
# main.wasm が生成される
$ deno run --allow-read --no-check run.ts
3

中身をみると、ほぼ単純な add 関数になっており、余計なランタイムがない。

$ wasm2wat main.wasm
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    local.get 1
    local.get 0
    i32.add)
  (memory (;0;) 1)
  (global (;0;) (mut i32) (i32.const 65536))
  (export "memory" (memory 0))
  (export "add" (func 0)))

wasm32-wasi

標準入出力を wasi で出力してみる

const std = @import("std");

pub fn main() anyerror!void {
    std.log.info("Hello", .{});
}
$ zig build-exe src/main.zig -target wasm32-wasi
$ wasmtime main.wasm
info: Hello

さすがに wasi インターフェースを持つと 22k と膨らむ。

mizchimizchi

vscode-zls(zig lsp) の導入

LSP を導入したら快適になったのだが、導入が結構面倒だったのでメモ

Mac の Homebrew で Head で入れて、自力でビルドする必要があった。

# HEAD を入れないと zls がビルドできなかった
$ brew install zig --HEAD
$ zig version
0.10.0-dev.3007+6ba2fb3db

# ソースコードからビルド
$ git clone https://github.com/zigtools/zls-vscode ~/zls
$ cd ~/zls
$ zig build

# バイナリに実行権限を足しておく
$ chmod +x zig-out/bin/zls

この zls コマンドは jsonrpc を喋り、vscode がそれをハンドルする。

次に zls の LSP コマンドを受け取る vscode 拡張を追加する。 vscode marketplace ではなく、 https://github.com/zigtools/zls-vscode/releases から最新の vsix をダウンロードする。

vscode のコマンドパレットから Extensions: Install from VSIX を選択し、↑ を指定することでインストールされる。

次に .vscode/settings.json で有効化する

.vscode/settings.json
{
  "zls.path": "/Users/mizchi/zls/zig-out/bin/zls"
}

このとき、~ のHOME のパスを指定すると解決できなかったので、絶対パスで入力する必要があった。

この状態でリロードすると補完が効くようになった。

今回はローカルの .vscode/settings.json に入れたが、User のグローバル設定に入れてしまってもよいかもしれない。

mizchimizchi

ライブラリを追加する。パッケージマネージャーが乱立してるが、現状決定打はなく、公式のチュートリアルでは手元で clone して使う例が書かれている。

https://ziglearn.org/chapter-3/

手元に clone する

git clone https://github.com/Sobeston/table-helper libs/table-helper

これを build.zig で addPackagePath として追加

build.zig
    const exe = b.addExecutable("zig-play", "src/main.zig");
    exe.addPackagePath("table-helper", "libs/table-helper/table-helper.zig");

この table-helper を main.zig から使ってみる

src/main.zig
const std = @import("std");
const Table = @import("table-helper").Table;

pub fn main() !void {
    try std.io.getStdOut().writer().print("{}\n", .{
        Table(&[_][]const u8{ "Version", "Date" }){
            .data = &[_][2][]const u8{
                .{ "0.7.1", "2020-12-13" },
                .{ "0.7.0", "2020-11-08" },
                .{ "0.6.0", "2020-04-13" },
                .{ "0.5.0", "2019-09-30" },
            },
        },
    });
}

zig run src/main.zig だと build.zig をみないようで、zig build run する必要があった。

$ zig build run
Version Date       
------- ---------- 
0.7.1   2020-12-13 
0.7.0   2020-11-08 
0.6.0   2020-04-13 
0.5.0   2019-09-30 

zig のパッケージレジストリ

https://astrolabe.pm/
https://aquila.red/

現状はあんまりこの辺依存せず、git clone したほうが良さそう