Zig 言語のファーストインプレッション
Bun を読むにあたって、まずZigを抑える必要があると思ったので数時間学習してみた。チュートリアルを一通りやったのと、ちょっと手を動かした程度で、正直エアプの域は出てない。
自分の動機として wasm を吐くのに使う言語をずっと探していて、Rust も悪くないが正直学習コスト高すぎでしんどく、Zig がそれに足るか調査していたという感じ。
この記事を書くにあたっての細かい作業はこちら https://zenn.dev/mizchi/scraps/287b4414da2b29
Zig 言語自体のスタンス
まず Zig 言語自体がなぜ D や Rust ではないかはこの記事がわかりやすい https://ziglang.org/learn/why_zig_rust_d_cpp/
以下 Deepl で訳してちょっと修正したもの
nostd 指向
標準ライブラリなしでもファーストクラスでサポート
上で述べたように、Zigには全くオプションの標準ライブラリがあります。各標準ライブラリのAPIは、それを使用する場合にのみプログラムにコンパイルされます。Zigはlibcとリンクすることも、リンクしないことも等しくサポートしています。Zigはベアメタル開発にもハイパフォーマンス開発にも適しています。
例えば、Zigでは、WebAssemblyのプログラムは標準ライブラリの通常の機能を使うことができ、かつ、WebAssemblyへのコンパイルをサポートする他のプログラミング言語と比較して、最も小さなバイナリを生成することができるのです。
言語仕様の方向性
C++、Rust、Dは、機能が多すぎて、作業しているアプリケーションの実際の意味から逸脱してしまうことがある。アプリケーションをデバッグするのではなく、プログラミング言語の知識をデバッグしている自分に気がつくのです。
Zigにはマクロもメタプログラミングもありませんが、それでも複雑なプログラムを明確かつ反復的でない方法で表現するには十分強力です。Rustでもformat!のような特殊なケースを想定したマクロがあり、これはコンパイラ自体に実装されています。一方Zigでは、同等の関数が標準ライブラリに実装されており、コンパイラに特殊なケースのコードはありません。
インストールと Hello World
後述するが、HEAD でないとZLSが導入できなかったので、自分はHEAD でいれた。
$ brew install zig --HEAD
https://github.com/ziglang/vscode-zig で vscode にシンタックスハイライトが入る。
Hello Zig
とりあえずプロジェクトを作ってみる
zig init-exe
で build.zig
等のプロジェクト初期設定をしてくれる。
$ mkdir zig-play
$ cd zig-play
$ zig init-exe
実行
$ zig run src/main.zig
info: All your codebase are belong to us.
$ zig build
$ 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);
}
anyerorrは Rust の anyhow の輸入っぽい雰囲気。
test がインラインで書けるのは、好みはあるが最近の言語のはやりではある。
throw する可能性があるものは try で実行しないといけない、というのがわかりやすい。
VSCode のフォーマッタの設定
zig_exe_path を設定すると保存時のフォーマットが効くようになった。
{
"editor.defaultFormatter": "tiehuis.zig",
"editor.formatOnSave": true,
"zls.zig_exe_path": "~/brew/bin/zig",
"files.exclude": {
"**/zig-cache/**": true,
"**/zig-out/**": true
},
}
オプショナル: ZLS の導入
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 のグローバル設定に入れてしまってもよいかもしれない。
言語仕様
この2つを読むのがたぶん現状最速
Zig 言語の感想
最初は Rust サブセットという雰囲気だったが、学ぶにつれてモダン C という感じが強くなった。途中からオフサイドルールではない nim 言語じゃんという雰囲気になった。C 言語との連携がよくできていて、C 資産をモダンなツールチェインでまとめ上げるのにも使えるし、実際 Uber は C コンパイラとして使っているそう。
How Uber Uses Zig - Motiejus Jakštys Public Record
とにかく一貫してOOPではない感じで、構造体を引き回してクラスが存在しないのが、rust や go もそうだが世代が一周回った感じがある。自分はTSでもビルドサイズの都合でこういうコードを書く(書かざるをえない環境)ので、結構好きではある。
言語仕様はモダンな言語の機能をちゃんとピックアップしていて、いちいち強力だが、自明な範囲はここまでのはず、というショートハンドを要求されてる感じが強く、脳への負荷は高めに感じている。これは後述する LSP の不出来によるところもある。
メモリアロケータを自分で制御しろというのが、これが好みの人もいるだろうし、Rust の後に出てきた言語としては退化だと思う人もいるだろう。個人的な感覚として、unsafe まみれの Rust を書くぐらいだったら zig でも良いとは思ったし、実際そういう意見はいくつか調べてる間に見た。
生産性の面で、ちゃんと書くには ZLS を入れても LSP の補完が物足りない。補完や構文エラーのエディタサポートは貧弱。パッケージマネージャ不在なのがエコシステム面で結構しんどく、npm や cargo に慣れた世代としては不満がある。ただ Better C とすると十分すぎる、みたいなバランス。
ziglearn は役に立つし読むべきだが、出てくるのが作者が紹介したい順という感じで、網羅的に仕様を抑えるにはちょっと負荷が高かった。
で、結局自分が zig を使うかというと、エコシステムが充実して、LSP による支援が充実した段階で再検討という感じ。依存ゼロのプロダクトを趣味で作る場合は Rust の息苦しさみたいなのものがないので、もしかしたら選択するかもしれない。例えば自作TSコンパイラの https://github.com/mizchi/mints はJS実装の限界を感じているので、 Zig は全然候補になる。
自分の知る限り、 wasm で動く前提で小さいバイナリを依存ゼロで作る場合、Rust と同じぐらいよい選択肢になりそう。
おまけ: wasm を吐いてみた
これは単純なコードを wasm で吐いてみた例
export fn add(a: i32, b: i32) i32 {
return a + b;
}
これを wasm でビルドする(どこまでオプションが必要なのかまだわかってない)
$ zig build-lib src/main.zig -target wasm32-freestanding-musl -dynamic -O ReleaseSmall --export=add
生成される main.wasm の中身をみると、wast レベルでほぼ単純な add 関数になっており、余計なランタイムがない。 nostd が結構現実的に運用できそう。
$ 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)))
Discussion