zig study
bun を読む前に zig を読むべきだと思ったので、チュートリアルをやっていく
まず zig 言語自体のスタンスを確認。
nostd
標準ライブラリなしでもファーストクラスでサポート
上で述べたように、Zigには全くオプションの標準ライブラリがあります。各標準ライブラリのAPIは、それを使用する場合にのみプログラムにコンパイルされます。Zigはlibcとリンクすることも、リンクしないことも等しくサポートしています。Zigはベアメタル開発にもハイパフォーマンス開発にも適しています。
例えば、Zigでは、WebAssemblyのプログラムは標準ライブラリの通常の機能を使うことができ、かつ、WebAssemblyへのコンパイルをサポートする他のプログラミング言語と比較して、最も小さなバイナリを生成することができるのです。
これは自分が必要がほしかった特性
言語仕様の方向性
C++、Rust、Dは、機能が多すぎて、作業しているアプリケーションの実際の意味から逸脱してしまうことがある。アプリケーションをデバッグするのではなく、プログラミング言語の知識をデバッグしている自分に気がつくのです。
Zigにはマクロもメタプログラミングもありませんが、それでも複雑なプログラムを明確かつ反復的でない方法で表現するには十分強力です。Rustでもformat!のような特殊なケースを想定したマクロがあり、これはコンパイラ自体に実装されています。一方Zigでは、同等の関数が標準ライブラリに実装されており、コンパイラに特殊なケースのコードはありません。
小さい言語仕様は好みなので、実際書いていって確かめる
Install
$ brew install zig
$ zig version
0.9.1
https://github.com/ziglang/vscode-zig をとりあえずいれた。
Hello Zig
とりあえずプロジェクトを作ってみる
$ 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.
生成されたコード
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);
Getting Started からリンク貼られていた https://ziglearn.org/ を読んでいく
その前に色々 vscode 周りの設定した
{
"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);
}
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);
関数定義
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);
}
defer ブロックを抜ける時に処理される。
test "defer" {
var x: i16 = 5;
{
defer x += 2;
try expect(x == 5);
}
try expect(x == 7);
}
エラー構造体。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には例外はなく、エラーは値です。では、エラーセットを作ってみましょう。
エラー周りはちょっと複雑っぽく、今見るより後で見返したほうが良さそうなので飛ばす
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);
}
ランタイム安全性を自分で外せるやつ。Rust の unsafe {}相当
test "out of bounds" {
@setRuntimeSafety(false);
const a = [3]u8{ 1, 2, 3 };
var index: u8 = 5;
const b = a[index];
_ = b;
}
参照を取る
fn increment(num: *u8) void {
num.* += 1;
}
test "pointers" {
var x: u8 = 1;
increment(&x);
try expect(x == 2);
}
Rust みたいな安全性はなさそう。
.*
は deref の意味っぽい
ちょっと飛ばして 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 ネームスペースはこの時自明と言えるのかな
enum は実質構造体みたいに振る舞う
const Mode = enum {
var count: u32 = 0;
on,
off,
};
test "hmm" {
Mode.count += 1;
try expect(Mode.count == 1);
}
構造体
経験上この辺から新しい言語は癖が出てくるのでがんばるぞ。
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{
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);
}
zig の wasm ビルドをしたい
export fn add(a: i32, b: i32) i32 {
return a + b;
}
なんか色々試した感じ、 export 宣言が必要だった(pub とは違うのか?)
deno でwasm実行するコードを用意する。
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 と膨らむ。
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
で有効化する
{
"zls.path": "/Users/mizchi/zls/zig-out/bin/zls"
}
このとき、~
のHOME のパスを指定すると解決できなかったので、絶対パスで入力する必要があった。
この状態でリロードすると補完が効くようになった。
今回はローカルの .vscode/settings.json
に入れたが、User のグローバル設定に入れてしまってもよいかもしれない。
ライブラリを追加する。パッケージマネージャーが乱立してるが、現状決定打はなく、公式のチュートリアルでは手元で clone して使う例が書かれている。
手元に clone する
git clone https://github.com/Sobeston/table-helper libs/table-helper
これを build.zig で addPackagePath として追加
const exe = b.addExecutable("zig-play", "src/main.zig");
exe.addPackagePath("table-helper", "libs/table-helper/table-helper.zig");
この table-helper を 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 のパッケージレジストリ
現状はあんまりこの辺依存せず、git clone したほうが良さそう