Zig探訪 - エコシステム編
イントロ
さあ、やって参りました。
第2回 Zig探訪 のお時間です。
今回担当するのは、ここ最近冷房が寒くてエアコンを消すと、今度は内部洗浄で湧き出てくる熱気のせいでまた部屋が暑くなることに悩まされている、社会人歴マイナス2年のsmallkirbyです。昨日Ubuntuを24.04にしました。まだ22.04使ってる人は反省してください。もう22.04からアップグレードできます。Ubuntuは最初のポイントリリースまでアップグレードできない印象あったんだけど、どういうこと?
Zig探訪では、Zigの機能や特徴の中で面白いんじゃないかと思うものをピックアップして紹介していきます。
紹介しないこともあります。
第2回のテーマは、Zigを取り囲むエコシステムについてです。
実際にコーディングをする上で開発体験に大きく影響を与える部分ですね。
最近の言語だと、ただ言語仕様自体が優れているだけでは不十分で、エコシステムが充実していることが求められます。
今回はZigを使ったコーディング環境について見ていきたいと思います。
パッケージマネージャ: あるにはある
Zigはv0.11.0からオフィシャルなパッケージマネージャを使うことが出来ます。
パッケージは ZON (Zig Object Notation) というZigの構造体として記述されます。
このパッケージマネージャ自体に名前はなさそうですが、このデータ形式の名前で呼ばれることが多い気がするため本記事でもパッケージマネージャを指してZonと呼ぶことにします。
Zonは言語システム自体に組み込まれており、Zigをインストールするだけで使うことが出来ます。
Zigはnpmやcrates.ioのようなオフィシャルなパッケージリポジトリを持っていません。
依存するパッケージは以下のようなbuild.zig.zon
ファイルに記述します:
.{
.name = "zakuro-os",
.version = "0.0.0",
.minimum_zig_version = "0.12.0",
.dependencies = .{
.chameleon = .{
.url = "https://github.com/tr1ckydev/chameleon/archive/a473ce38f5111dd4cb0d638cdc8339d05b9b808e.tar.gz",
.hash = "1220a420db55114e882dd069f3bfc421da4520a63c190fbde3634aa8813c21ec7660",
.lazy = false,
},
},
}
このbuild.zig.zon
ファイルは、「自分自身のパッケージ情報」と「依存するパッケージ情報」の両方を記述します。cargo.toml
とかpackage.json
と一緒ですね。
上の例の場合には、chameleonという文字列修飾ライブラリを依存関係に追加しています。URLに関してはGitHubのアセットを取ってこれるURLを指定すればいいです。問題はhash
の部分です。Multihashというフォーマットのハッシュらしいのですが、これの計算方法がよく分かりません。というか、そもそもに現在のところZonの公式ドキュメントは一切ありません。唯一の情報源は、zig init
した時に自動生成されるbuild.zig.zon
のコメントくらいです。
今の所一番ラクにハッシュを知る方法は、「エラーメッセージに怒られる」ことです。例として、上のzonファイルからhash
フィールド自体を削除してビルドすると、以下のように怒られます:
ハッシュを教えてくれましたね!やった〜〜〜〜〜〜。
まあこれはかなりめんどくさいので、GitHubのREADMEにハッシュを書いておいて貰えると、利用者側からするととても助かります:
ちなみに、このbuild.zig.zon
の必須フィールドもマイナーアップデートの度に変わるため、対応していないパッケージは使うことができなくなります。使いたい場合には、PRを出して追加されたフィールドを書き足してあげましょう。
というわけで、Zigのパッケージマネージャは「あるにはある」という状況です。普通に使えるには使えるのですが、ドキュメントの欠如やパッケージ側の対応も含め、まだまだ開発途上だなという印象ですね。依存を追加するならSubmoduleとして追加することもできるので、Zonが安定するまで待ちたいという人はそれでも良いかもしれません。
バージョンマネージャ: 非公式で使い勝手いいのがある
Zig自体のバージョン管理の話です。RustだとCargo、Node.jsだとn(非公式)みたいなやつです。
Zigは公式でバージョンマネージャを持っていません。まぁこれは言語によりけりで、寧ろバージョン管理を公式だったりエコシステムだったりに取り入れてる言語のほうが少ないのかもしれないですね。ほんまか?
非公式だと、ZVM[1]というマネージャがあります。これで十分です。
使い勝手はnと似ていて、zvm use <version>
とするだけでZigのバージョンを切り替えることが出来ます。便利だね:
Language Server: (多分)公式で、ある
ZigのLanguage ServerとしてZLSがあります。Zigのバージョンに追随しており、MicrosoftのLSPに準拠しています。エディタに依存しないため、VimでもVSCodeでもLSPをサポートする任意のエディタで使うことが出来ます。VSCodeの場合には ziglang.org/Zig Language という拡張にZLSサポートが入っているため快適に使うことが出来ます。シグネチャ情報もcompletionももちろん出ます:
但し、一つ注意事項として複数のRoot Moduleを持つソースツリーには対応していません。ここで言うRoot Moduleは、とりあえずexecutable fileとして認識しておけばOKです。ビルドシステムについては後述しますが、Zigでは一つのビルドファイル(ビルドツリー)の中で複数のArtifactを定義することが出来ます。しかしながら、ZLSは現在開いているファイルが属するRoot Moduleがどれなのかを識別することは出来ないわけです。そのため、以下のようなディレクトリ構成になっていた場合に:
├── build.zig
└── src
├── main.zig
└── mod_a.zig
以下のようにしてモジュールAをインポートすることは、複数のRoot Moduleが定義されている場合には出来ません:
const me = @import("root").mod_a;
そうしたい場合には、mod_a.zig
を(名前付き)モジュールとしてbuild.zig
に定義して、対象の実行ファイルに追加するしかありません。まぁ、そもそも原理的に不可能なのでしょうがないよね。
テスト: 言語に組み込まれてる、In-Fileに書ける、最高。
テストには2種類あります。同一ファイルに書けるRustのような言語と、わざわざGoogleTestとかいうサードパーティのライブラリを持ってきて別のファイルに書いてあげる必要がある名前を言ってはいけないあの言語のようなもの。ぼくは圧倒的に前者が好きです。Unit Testはテスト対象があるファイルの中に書いてあげたい。
Zigでは、test
キーワードを書くことでIn-Fileにテストを記述することが出来ます。最高です:
test "gate descriptor" {
const gate = GateDesriptor{
.offset_low = 0x1234,
.offset_middle = 0x9abc,
.offset_high = 0x0123def0,
};
try testing.expectEqual(0x0123def0_9abc_1234, gate.offset());
}
また、テスト用のアロケータとしてstd.testing.allocator
が用意されており、このAllocatorを使うとテスト終了時にメモリリークが起きてないかを勝手にチェックしてくれます(Zigでは関数がヒープを利用する場合には利用するAllocatorをcaller側が渡してあげる必要があります)。リークが起きていた場合には、どこで確保したメモリがリークしてるYOと教えてくれます。ありがたいですね。もちろん速度が気になる場合には通常のsanitizer無しのAllocatorを利用することも出来ます。あ、因みに上の例で出てきているようにZigでは整数リテラルを任意の長さにおいて_
で区切ることが出来ます。C++の'
と一緒だね。
注意点として、ZigではRootファイルから参照されるシンボルしか評価されません。例えばbuild.zig
でmain.zig
というファイルをソースとして指定したとしましょう。ZigではどこぞのC言語とは異なり、必要なソースファイルを全て指定する必要はなく、ルート1つだけを指定すれば十分で、勝手に参照されているファイルをコンパイルしてくれます。しかしながら、逆に言うとRootから辿れないファイルは評価されないため、そのようなファイルにあるtest
は実行されません:
// main.zig
const mod_a = @import("mod_a.zig");
pub fn main() !void {...}
// mod_a.zig
test "Tested" {...}
// mod_b.zig
test "Not Tested" {...}
上の例の場合、main.zig
でmod_a.zig
を参照しているためTested
テストは実行されますが、mod_b.zig
のNot Tested
テストは実行されません。先にモジュールだけ開発してまだmain
から呼び出す前にテストを書いたとしても実行されないことには注意しましょう。
それと関連して、Rootから参照されるファイルであってもRootから利用されないようなコード片に関してはsyntax的な評価だけが為され、semanticな評価が一切行われません。以下のように、mod_a.zig
に2つの構造体が定義されていたとしましょう:
// mod_a.zig
pub const Referenced = struct {
pub fn hello(n: u32) u32 {
return 1 << n;
}
};
pub const NotReferenced = struct {
pub fn hello(n: u32) u32 {
return 1 << n;
}
};
Zigではシフト演算のLHSはcomptimeな型を持っていないといけないのですが、1
のビット幅が指定されていないため本来はエラーになるはずです。しかし、main.zig
から参照・利用されるReferenced
ではエラーが発生するのに対し、参照されないNotReferenced
構造体ではエラーが発生しません。Referenced
の方だけ直せば、コンパイルすら通ってしまいます:
これもZigのコード評価方法の賜物です。因みに、同一ファイル中のコードを(たとえRootから参照されていなくても)評価してほしいときには以下のように書くことが出来ます:
std.testing.refAllDecls(@This());
こうすると@This()
に含まれる全て(とはいってもpub
なもののみ)が評価されるため、上の例ではNotReferenced
でもエラーが出るようになります。
他にZigのテストで不満なところは、テスト出力です。Zigのテストは、Passしたテストの一覧を表示してくれません。そのため、どのテストが実行されたかが分かりません :( 評価されないと実行されない性質と相まって、書いたテストが本当に実行されてるのか不安になっちゃいますね。これは改善が期待されるところです。
doc-string: 言語に組み込まれてる
Zigでは、割と珍しくドキュメント用コメントが言語仕様に組み込まれています。Zigには3タイプのコメントがあります:
-
normal comment:
//
で始まるコメントで、言語としては無視されます。他の言語で言うところのコメントです。 -
doc-comment:
///
で始まるコメントで、関数やら変数やらのドキュメントを書けます。 -
top-level doc-comment:
//!
で始まるコメントで、ファイルの先頭に書くことでモジュールやらパッケージやらのドキュメントを書けます。
コメントはMarkdownで書くことが出来ます:
また、ビルド時に-femit-docs
をつけることでドキュメントを生成することも出来ます。コメントで特徴的なのは、normal comment以外を誤った場所に書くとちゃんとエラーが出ます:
ビルドシステム: ...
ドキュメントが、無いです!!!!!!!!!!!!!!!!!!!!!!!!
アウトロ
ねこ〜〜〜〜〜〜〜〜。
しば犬飼いたい〜〜〜〜〜〜〜〜〜〜。
働きたくないよ〜〜〜〜〜〜〜〜〜〜〜〜。
いぬ〜〜。ねこ〜〜〜〜〜。
犬カフェ〜〜〜〜〜。ポメラニアン〜〜〜〜。
🐕
🐕
🐕
🐕
😺
🐕
🐕
🐕
Discussion
test Runnerについて
ziggitにも参加されているので、もう見かけているとは思いますが、以下のスレで紹介されてるtest runnerが結果表示してくれて便利ですよ。
コードの直リン
私は、rust testっぽく、引数で渡した名前で絞り込みするよう手を加えて使っています。
zigupもおすすめですよ