Zig

本書は、プログラミング言語Zigのチュートリアルです。 Zigは、堅牢で最適かつ再利用可能なソフトウェアを維持するための汎用システムプログラミング言語およびツールチェインです。 Zigは、アンドリュー・ケリー( Andrew Kelley )によって設計され、静的で強い型付けで型推論とジェネリックプログラミングをサポートします。
Zigは、2016年2月に発表された比較的若いプログラミング言語で、2024年2月10日現在の最新バージョンは 0.11.0 で、pre-release と位置づけられています。このため Hello world ですら、バージョン間で互換性がなくなることもあり、今後もリリースバージョンまでは言語仕様やライブラリーおよびツールチェインの仕様が変更される可能性があります。
Zigはメモリ安全性、高速なビルド、そして明快な言語設計を特長とする新しいプログラミング言語です。
以下のZigのクイックツアーでは、基本的な概念とコード例を紹介します。
ここでは、Zigの基本的な構文とコンセプトを簡単に紹介しました。
Zig Language Referenceの、Hello worldの変遷(新しい順)。
Zigは、コンパイル時に安全で高速なシステムプログラミング言語です。以下に、Zigの主な特徴を詳しく説明します。
以上が、Zigの主な特徴のいくつかです。これらの特徴により、Zigは高速かつ安全なシステムプログラミングを可能にします。
以下、個別の要素に言及します。
実際にZigで書かれたのコードを見てみましょう。
Ruby#ユーザー定義クラスの都市間の大圏距離を求めるメソッドを追加した例を、Zigに移植しました。
ファイルを開いて内容をアロケートしたバッファに読出し標準出力に出力する例。
このコードは、指定された絶対パスにあるファイル(この場合は"/etc/hosts")を開いて、その内容を標準出力に書き込むプログラムです。以下に、コードの各部分の機能を解説します。
このプログラムは、指定されたファイルの内容を読み取り、その内容を標準出力に書き込みます。
ジェネリックプログラミングを使った複素数型を実装してみました。
標準ライブラリーのソート機能の使用例。
エラトステネスの篩を、若干 Zig らしく書いてみました。
Java/抽象クラスを、Crystalに移植しました例を題材に、Zigにはない抽象クラスの代替のコード例を書いてみました。
Zigには範囲型がないので代替のコード例を書いてみました。
[TODO:書くべき項目を並べてみましたが、例えば「値と型」だけでも網羅的に書いていくとコンテンツの分量が爆発するのが目に見えているので、過剰になったらリファレンス篇に移動するなどの方法で、各節はコンパクトさを心がけたい]
Zigでは、関数 mainがエントリーポイントです。
なにもしないプログラムはこの様になりますが、エラー集合型を返す可能性があるときは
と戻値の型を void から !void に代えます。
Zigのコメントに関する基本的なポイントを要約します。
Zigのコメントシステムは、コードの文書化と理解を容易にするための強力なツールです。
Zigでは、// から行末までがコメントです。 C言語の /* ... */ のスタイルの複数行に渡るコメントはありません。 これは、コードの各行を文脈に関係なくトークン化できるようにするためです。
Zigでは、/// から始まるコメントは特別なコメントで、Docコメント( Doc comments )と呼ばれます。 Docコメントは特定の場所にしか許されません。式の途中や非Docコメントの直前など、予想外の場所にdocコメントがあると、コンパイルエラーになります。
[TODO:サンプルコードと整形結果]
Zigでは、//! から始まるコメントは特別なコメントで、トップレベルDocコメント( Top-Level Doc Comments )と呼ばれます。 コンテナレベルのドキュメントのように、直後のドキュメントに属さないユーザードキュメントに、トップレベルDocコメントを使います。
[TODO:サンプルコードと整形結果]
DocコメントおよびトップレベルDocコメントは、コンパイル時に zig build-exe -femit-docs ソースファイル.zig の様に、-femit-docs をあたえると、 docs/ 以下にドキュメントが生成されます。
Zigの値と型に関する基本的なポイントを要約します。
型
Zigの値と型のシステムは、明確で柔軟なプログラミングをサポートし、安全なコードの作成を促進します。
[TOD0:整数・浮動小数点数・bool・文字列・union・struct・enum・配列・ベクトル・スライス・ポインター・ゼロビットな型, 関連する組込み関数]
[TODO:master/lib/std/fmt.zigを観て書いています。参照すべき標準ライブラリーのドキュメントを出来たら/仕様が安定したら見直し]
文字列リテラルは、ヌル終端バイト配列への定数型単一項目ポインターです。文字列リテラルの型は、長さとヌル終端であるという事実の両方をコード化しているため、スライスとヌル終端ポインターの両方に強制することが可能です。文字列リテラルを再参照すると配列に変換されます。
Zigにおける文字列のエンコーディングは、事実上UTF-8であると仮定されています。ZigのソースコードはUTF-8でエンコードされているので、ソースコードの文字列リテラル内に現れる非ASCIIバイトは、そのUTF-8の意味をZigのプログラム内の文字列の内容に引き継ぎ、コンパイラーがそのバイトを修正することはありません。ただし、UTF-8以外のバイトを文字列リテラルに埋め込むことは可能で、その場合は \xNN 記法を使用します。
Unicodeコードポイントリテラルの型は comptime_int で整数リテラルと同じです。すべてのエスケープシーケンスは、文字列リテラルと Unicodeコードポイントリテラルの両方において有効です。
他の多くのプログラミング言語では、Unicodeコードポイントリテラルは「文字リテラル」と呼ばれます。しかし、Unicode仕様の最近のバージョン(Unicode 13.0時点)では、「文字」の正確な技術的定義は存在しません。Zigでは、Unicodeコードポイントリテラルは、Unicodeのコードポイントの定義に対応します。
マルチライン文字列リテラルは、エスケープが必要なく、複数の行にわたって記述することができます。マルチライン文字列リテラルを始めるには、\\ トークンを使用します。コメントと同様に、文字列リテラルは行の末尾まで続きます。行末は文字列リテラルに含まれません。ただし、次の行が \\ で始まる場合、改行が追加され、文字列リテラルが続きます。
Zigにおける代入に関する説明の要旨は以下の通りです。
具体的なコード例は以下の通りです。
上記のコードを実行すると、それぞれの変数や定数の挙動を確認できます。
comptimeは、Zig言語においてコンパイル時に式が評価されることを示します。この概念は、ジェネリック、定数式の評価、コンパイル時の最適化、およびコンパイル時の静的解析において重要な役割を果たします。
以下に、comptimeの詳細を説明します:
comptimeの使用により、Zig言語ではコンパイル時に高度な静的解析が実行され、パフォーマンスの向上やコードの安全性が確保されます。
このコードはコンパイルエラーになります。
エラーメッセージの意味は以下の通りです:
つまり、Zigでは配列の長さなどの定数値はコンパイル時に解決できる必要があります。そのため、関数の戻り値のように実行時に決定される値を定数の初期値として使用することはできません。
C++であれば、constexpr が適用なケースですが、Zigでは次のような解決方法を取ります。下記コードはエラーになりません。
Zigは、言語仕様とツールチェインの両方でテストをサポートしています。 Zigのテストフレームワークは、テストを実行し、アサーションを評価し、カバレッジレポートを生成するための機能を提供します。
以下は、Zigのテストに関する概要です。
Zigのテストフレームワークは、コードの信頼性を向上させ、開発プロセスを効率化するのに役立ちます。テストは、バグを早期に発見し、コードの安定性を確保するのに不可欠な要素です。
このZigのテストコードは、 if 式と if 文の振る舞いをテストしています。
テスト結果は、両方のテストが成功し、期待通りの結果が得られたことを示しています。つまり、条件式が正しく評価され、それに基づいて適切な操作が行われました。
このように、Zigのテストフレームワークを使用することで、さまざまな条件下でのプログラムの振る舞いを自動的にテストし、コードの信頼性を高めることができます。
この例では、Zig言語で fdiv-inf-nan.zig というファイルに記述されたテストコードが示されています。このコードは、fdiv 関数に対する3つのテストケースを含んでいます。
テストを実行すると、2つのテストがパスし、1つのテストが失敗します。失敗したテストケースは "fdiv 3" です。このテストは、 fdiv(0.0, 0.0) が nan と等しいかどうかを検証しますが、実際にはそうではありませんでした。
この失敗は、期待される結果と実際の結果が一致しなかったことを示しています。この情報を元に、fdiv 関数の実装やテストコードを再検討することで、この問題を修正することができます。
修正された fdiv-inf-nan-FIX.zig ファイルでは、3番目のテストケースが修正されています。その変更箇所と結果について解説します。
この修正では、fdiv(0.0, 0.0) の結果が nan であるかどうかを確認するために、 std.math.isNan 関数を使用しています。この関数は、与えられた浮動小数点数が非数 (nan) であるかどうかを判定します。
修正後のコードを実行すると、全てのテストケースが成功し、期待通りの結果が得られました。
この修正により、fdiv 関数が非数 (nan) を返す場合に正常にテストが通るようになりました。
Zigでは、変数は名前を持った連続したメモリー領域で、型を持ちます。 変数は、宣言が必要です。
Zig言語では、これらの変数のタイプやスコープを利用して、効率的で安全なプログラミングが可能です。
Zigでは型に名前を付けるためにも変数が使われます。
キーワード const で宣言された変数は、必ず初期化が必要で、宣言以降は値を変更することはできません(イミュータブル)。 const 変数の宣言のとき型が省略されると、初期値から型をコンパイラーが決めてくれます(型推論)。
このコードは、const キーワードを使って定数を宣言し、その値を出力しています。
以下に、このコードの各部分の解説を示します。
キーワード var で変数を宣言するときも初期化は必須です。またいつでも値を変更することはできます。 var 変数の宣言では、型を省略することはできません。
このコードは、var キーワードを使用して可変の変数を宣言し、その値を変更しています。
以下に、このコードの各部分の解説を示します。
変数は、外部スコープの変数をシャドーイングすることは許されません。
エラーメッセージによれば、外部スコープで宣言された x がローカルスコープで再度宣言されたため、エラーが発生したことがわかります。Zigでは、外部スコープの変数をシャドーイングすることは許されていないため、このコードはコンパイルエラーとなります。
識別子は英数字かアンダースコアで始まり、英数字かアンダースコアがいくつでも続くことができます。また、キーワードと重なってはいけません。
外部ライブラリーとのリンクなど、これらの要件に適合しない名前が必要な場合は、@""構文を使用することができます。
このコードは、Zigで識別子にスペースが含まれる場合や、数字で始まる識別子を使用する方法、および外部C関数に識別子を付ける方法を示しています。
以下に、各部分の解説を示します。
このように、Zigでは識別子にスペースや数字を含めることができ、また識別子を外部C関数に付けることもできます。これにより、異なる名前付け規則を持つライブラリや関数をZigのコードから使用することができます。
Zigでは、さまざまな整数リテラルの表現がサポートされています。
整数リテラルにはサイズ制限がなく、不明な動作が発生する場合にはコンパイラがそれをキャッチします。
ただし、整数値がコンパイル時には不明である場合、サイズが判明している必要があり、未定義の動作の影響を受けます。
このような関数では、値 a と b は実行時のみに判明するため、この除算操作は整数オーバーフローやゼロ除算の影響を受ける可能性があります。
整数演算では、+ や - などの演算子は整数オーバーフローに対して未定義の動作を引き起こします。代わりに、すべてのターゲットに対してラッピングおよびサチュレーティング演算を行うための代替演算子が提供されています。
Zigでは任意のビット幅の整数もサポートされており、i または u の後に数字が続く識別子を使用して参照されます。たとえば、i7 は7ビットの符号付き整数を示します。符号付き整数型の場合、Zigでは2の補数表現が使用されます。
このZigのプログラムは、異なるビット幅の整数型を使用し、それらの振る舞いを示しています。以下にコードの解説を示します。
プログラムの実行結果から、i の値がオーバーフローし、未定義の振る舞いが発生しました。これは i5 型の整数が範囲を超えたためです。このような場合、Zigは整数オーバーフローを検出してプログラムをパニックさせます。一方で、j の値はラッピング演算子により範囲外になることなく、k の値は飽和演算子により最大値で飽和しています。
Zigでは、以下の浮動小数点数型がサポートされています:
浮動小数点数リテラルは、comptime_float型を持ち、最大の他の浮動小数点数型(f128)と同じ精度と操作を保証します。
浮動小数点数リテラルは、任意の浮動小数点数型および整数型(小数点以下がない場合)に変換されます。
NaN、無限大、負の無限大のための構文はありません。これらの特殊な値については、標準ライブラリを使用する必要があります。
デフォルトでは、浮動小数点数の演算はStrictモードを使用しますが、ブロックごとにOptimizedモードに切り替えることができます。
上記の例では、演算をStrictモードとOptimizedモードで比較しています。
Zigの演算子の概要は以下の通りです:
演算子の詳細は、Zigのドキュメントやリファレンスを参照してください。
Zigは、やや関数型プログラミング言語の影響を受けており、多くの構文が値を持ちます。 Zigの制御構造の多くは式構文と文構文を持ちます(例外は #switch で式構文しかありません)。
以下、Kotlin#分岐から、一部の例を移植しました。
Zigには、#if と #switch の2つの分岐構文があります。
Zigでは、if は値を分岐する if式 とブロックを分岐する if文 があります。
if式で、条件が成立せずelseを省略されたとき、式の値は void となります。
ifの条件式にはオプショナル型( ?T )を使うことが出来ます。この場合は、通常の値のほか null を想定でき、null に出会った場合は else 節が実行されます。
[TODO:コード例]
ifの条件式にはエラーユニオン型( !T )を使うことが出来ます。この場合は、通常の値のほかエラーコードを想定でき、エラーコードに出会った場合は else |err| 節が実行され、err がエラーコードです。
[TODO:コード例]
Zigでは、switch は式で値を返します。switch文はありません。switch-prong(分岐先)の値の型は一致している必要があります。
Zigには、#while と #for の2つの反復構文があります。
whileは条件が成立している間、繰り返しを行います。条件がブロック内で評価され、条件が偽になるまで続きます。whileにはelse節もあり、ループ完了後に実行されます。また、ラベルを付けてネストしたループからのbreakやcontinueも可能です。オプショナル型やエラーユニオン型の条件も受け付け、それらの値をキャプチャできます。インライン化も可能で、コンパイル時の最適化や型の利用が可能です。
whileループにラベルを付けると、ネストしたループ内からのbreakやcontinueから参照できます。
ifと同じように、whileループは条件としてオプショナル型の値を受け取り、ペイロードをキャプチャすることができます。null に遭遇した場合、ループは終了します。
while 式に |x| 構文がある場合、while 条件はオプショナル型(あるいは次で述べるエラーユニオン型)でなければなりません(この x かキャプチャされたペイロードです)。
ifと同じように、whileループは条件としてエラーユニオン型の値を受け取り、ペイロードをキャプチャすることができます。エラーコードに遭遇した場合、ループは終了します。
while 式に |x| 構文がある場合、while 条件はエラーユニオン型(あるいは前で述べたオプショナル型)でなければなりません(この x かキャプチャされたペイロードです)。
while 式に else |x| 構文がある場合、while 条件にエラーユニオン型が必要です。
whileループはインライン化することができる。これにより、ループが展開され、コンパイル時にしかできないこと、例えば、型をファーストクラスの値として使用することなどができるようになります。
[TODO:コード例]
Zigのforループは、スライスや配列をイテレートする際に柔軟性を提供します。要素ごとに処理を行い、continueやbreakを使用して制御フローを操作できます。インデックスや複数のオブジェクトの同時イテレーション、参照によるイテレーションなど、多彩な機能を持ちます。また、forループを式として使用することも可能で、else節を使ってループが完了した際に特定の処理を行うことができます。ラベル付きループやインライン化もサポートされ、効率的なコーディングを実現します。
ラベル付きfor( Labeled for )とは、ラベルを伴った for ループでラベルも for 構文の一部です。for ループにラベルを付けると、ネストしたループ内からのbreakやcontinueから参照できます。
Forループはインライン化することができます。これにより、ループが展開され、コンパイル時にしかできないこと、例えば、型をファーストクラスの値として使用することなどができるようになります。インライン化されたforループのキャプチャ値とイテレータ値は、コンパイル時既知です。
Zigの関数は、プログラム内で再利用可能なコードブロックを表します。
Zigの関数の引数は、const で宣言された変数と同じくイミュータブルです。 ミュータブルにする方法はありません。
Zigの関数は、柔軟性があり、さまざまな目的に使用できます。関数は、プログラムの構造化と再利用性を高めるために不可欠な要素です。
Zigでは、エラー( Errors )も1つの型です。
エラー集合型( Error Set Type )は、enum のようなものです。同じエラー名を複数回宣言することは可能で、宣言した場合は同じ整数値が割り当てられます。 コンパイル全体のユニークなエラー値の数が、エラー集合型のサイズを決定するはずです。しかし、いまは u16 になるようにハードコーディングされています。 サブセットからスーパーセットへエラーを強制することができます。
エラー集合型をマージするには、|| 演算子を使用します。 結果として得られるエラー集合型には、両方のエラー集合型のエラーが含まれます。
anyerror は、グローバルエラー集合を参照します。これは、コンパイルユニット全体のすべてのエラーを含むエラー集合です。これは他のすべてのエラー集合のスーパーセットで、どのエラー集合のサブセットでもありません。
任意のエラー集合をグローバルエラー集合に強制することができ、グローバルエラー集合のエラーを非グローバルエラー集合に明示的にキャストすることができます。この場合、言語レベルのアサートが挿入され、エラー値が宛先のエラー集合に実際に含まれていることが確認されます。
グローバルエラー集合は、コンパイラーがコンパイル時にどのようなエラーが起こりうるかを知ることができないため、一般に避けるべきです。コンパイル時にエラー集合を知っていた方が、生成されるドキュメントや有用なエラーメッセージ(例えば switch で起こりうるエラー値を忘れてしまうなど)に有利です。
エラー集合型と正常型を二項演算子 ! で結合して、エラーユニオン型( Error Union Type )にすることができます。
エラーユニオン型は、エラー集合型単体よりも頻繁に使用される可能性があります。
Zigには、演算子のオーバーロードはありません。 Zigのプログラムに演算子を見たとき、それが次の一覧表に示すもので、それ以外のものでないことが保証されます。
Zigでは、四則演算などの演算子はディフォルトでオーバーフローを検出します。それとは別に、(C言語などのように)ラッピングを行う演算子が別に用意されています。
Zigには、四則演算などの演算子に飽和演算( Saturation calculation )バージョンが用意されています。
高
低
Zigには「オプショナル型」( optional type )と「非オプショナル型」があります。
ここまでに紹介した変数および定数の型は非オプショナル型です。
オプショナル型の変数および定数は宣言のときに ?型名 と書きます。
オプショナル型の式から基底型の値を参照することをアンラップを呼びます。
Zigの配列は、次のような特徴を持ちます:
Zigの配列は柔軟で効率的なデータ構造であり、様々なアプリケーションで使用されます。
配列には、特有な演算子が2つあります。
多次元配列( Multidimensional Arrays )は、ネストした配列で生成します。
センチネル終端配列( Sentinel Terminated Arrays )は、文字列のように特殊な値(センチネル( sentinel;番兵 ))で終端した配列です。 文字列の場合のセンチネルは ’\0’ ですが、構文 [配列長:x]要素型 の x で任意の値をセンチネルにできます。
ベクトルは、bool、整数、浮動小数点数、ポインターのグループで、可能であればSIMD命令を使って並列に操作されます。ベクトル型は、組込み関数 @Vector で作成します。
ベクトルは、基本型と同じ組み込み演算子をサポートしています。これらの演算は要素ごとに行われ、入力ベクトルと同じ長さのベクトルを返します。これには以下が含まれます。
スカラー(個々の数値)とベクトルが混在している場合に数学演算子を使用することは禁止されています。Zigでは、スカラーからベクトルへの変換を容易にするために組込み関数@splatが用意されており、ベクトルからスカラーへの変換には組込み関数@reduceと配列インデックスの構文がサポートされています。ベクトルは、長さが既知の固定長の配列との間の代入もサポートしています。
Zigは,組込み関数@shuffleと組込み関数@selectを提供し,ベクトル内やベクトル間の要素の並べ替えを行います.
ターゲットマシンのネイティブ SIMD サイズより短いベクトルに対する操作は、通常は単一の SIMD 命令にコンパイルされます。ある演算がターゲット・アーキテクチャで SIMD をサポートしていない場合、コンパイラーはデフォルトで各ベクトル要素に対して一度に 1 つずつ演算します。Zig は、2 までの既知のベクトル長をサポートしていますが、2 の累乗(2-64)が最も一般的です。ただし、現在の Zig では、長すぎるベクトル長 (例えば 2) はコンパイラーがクラッシュする可能性があります。
[TODO:配列との相互変換]
Zigには、単一項目と多項目の2種類のポインターがあります。
これらの型は、配列やスライスと密接に関係しています。
単項目ポインターを得るには、&xを使用します。
[TODO]
ロードとストアは、副作用がないことが前提です。MMIO (Memory Mapped Input/Output) のように、ロードやストアが副作用を持つべき場合は volatile を使用します。
それぞれの型にはアライメント( Alignment )があり、その型の値がメモリーからロードされたり、メモリーにストアされたりするとき、メモリーアドレスがこの数で均等に割り切れるようなバイト数になっています。この値は組込み関数 @alignOf を使って知ることができます。
アラインメントはCPUアーキテクチャに依存しますが、常に2のべき乗であり、1 << 29 より小さい値です。
Zigでは、ポインター型はアライメント値を持っています。この値が基礎となる型のアライメントと等しい場合、その型は省略することができます。
align は、ポインターのアライメントを指定するために使用します。また、変数や関数の宣言の後に使用して、その変数や関数へのポインターのアライメントを指定することができます。
allowzeroポインター属性は、ポインターのアドレスがゼロであることを許可します。これは、アドレスゼロがマッピング可能な独立型OSターゲットでのみ必要とされます。nullポインターを表現したい場合は、代わりにオプショナルポインターを使用します。allowzeroを持つオプショナルポインターは、ポインターと同じサイズではありません。
constポインター属性は、(ポインターではなく)ポインターが参照する値が変更できないことを示します。
センチネル終端ポインター( Sentinel Terminated Pointers )。 構文 [*:x]T は、センチネル値によって長さが決定されるポインターを記述します。これにより、バッファオーバーフローやオーバーリードから保護されます。
スライス( Slices )はポインターと長さです。配列とスライスの違いは、配列の長さが型の一部でコンパイル時にわかるのに対して、スライスの長さは実行時にわかることです。どちらも len プロパティでアクセスすることができます。
センチネル終端スライス( Sentinel-Terminated Slices )。 構文[:x]Tは、実行時に既知の長さを持ち、また長さでインデックスされた要素で センチネル値を保証するスライスです。この型は、それ以前にセンチネル要素がないことを保証するものではありません。センチネル終端スライスはlenインデックスへのエレメントアクセスを可能にします。
コンテナー(Containers)とは、変数や関数の宣言を保持する名前空間として機能する構文上の構造です。コンテナーは、インスタンス化可能な型定義でもあります。struct、enum、unionとopaque そしてZigのソースファイル自体も、コンテナーの例です。
コンテナーは、定義を囲むために波括弧を使用しますが、ブロックや関数とは異なります。コンテナーには文が含まれていません。
[TODO:std.containerについて]
struct(構造あるいは構造型)はデータの集合を定義するための構文であり、フィールドにはデフォルト値やメソッドを持たせることができます。packed structやextern structといった特殊な形式も提供され、メモリレイアウトを制御します。関数内でのstructの生成やジェネリックな構造体の定義もサポートされています。さらに、関数からstructを返すことも可能です。
struct はメソッド( Methods )を持つことができます。 メソッドは特別なものではなく、名前空間を持つだけです。 ドットシンタックス(インスタンス名.メソッド名)で呼び出すことができる関数です。
[TODO:組込み関数@This]
Zigでは、structリテラルの型は識別子と結びついている必要はありません。結果が強制される場合、structリテラルはコピーなしで結果の場所を直接インスタンス化します。 この様に、識別子と結びついていないstructリテラルを、匿名structリテラル( Anonymous Struct Literals )あるいはタプル( Tuple )と呼びます。
Zigのenumは列挙型を定義するための構文であり、異なる値をグループ化します。 値の順序を指定せずに列挙型を宣言することができ、任意の整数型を指定してタグ型を制御できます。 タグ型を指定しない場合、自動的に整数型が割り当てられ、0から始まる連続した値が与えられます。 列挙型にはメソッドを追加することができ、switch文を使用して値を切り替えることができます。 また、列挙型リテラルを使用して、特定の列挙値を指定することも可能です。 Zigのenumは柔軟性が高く、CのABIとの互換性を保つための機能も提供されています。
Zigのunionは、値が取りうる可能な型のセットをフィールドのリストとして定義しますす。 一度に1つのフィールドしかアクティブにできません。 裸のUnionのメモリ表現は保証されておらず、メモリを再解釈するためには@ptrCastを使用するか、保証されたメモリレイアウトを持つextern unionまたはpacked unionを使用する必要があります。 非アクティブなフィールドへのアクセスは安全性が確認されておらず、未定義の動作になります。 Union全体を割り当てることで他のフィールドをアクティブにできます。 また、Switch文を使用するためには、Unionにenumタグ型を付ける必要があります。 これにより、Switch式でUnionのペイロードを変更することが可能になります。 Unionにはenumタグ型を推論させることもでき、またstructやenumと同様にメソッドを持つことができます。
union はenumタグタイプを伴って宣言することができます。これにより unionは、タグ付きunion( Tagged union )になり、switch式で使用することができるようになります。タグ付きunionは、そのタグ型に強制されます。
構文木を考えてみましょう。Cで実装するとノードの種別と種別ごとのペイロードの共用体になります。Zigではこれを一般化してタグ付きunion1つで実装することが出来ます。 タグ付きunionを使うと網羅性の保証もでき、多くの特性をメソッドとして記述できます。たとえばキーワードや演算子を追加した場合、対応するswitchのパターンがないとエラーになりコンパイル時に事前に変更必要箇所を確認できます。
switch式でタグ付きunionのペイロードを変更するには、変数名の前に*を置き、ポインターにします
union も他のコンテナーと同様にメソッドを持つことが出来ます。
このコードは、ZigのUnionにメソッドを追加し、そのメソッドを使用して共用体の値を操作する方法を示しています。また、浮動小数点数の場合には、std.math.signbit を使用して符号を判定しています。
まず、Number というUnionが定義されています。このUnionは、Int フィールドと Float フィールドを持ち、それぞれ整数型と浮動小数点数型の値を保持します。また、const Self = @This() を使用して、メソッド内で共用体の型を参照できるようにしています。
次に、共用体に isPositive メソッドが追加されています。このメソッドは、共用体が保持する値が正の値かどうかを判定します。整数型の場合は、単純に値が0より大きいかどうかを確認しています。浮動小数点数型の場合は、std.math.signbit を使用して、値の符号を判定しています。符号が正の場合は、値が正の数であると判断します。
最後に、main 関数では、整数と浮動小数点数の共用体を作成し、それぞれの isPositive メソッドを呼び出して、共用体が保持する値が正の値かどうかを確認しています。結果は標準出力に出力されます。
実行結果では、整数が正の値であり、浮動小数点数が負の値であるため、"i.isPositive() = true" と "f.isPositive() = false" というメッセージが表示されます。
Zigのopaqueは、サイズとアライメントが不明(ただしゼロではない)な新しい型を宣言します。struct、union、enumと同様に、宣言を含めることができます。
これは、構造の詳細を公開しないCコードとやり取りする際の型安全性のために一般的に使用されます。例えば:
opaqueを使うと、foo関数内でbar関数を呼び出す際に型の安全性が保証されます。
ブロック(Blocks)は、プログラム内で複数の文をグループ化する構造で、主に変数のスコープを制限し、可読性を向上させます。ブロック内で宣言された変数は、そのブロックの外部からはアクセスできず、名前の衝突を防ぎます。また、条件分岐やループの本体として使用され、制御フローを明確にします。さらに、ラベル付きブロックではbreak文を使って値を返したり、ブロックから脱出したりすることができます。空のブロックはプログラムの構造を整理するために使用され、有効な構造化要素として機能します。ブロックはコードの構造化に不可欠であり、プログラムの理解と保守性を向上させます。
ブロックは式です。ラベルを付けると、break はブロックから値を返すために使うことができます。
識別子は、同じ名前を使用して他の識別子を "隠す"ことは決して許されません。 コンパイラーは、外部スコープで既に使われている識別子を、ブロック内で使うとエラーにします。
このため、Zigのコードを読むときには、その識別子が定義されたスコープ内では常に同じ意味であることを確認することができます。ただし、スコープが分かれている場合(入れ子関係にない場合)には同じ名前を使用することができます。
defer は、スコープを抜けるときに実行される式またはブロックを登録します。 複数の defer 文で登録された場合、登録された逆の順序に実行されます。
errdefer は、エラーが原因でスコープを抜けるときに実行される式またはブロックを登録します。 複数の errdefer 文で登録された場合、登録された逆の順序に実行されます。 errdefer は、スコープを抜ける原因となったエラーコードをキャプチャーできます。
Zigでは、キャスティングは一つの型から別の型への明示的な変換を指します。この機能は、異なる型間でのデータの変換や操作を可能にし、プログラムの柔軟性を高めます。Zigにはさまざまな種類のキャストがあり、それぞれが特定の目的に使用されます。
以下では、Zigのキャスティングについて詳細に説明します。
これらのキャスト機能は、Zigの柔軟性と安全性を向上させ、異なる型間でのデータの変換や操作を容易にします。
これらの機能は、Zig言語において型の柔軟な操作と安全な変換を可能にし、コードの表現力を高めます。
Zigにおける型強制(Type Coercion)は、コンパイラが型の不一致を解消するために行う自動的な型変換のプロセスを指します。Zigでは、いくつかの場面で型強制が行われます。
Zigの型強制は、安全な変換のみを行うことを目指しており、潜在的なデータの損失や不正確な結果を防ぐために注意深く設計されています。
明示的キャスト(Explicit Casts)は、コンパイラに対して特定の型への明示的な変換を指示する手段です。これは、コンパイラによって自動的に処理される型強制とは異なり、プログラマが意図的に型の変換を指定する場合に使用されます。Zigでは、さまざまな種類の明示的キャストが提供されています。
以下は、Zigで使用できる主な明示的キャストの例です:
これらの明示的なキャストは、プログラマがコンパイラに対して特定の型変換を指定する必要がある場合に使用されます。ただし、使用する際には注意が必要であり、不適切なキャストがプログラムの安全性や正確性に影響を与える可能性があるため、慎重に検討する必要があります。
Zigのピア型解決(Peer Type Resolution)は、複数のオペランドの型から、それらが共通して受け入れ可能な型を決定するプロセスです。このプロセスは、通常、switchやifなどの条件式、またはforやwhileなどのループの条件式で使用されます。ピア型解決は、これらの式で使用される型を決定するために、与えられた複数の型の間で最も適切な共通型を見つけることを目的としています。
具体的には、ピア型解決は以下の場面で発生します:
ピア型解決では、複数の型の間で共通の型を見つけるために、次のようなルールが適用されます:
これにより、Zigのコンパイラは、異なる型を持つ複数のオペランドに対して最も適切な共通の型を決定し、型の整合性を確保します。
ゼロビット型( Zero Bit Types )、実際にはデータを保持しない型のことを指します。これらの型は、メモリ内の領域を占有しないため、サイズがゼロビットです。
Zigでは、ゼロビット型は主に型システムの柔軟性と型安全性を高めるために使用されます。これらの型は、実行時のメモリ使用量やパフォーマンスには影響しませんが、コードの明確さと保守性を向上させるのに役立ちます。
[TODO:所謂インラインアセンラ]
Zigでは、キーワード async を伴って関数またはメソッドを呼び出すと、その関数またはメソッドの処理を休止し再開することができます。
Zigのunreachableは、制御フローが特定の位置に到達しないことを明示するために使用されます。具体的には、その部分に到達することがプログラムの不正な動作であることを表明します。
Debug と ReleaseSafe モード、および zig test を使用する場合、unreachable は到達不能なコードに到達したメッセージとともに panic への呼び出しを出します。
ReleaseFast モードでは、オプティマイザーは到達不能なコードは決してヒットしないという仮定を使用して最適化を実行します。しかし、ReleaseFastモードでもzigテストはunreachableをpanicへの呼出しとして出力します。
unreachableの型はnoreturnです。 @TypeOf(unreachable)はコンパイルに失敗します。 unreachable式はコンパイルエラーになるからです。
noreturnは、つぎ文の型です。
if節やswitchの分岐先( prongs )など、型を一緒に解決する場合、noreturn型は他のすべての型と互換性があります。
組込み関数( Builtin Functions )はコンパイラによって提供され、接頭辞に @ が付けられます。 パラメーターについての comptime キーワードは、そのパラメーターがコンパイル時に既知である必要があることを意味します。
Zigのビルドモードは、プログラムをビルドする際の特定の設定とオプションの組み合わせを指定するものです。Zigには次の4つのビルドモードがあります。
それぞれのビルドモードは、異なる目的に対応しています。例えば、デバッグ中には実行時のパフォーマンスよりもデバッグしやすさや安全性が重要ですが、リリース時には実行時のパフォーマンスやバイナリサイズの最適化が優先されることがあります。
ビルドモードを指定するには、zig build-exeコマンドを使用します。例えば、ReleaseFastモードでプログラムをビルドするには、次のようにします。
各ビルドモードの特性は以下の通りです。
これらのビルドモードは、プロジェクトのニーズや要件に応じて適切なものを選択することが重要です。
Zigのメモリー管理の基礎について、重要なポイントを要約します。
Zigは、メモリー管理に関する柔軟性と制御を提供し、プログラマーがそのアプリケーションのニーズに合わせて最適なアロケーターを選択できるようにしています。
Zig言語はプログラマーに代わってメモリー管理を行うことはありません。このため、Zigにはランタイムがなく、Zigのコードはリアルタイム・ソフトウェア、OSカーネル、組込み機器、低遅延サーバーなど、多くの環境でシームレスに動作します。その結果、Zigのプログラマーは常に次のような問いに答えられなければなりません。
Zigと同様、C言語もメモリー管理を手動で行っています。しかし、Zigとは異なり、C言語にはmalloc、realloc、freeというデフォルトのアロケーターがあります。libc とリンクするとき、Zig はこのアロケーターを std.heap.c_allocator で公開します。しかし、慣習として、Zig にはデフォルトのアロケーターはありません。代わりに、割当てが必要な関数は Allocator パラメーターを受け取ります。同様に、std.ArrayList のようなデータ構造もその初期化関数で Allocator パラメーターを受け付けます。
上記の例では、スタック上の 100 バイトのメモリーが FixedBufferAllocator の初期化に使われ、それが関数に渡されます。
利便性のために std.testing.allocator でグローバルな FixedBufferAllocator が用意されており、基本的なリーク検出も行うことができます。
Zig には std.heap.GeneralPurposeAllocator でインポート可能な汎用アロケーターがあります。しかし、やはり、アロケーターの選び方の指針に従うことが推奨されます。
どのアロケーターを使うかは、様々な要因によって決まります。以下は、判断のためのフローチャートです。
Where are the bytes?
”foo” のような文字列リテラルは、グローバル定数データセクション( global constant data section )にあります。このため、文字列リテラルをミュータブルスライスに渡すとエラーになります。
文字列リテラルと同様に、コンパイル時に値が分かっているconst宣言は、グローバル定数データセクションに格納されます。また、コンパイル時変数もグローバル定数データセクションに格納されます。
関数内のvar宣言は、その関数のスタックフレームに格納されます。関数から戻ると、関数のスタックフレームにある変数へのポインターは無効な参照となり、その参照解除は未確認の未定義動作となります。
Zig プログラマーは Allocator インターフェースを満たすことで、自分自身のアロケーターを実装することができます。そのためには、std/mem.zig のドキュメントコメントをよく読んで、allocFn と resizeFn を指定する必要があります。
インスピレーションを得るために、多くのアロケータの例を見ることができます。std/heap.zig と std.heap.GeneralPurposeAllocator を見てください。
多くのプログラミング言語では、ヒープ割り当てに失敗した場合、無条件にクラッシュすることで対処することにしています。
Zig のプログラマは、慣習として、これが満足のいく解決策であるとは考えていません。
その代わり、error.OutOfMemoryはヒープ割り当ての失敗を表し、Zigライブラリーはヒープ割り当ての失敗で処理が正常に完了しなかったときはいつでもこのエラーコードを返します。
Linuxなどの一部のOSでは、デフォルトでメモリーのオーバーコミットが有効になっているため、ヒープ割り当ての失敗を処理することは無意味であると主張する人もいます。この理由には多くの問題があります。
再帰( Recursion )はソフトウェアをモデリングする際の基本的なツールである。しかし、再帰にはしばしば見落とされがちな問題があります。
再帰はZigで活発に実験されている分野であり、ここにある文書は最終的なものではありません。0.3.0のリリースノートで、再帰の状況を要約して読むことができます。
簡単にまとめると、現在のところ再帰は期待通りに動作しています。Zigのコードはまだスタックオーバーフローから保護されていませんが、Zigの将来のバージョンでは、Zigのコードからのある程度の協力が必要ですが、そのような保護を提供することが予定されています。
ポインターを指しているメモリーが利用できなくなったときに、ポインターにアクセスしないようにするのは、Zigのプログラマーの責任です。スライスは、他のメモリーを参照するという点で、ポインターの一種であることに注意してください 。
バグを防ぐために、ポインターを扱うときに従うと便利な規則があります。一般に、関数がポインターを返す場合、その関数のドキュメントでは、誰がそのポインターを「所有」しているかを説明する必要があります。この概念は、プログラマーがポインターを解放することが適切である場合、そのタイミングを判断するのに役立ちます。
例えば、関数のドキュメントに「返されたメモリーは呼び出し元が所有する」と書かれていた場合、その関数を呼び出すコードは、いつそのメモリーを解放するかという計画を持っていなければなりません。このような場合、おそらく関数は Allocator パラメーターを受け取ります。
時には、ポインターの寿命はもっと複雑な場合があります。例えば、std.ArrayList(T).items スライスは、新しい要素を追加するなどしてリストのサイズが次に変更されるまで有効です。
関数やデータ構造のAPIドキュメントでは、ポインターの所有権と有効期限について細心の注意を払って説明する必要があります。所有権とは、ポインターが参照するメモリーを解放する責任が誰にあるかということであり、寿命とは、メモリーがアクセス不能になる時点(未定義動作が発生しないように)を決めることです。
ZigはC言語から独立しており、他の多くの言語とは異なりlibcに依存しませんが、Zigは既存のC言語で書かれたとの相互作用の重要性を認めています。
ZigはC言語との相互運用を容易にするために、いくつかの方法を用意しています。
以下に示すZigの型はC言語とのABI互換性が保証されており、他の型と同様に使用することができます。
C の void 型と相互運用する場合は anyopaque を使用します。
組込み関数 @cImport を使用すると、.h ファイルからシンボルを直接インポートすることができます。 @cImport 関数は、パラメーターとして式を受取ります。この式はコンパイル時に評価され、プリプロセッサー指令の制御や複数の.hファイルをインクルードするために使用されます。
Zigは、2024年2月10日時点で pre-release の段階にあり、インストール手順も何度か変わっているので、まずは https://ziglang.org/learn/getting-started/ を参照し、最新のインストール手順を確認してください。
ローカルにコンパイル実行環境を作るのが困難、あるいは手間を掛けずにコンパイルと実行を試してみたい。そんな場合には、オンラインコンパイル実行環境を使うことも検討に値します。
nightly の Zigを使いたい場合でない限り、お使いのOSやディストリビューションでサポートされているパッケージ マネージャーを使ってインストールする事をお勧めします。 また、ローカルのパッケージデータベースをパッケージシステムのリポジトリーと同期を取り、鮮度の高い状態を維持するよう心がけてください。これは、インストール後も同じですし、zigに限ったことでもありません。
Zig は、ソースコードが Github の https://github.com/ziglang/zig.git で公開されているので、必要に応じてソースコードからビルドすることができます。 Zigは、セルフホスティングなので、最初にバイナリーを入手してブートストラップするか、クロスビルドしたバイナリーを持込むか、パッケージシステムからインストールし、ターゲットでセルフコンパイル出来る状態を作る方法があります(FreeBSDのPortsが行っているのは、まさにこれです)。
ビルド方法こそ頻繁に内容が変わるので、個別具体的な手順は述べませんが、zig はコンパイラーであるとともにツールチェインでもあり、ビルドシステムも内包しているので、
Zig の処理系 zig という1つのコマンドで、コンパイラーだけでなく、アーカイバー・C/C++コンパイラーやテストフレームワーク・フォーマッター・ビルドツールなどのツールチェインが統合されています。
Zig言語のzig runコマンドは、Zigコンパイラを使用してZigソースコードをコンパイルし、実行可能なバイナリを生成して実行するためのコマンドです。このコマンドを使うと、Zigソースコードを直接実行することができます。