- これは Zig ドキュメント 0.10.0 を DeepL Pro を利用して翻訳したものです。
- 自分用に翻訳しています
- そのうち GitHub リポジトリ管理に切り替えます
-
-
Save nomissbowling/2d2864334853a808ce9459c70daf0f5f to your computer and use it in GitHub Desktop.
Zig は、堅牢で最適かつ再利用可能なソフトウェアを維持するための汎用プログラミング言語およびツールチェインです。
- ロバスト
- メモリ不足などのエッジケースでも正しく動作する。
- 最適化
- プログラムが最適に動作・実行されるように記述する。
- 再利用可能
- 同じコードが、制約の異なる多くの環境で動作します。
- 保守性
- コンパイラや他のプログラマに意図を正確に伝えることができる。コードを読むためのオーバーヘッドが少なく、要件や環境の変化に強い言語です。
新しいことを学ぶのに一番効率的な方法は例を見ることであることが多いので、このドキュメントはZigの各機能の使い方を紹介しています。すべて1つのページにまとめてありますので、ブラウザの検索ツールで検索することができます。
このドキュメントにあるコードサンプルは、Zigのメインテストスイートの一部としてコンパイルされ、テストされています。
このHTML文書は外部ファイルに一切依存していませんので、オフラインで使用することができます。
Zig 標準ライブラリは、独自のドキュメントを持っています。
Zig の標準ライブラリには、プログラムやライブラリを構築するためによく使われる アルゴリズムやデータ構造、定義が含まれています。このドキュメントでは、Zig 標準ライブラリの使用例を多く見ることができます。Zig 標準ライブラリの詳細については、上のリンクを参照してください。
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{"world"});
}
$ zig build-exe hello.zig
$ ./hello
Hello, world!
上の Zig のコードサンプルは、出力するプログラムを作成する方法の一つを示したものです。Hello, world!
このコードサンプルは、hello.zig という名前のファイルの内容を示しています。Zig のソースコードを格納したファイルは、UTF-8でエンコードされたテキストファイルです。Zig のソースコードを格納したファイルは、通常 .zig という拡張子で命名されます。
Zig コードサンプル hello.zig に続いて、Zig ビルドシステムを使って hello.zig のソースコードから実行可能なプログラムをビルドします。そして、hello プログラムが実行され、その出力 Hello, world! で始まる行は、コマンドラインプロンプトとコマンドを表しています。それ以外はすべてプログラムの出力です。
このサンプルコードでは、まず @import 組み込み関数を使って Zig 標準ライブラリをビルドに追加しています。import("std")
関数呼び出しは、Zig 標準ライブラリを表す構造体を作成します。次に、Zig Standard Library の機能にアクセスするための std という名前の定数識別子を宣言しています。
次に、main という名前のパブリック関数、pub fn が宣言されています。main 関数は、Zig コンパイラにプログラムの先頭がどこに存在するかを伝えるために必要です。実行されるように設計されたプログラムには、pub fn の main 関数が必要です。
より高度なユースケースのために、Zig はプログラムの開始点がどこに存在するかをコンパイラに知らせるための他の機能を提供しています。また、ライブラリのコードは他のプログラムやライブラリから呼び出されるため、ライブラリには
pub fn main
関数は必要ありません。
関数とは、いくつかの文や式からなるブロックで、全体として一つのタスクを実行するものです。関数は、タスクの実行後にデータを返すこともあれば、返さないこともあります。関数がタスクを実行できない場合は、エラーを返すかもしれません。Zigはこれら全てを明示します。
hello.zig のサンプルコードでは、main 関数が !void
戻り値型で宣言されています。この戻り値の型はエラーユニオン型と呼ばれるものです。この構文は、Zig コンパイラに、この関数はエラーか値を返すと伝えます。エラーユニオン型は、エラーセット型と他のデータ型(例えば、プリミティブ型や、構造体、enum 、ユニオンなどのユーザー定義型)を組み合わせたものである。エラーユニオン型の完全な形式は、<エラーセット型>!<任意のデータ型>
です。コードサンプルでは、エラーセットタイプは !
演算子の左側に明示的に書かれていません。このように書かれた場合、エラーセット型は推論されたエラーセット型となります。演算子 !
の後の void
は、通常の場合(すなわち、エラーが発生しない場合)にはその関数が値を返さないことをコンパイラに伝えます。
経験豊富なプログラマーへの注意 Zig にはブーリアン演算子
!a
もあります。ここでa
は bool 型の値です。エラーユニオン型は、構文上、型の名前を含んでいます。<任意のデータ型>
。
Zig では、関数のステートメントと式のブロックは、開いた中括弧 {
と閉じた中括弧 }
で囲まれています。main
関数の内部には、Hello, world! を標準出力に出力するタスクを実行する式があります。
まず、標準出力の書き手を表す定数識別子 stdout
が初期化される。そして、Hello, world! のメッセージを標準出力に出力しようとする。
関数は、そのタスクを実行するために情報を必要とすることがある。Zig では、関数に渡す情報は、関数名の後に置かれた開き括弧 (
と閉じ括弧 )
の間に置かれます。この情報は、引数とも呼ばれます。関数に渡される引数が複数ある場合、それらはカンマ ,
で区切られます。
stdout.print()
関数に渡された2つの引数、"hello, {s}!\n"
と .{"world"}
は、コンパイル時に評価されます。このコードサンプルは、print関数内で文字列代入を行う方法を示すために意図的に書かれたものです。第1引数の中括弧は、第2引数のコンパイル時に既知の値(匿名構造体リテラル)に置換されます。第1引数の二重引用符の内側の \n
は、改行文字用のエスケープシーケンスです。try式は、stdout.print
の結果を評価します。結果がエラーの場合、try
式はエラーとともに main
から戻ってきます。そうでなければ、プログラムは続行されます。この場合、main
関数の中で実行できる文や式はもう残っていないので、プログラムは終了します。
Zigでは、標準出力ライターのprint関数は、実際には汎用ライターの一部として定義された関数であるため、失敗が許される。ファイルへのデータ書き込みを表す汎用ライタを考えてみよう。ディスクが満杯になると、ファイルへの書き込みは失敗します。しかし、標準出力へのテキスト書き込みが失敗することは通常想定されません。標準出力への出力が失敗した場合の処理を回避するために、代替の関数を使用することができます:適切なロギングを行うための std.log
の関数または std.debug.print
関数です。このドキュメントでは、後者のオプションを使用して、標準エラー出力(stderr)に印刷し、失敗した場合は静かにリターンすることにします。次のコードサンプルhello_again.zig
は std.debug.print
を使用した例です。
const print = @import("std").debug.print;
pub fn main() void {
print("Hello, world!\n", .{});
}
$ zig build-exe hello_again.zig
$ ./hello_again
Hello, world!
なお、std.debug.print
は失敗できないので、戻り値の型から !
を抜いても良い。
こちらもご覧ください。
- Values
- @import
- Errors
- Root Source File
- Source Encoding
const print = @import("std").debug.print;
pub fn main() void {
// Comments in Zig start with "//" and end at the next LF byte (end of line).
// The line below is a comment and won't be executed.
//print("Hello?", .{});
print("Hello, world!\n", .{}); // another comment
}
$ zig build-exe comments.zig
$ ./comments
Hello, world!
Zig には複数行のコメント(例えば、C言語の /* */
コメントのようなもの)はありません。このため、Zigはコードの各行を文脈に関係なくトークン化できるという特性を持つようになりました。
ドキュメント・コメントは、ちょうど3つのスラッシュで始まるものです(すなわち、////
ではなく、///
)。一列に並んだ複数のドキュメント・コメントは、複数行のドキュメント・コメントとしてマージされます。ドキュメント・コメントは、その直後にあるものを文書化します。
/// A structure for storing a timestamp, with nanosecond precision (this is a
/// multiline doc comment).
const Timestamp = struct {
/// The number of seconds since the epoch (this is also a doc comment).
seconds: i64, // signed so we can represent pre-1970 (not a doc comment)
/// The number of nanoseconds past the second (doc comment again).
nanos: u32,
/// Returns a `Timestamp` struct representing the Unix epoch; that is, the
/// moment of 1970 Jan 1 00:00:00 UTC (this is a doc comment too).
pub fn unixEpoch() Timestamp {
return Timestamp{
.seconds = 0,
.nanos = 0,
};
}
};
ドキュメント・コメントは特定の場所にしか許されません。最終的には、式の途中や非ドキュメント・コメントの直前など、予想外の場所にドキュメント・コメントがあると、コンパイルエラーになります。
コンテナレベルのドキュメントのように、直後のドキュメントに属さないユーザードキュメントは、トップレベルのドキュメント・コメントで記述されます。トップレベルのドキュメント・コメントは、2 つのスラッシュと感嘆符で始まるコメントです。//!
.
//! This module provides functions for retrieving the current date and
//! time with varying degrees of precision and accuracy. It does not
//! depend on libc, but will use functions from it if available.
// Top-level declarations are order-independent:
const print = std.debug.print;
const std = @import("std");
const os = std.os;
const assert = std.debug.assert;
pub fn main() void {
// integers
const one_plus_one: i32 = 1 + 1;
print("1 + 1 = {}\n", .{one_plus_one});
// floats
const seven_div_three: f32 = 7.0 / 3.0;
print("7.0 / 3.0 = {}\n", .{seven_div_three});
// boolean
print("{}\n{}\n{}\n", .{
true and false,
true or false,
!true,
});
// optional
var optional_value: ?[]const u8 = null;
assert(optional_value == null);
print("\noptional 1\ntype: {s}\nvalue: {?s}\n", .{
@typeName(@TypeOf(optional_value)),
optional_value,
});
optional_value = "hi";
assert(optional_value != null);
print("\noptional 2\ntype: {s}\nvalue: {?s}\n", .{
@typeName(@TypeOf(optional_value)),
optional_value,
});
// error union
var number_or_error: anyerror!i32 = error.ArgNotFound;
print("\nerror union 1\ntype: {s}\nvalue: {!}\n", .{
@typeName(@TypeOf(number_or_error)),
number_or_error,
});
number_or_error = 1234;
print("\nerror union 2\ntype: {s}\nvalue: {!}\n", .{
@typeName(@TypeOf(number_or_error)),
number_or_error,
});
}
$ zig build-exe values.zig
$ ./values
1 + 1 = 2
7.0 / 3.0 = 2.33333325e+00
false
true
false
optional 1
type: ?[]const u8
value: null
optional 2
type: ?[]const u8
value: hi
error union 1
type: anyerror!i32
value: error.ArgNotFound
error union 2
type: anyerror!i32
value: 1234
文字列リテラルは、ヌル終端バイト配列への定数型単一項目ポインタです。文字列リテラルの型は、長さとヌル終端であるという事実の両方をコード化しているため、スライスとヌル終端ポインタの両方に強制することが可能です。文字列リテラルを再参照すると、配列に変換されます。
Zig における文字列のエンコーディングは、事実上 UTF-8 であると仮定されています。Zig のソースコードは UTF-8 でエンコードされているので、ソースコードの文字列リテラル内に現れる非 ASCII バイトは、その UTF-8 の意味を Zig のプログラム内の文字列の内容に引き継ぎ、コンパイラがそのバイトを修正することはありません。ただし、UTF-8 以外のバイトを文字列リテラルに埋め込むことは、\xNN
表記で可能です。
Unicode コードポイント・リテラルのタイプは comptime_int
で、整数リテラルと同じです。すべての Escape Sequence は、文字列リテラルと Unicode コードポイント・リテラルの両方において有効です。
他の多くのプログラミング言語では、Unicode コードポイント リテラルを「文字リテラル」と呼びます。しかし、Unicode 仕様の最近のバージョン(Unicode 13.0時点)では、「文字」の正確な技術的定義は存在しません。Zig では、Unicode コードポイントリテラルは、Unicode のコードポイントの定義に対応します。
const print = @import("std").debug.print;
const mem = @import("std").mem; // will be used to compare bytes
pub fn main() void {
const bytes = "hello";
print("{s}\n", .{@typeName(@TypeOf(bytes))}); // *const [5:0]u8
print("{d}\n", .{bytes.len}); // 5
print("{c}\n", .{bytes[1]}); // 'e'
print("{d}\n", .{bytes[5]}); // 0
print("{}\n", .{'e' == '\x65'}); // true
print("{d}\n", .{'\u{1f4a9}'}); // 128169
print("{d}\n", .{'💯'}); // 128175
print("{}\n", .{mem.eql(u8, "hello", "h\x65llo")}); // true
print("0x{x}\n", .{"\xff"[0]}); // non-UTF-8 strings are possible with \xNN notation.
print("{u}\n", .{'⚡'});
}
$ zig build-exe string_literals.zig
$ ./string_literals
*const [5:0]u8
5
e
0
true
128169
128175
true
0xff
⚡
こちらもご覧ください。
- Arrays
- Source Encoding
なお、有効な Unicode ポイントの最大値は 0x10ffff
です。
複数行の文字列リテラルにはエスケープ記号がなく、複数行にまたがることができます。複数行の文字列リテラルを開始するには、 \\
トークンを使用します。コメントと同じように、文字列リテラルは行末まで続きます。行の終わりは文字列リテラルに含まれません。ただし、次の行が \\
で始まる場合は、改行が追加され、文字列リテラルが続行されます。
const hello_world_in_c =
\\#include <stdio.h>
\\
\\int main(int argc, char **argv) {
\\ printf("hello world\n");
\\ return 0;
\\}
;
こちらもご覧ください。
- @embedFile
const x = 1234;
fn foo() void {
// It works at file scope as well as inside functions.
const y = 5678;
// Once assigned, an identifier cannot be changed.
y += 1;
}
pub fn main() void {
foo();
}
$ zig build-exe constant_identifier_cannot_change.zig
docgen_tmp/constant_identifier_cannot_change.zig:8:7: error: cannot assign to constant
y += 1;
~~^~~~
referenced by:
main: docgen_tmp/constant_identifier_cannot_change.zig:12:5
callMain: /home/andy/tmp/zig/lib/std/start.zig:596:17
remaining reference traces hidden; use '-freference-trace' to see all reference traces
const print = @import("std").debug.print;
pub fn main() void {
var y: i32 = 5678;
y += 1;
print("{d}", .{y});
}
const
は、その識別子が直前にアドレスしているすべてのバイトに適用されます。ポインターはそれ自体が const を持ちます。
もし、変更可能な変数が必要な場合は、var
キーワードを使用します。
$ zig build-exe mutable_var.zig
$ ./mutable_var
5679
変数は初期化する必要があります。
pub fn main() void {
var x: i32;
x = 1;
}
$ zig build-exe var_must_be_initialized.zig
docgen_tmp/var_must_be_initialized.zig:2:5: error: variables must be initialized
var x: i32;
^~~~~~~~~~
変数の初期化を行わない場合は、undefined を使用します。
const print = @import("std").debug.print;
pub fn main() void {
var x: i32 = undefined;
x = 1;
print("{d}", .{x});
}
$ zig build-exe assign_undefined.zig
$ ./assign_undefined
1
undefined
は任意の型に強制することができます。これが起こると、値が undefined
であることを検出することができなくなる。undefined
は、値が何にでもなり得るということであり、型によれば無意味なものでさえもなり得るということである。英語に訳すと、undefined
は「意味のない値だ。この値を使うとバグになる。この値は使われないか、使われる前に上書きされるでしょう。」 という意味です。
デバッグモードでは、Zig は 0xaa
バイトを未定義メモリに書き込む。これは、バグを早期に発見し、デバッガで未定義メモリの使用を検出しやすくするためです。ただし、この動作はあくまで実装上のものであり、言語のセマンティクスではないので、コード上で観測可能であることは保証されていません。
1つまたは複数のテスト宣言の中で書かれたコードを使用して、動作が期待に沿うことを確認することができます。
const std = @import("std");
test "expect addOne adds one to 41" {
// The Standard Library contains useful functions to help create tests.
// `expect` is a function that verifies its argument is true.
// It will return an error if its argument is false to indicate a failure.
// `try` is used to return an error to the test runner to notify it that the test failed.
try std.testing.expect(addOne(41) == 42);
}
/// The function `addOne` adds one to the number given as its argument.
fn addOne(number: i32) i32 {
return number + 1;
}
$ zig test introducing_zig_test.zig
1/1 test.expect addOne adds one to 41... OK
All 1 tests passed.
introducing_zig_test.zig コードサンプルは、関数 addOne
が入力 41
に対して 42
を返すかどうかをテストします。このテストの観点からは、 addOne
関数がテスト対象のコードであると言えます。
zig test はテストビルドを作成し、実行するツールです。デフォルトでは、Zig Standard Library が提供するデフォルトのテストランナーを主なエントリポイントとして、実行可能なプログラムをビルドして実行します。ビルドの間、与えられた Zig ソースファイルを解決する際に見つかった test
宣言は、デフォルトのテストランナーが実行して報告できるように含まれます。
このドキュメントでは、Zig 標準ライブラリで提供されているデフォルトのテストランナーの機能について説明します。そのソースコードは lib/test_runner.zig にあります。
上のシェル出力は、zig test コマンドの後に 2 行を表示しています。これらの行は、デフォルトのテストランナーによって標準エラーに出力されます。
Test [1/1] test "expect addOne adds one to 41"...
このような行は、テストの総数のうち、どのテストが実行されているかを示しています。この場合、[1/1] は 1 つのテストのうち、最初のテストが実行されていることを示しています。テストランナープログラムの標準エラーがターミナルに出力される場合、テストが成功するとこれらの行はクリアされることに注意してください。
All 1 tests passed.
この行は、パスしたテストの総数を示しています。
テスト宣言は、キーワード test
、文字列リテラルで記述されたオプションの名前、 関数内で許可された有効な Zig コードを含むブロックの順で記述します。
慣習として、名前のないテストは他のテストを実行させるためにのみ使用されるべきです。名前のないテストにフィルタをかけることはできません。
テストの宣言は Functions と似ています。テストの暗黙の戻り値は anyerror!void
というエラーユニオン型であり、これを変更することはできません。Zig ソースファイルが zig テストツールを使ってビルドされない場合、テスト宣言はビルドから省かれます。
テスト宣言は、テスト対象のコードが書かれているのと同じファイルに書くこともできますし、別の Zig ソースファイルに書くこともできます。テスト宣言はトップレベルの宣言であるため、順序に関係なく、テスト対象のコードの前でも後でも書くことができます。
こちらもご覧ください。
- The Global Error Set
- Grammar
zig テストツールがテストランナーをビルドするとき、解決された test
宣言だけがビルドに含まれます。初期状態では、与えられた Zig ソースファイルのトップレベルの宣言だけが解決されます。ネストしたコンテナがトップレベルのテスト宣言から参照されていない限り、ネストしたコンテナのテストは解決されません。
以下のコード サンプルでは、std.testing.refAllDecls(@This()) 関数呼び出しを使用して、インポートした Zig ソース ファイルを含むファイル内のすべてのコンテナを参照し ています。このコードサンプルでは、_ = C
; 構文を使用してコンテナを参照する別の方法も示しています。この構文は、コンパイラに代入演算子の右辺にある式の結果を無視するように指示します。
Zig には未定義の動作が数多く存在します。未定義の動作がコンパイル時に検出された場合、Zig はコンパイル・エラーを 出して処理を続行させません。コンパイル時に検出できない未定義の動作のほとんどは、実行時に検出できま す。このような場合、Zigは安全性チェックを行います。安全性チェックは @setRuntimeSafety でブロック単位で無効にできます。ReleaseFast と ReleaseSmall ビルドモードでは、最適化を促進するために、すべての安全性チェックを無効にします(@setRuntimeSafety で上書きされる場合を除く)。
安全性チェックに失敗すると、Zig は以下のようなスタックトレースを残してクラッシュする。
test "safety check" {
unreachable;
}
$ zig test test.zig
1/1 test.safety check... thread 1639714 panic: reached unreachable code
docgen_tmp/test.zig:2:5: 0x211565 in test.safety check (test)
unreachable;
^
/home/andy/tmp/zig/lib/test_runner.zig:63:28: 0x212b68 in main (test)
} else test_fn.func();
^
/home/andy/tmp/zig/lib/std/start.zig:596:22: 0x211e4b in posixCallMainAndExit (test)
root.main();
^
/home/andy/tmp/zig/lib/std/start.zig:368:5: 0x211911 in _start (test)
@call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
^
error: the following test command crashed:
/home/andy/tmp/zig/zig-cache/o/c8cfbb273b267e28462c38d7de2721ed/test
コンパイル時:
comptime {
assert(false);
}
fn assert(ok: bool) void {
if (!ok) unreachable; // assertion failure
}
$ zig test test.zig
docgen_tmp/test.zig:5:14: error: reached unreachable code
if (!ok) unreachable; // assertion failure
^~~~~~~~~~~
docgen_tmp/test.zig:2:11: note: called from here
assert(false);
~~~~~~^~~~~~~
ランタイム時:
const std = @import("std");
pub fn main() void {
std.debug.assert(false);
}
$ zig build-exe test.zig
$ ./test
thread 1639774 panic: reached unreachable code
/home/andy/tmp/zig/lib/std/debug.zig:278:14: 0x211720 in assert (test)
if (!ok) unreachable; // assertion failure
^
docgen_tmp/test.zig:4:21: 0x20fffa in main (test)
std.debug.assert(false);
^
/home/andy/tmp/zig/lib/std/start.zig:596:22: 0x20f6bb in posixCallMainAndExit (test)
root.main();
^
/home/andy/tmp/zig/lib/std/start.zig:368:5: 0x20f181 in _start (test)
@call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
^
(process terminated by signal)
コンパイル時:
comptime {
const array: [5]u8 = "hello".*;
const garbage = array[5];
_ = garbage;
}
$ zig test test.zig
docgen_tmp/test.zig:3:27: error: index 5 outside array of length 5
const garbage = array[5];
^
ランタイム時:
pub fn main() void {
var x = foo("hello");
_ = x;
}
fn foo(x: []const u8) u8 {
return x[5];
}
$ zig build-exe test.zig
$ ./test
thread 1639834 panic: index out of bounds: index 5, len 5
docgen_tmp/test.zig:7:5: 0x2119b9 in foo (test)
return x[5];
^
docgen_tmp/test.zig:2:16: 0x21000b in main (test)
var x = foo("hello");
^
/home/andy/tmp/zig/lib/std/start.zig:596:22: 0x20f6bb in posixCallMainAndExit (test)
root.main();
^
/home/andy/tmp/zig/lib/std/start.zig:368:5: 0x20f181 in _start (test)
@call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
^
(process terminated by signal)
コンパイル時:
comptime {
var value: i32 = -1;
const unsigned = @intCast(u32, value);
_ = unsigned;
}
$ zig test test.zig
docgen_tmp/test.zig:3:36: error: type 'u32' cannot represent integer value '-1'
const unsigned = @intCast(u32, value);
^~~~~
ランタイム時:
const std = @import("std");
pub fn main() void {
var value: i32 = -1;
var unsigned = @intCast(u32, value);
std.debug.print("value: {}\n", .{unsigned});
}
$ zig build-exe test.zig
$ ./test
thread 1639895 panic: attempt to cast negative value to unsigned integer
docgen_tmp/test.zig:5:5: 0x21020d in main (test)
var unsigned = @intCast(u32, value);
^
/home/andy/tmp/zig/lib/std/start.zig:596:22: 0x20f89b in posixCallMainAndExit (test)
root.main();
^
/home/andy/tmp/zig/lib/std/start.zig:368:5: 0x20f361 in _start (test)
@call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
^
(process terminated by signal)
符号なし整数の最大値を得るには、std.math.maxInt
を使用します。
コンパイル時:
comptime {
const spartan_count: u16 = 300;
const byte = @intCast(u8, spartan_count);
_ = byte;
}
$ zig test test.zig
docgen_tmp/test.zig:3:31: error: type 'u8' cannot represent integer value '300'
const byte = @intCast(u8, spartan_count);
^~~~~~~~~~~~~
ランタイム時:
const std = @import("std");
pub fn main() void {
var spartan_count: u16 = 300;
const byte = @intCast(u8, spartan_count);
std.debug.print("value: {}\n", .{byte});
}
$ zig build-exe test.zig
$ ./test
thread 1639955 panic: integer cast truncated bits
docgen_tmp/test.zig:5:5: 0x2101c6 in main (test)
const byte = @intCast(u8, spartan_count);
^
/home/andy/tmp/zig/lib/std/start.zig:596:22: 0x20f84b in posixCallMainAndExit (test)
root.main();
^
/home/andy/tmp/zig/lib/std/start.zig:368:5: 0x20f311 in _start (test)
@call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
^
(process terminated by signal)
ビットを切り捨てるには、@truncate を使用します。
以下の演算子は、整数のオーバーフローを引き起こす可能性があります。
+
(加算)-
(減算)-
(否定)*
(乗算)/
(除算)- @divTrunc (除算)
- @divFloor (除算)
- @divExact (除算)
コンパイル時に加算を行う例:
comptime {
var byte: u8 = 255;
byte += 1;
}
$ zig test test.zig
docgen_tmp/test.zig:3:10: error: overflow of integer type 'u8' with value '256'
byte += 1;
~~~~~^~~~
ランタイム時:
const std = @import("std");
pub fn main() void {
var byte: u8 = 255;
byte += 1;
std.debug.print("value: {}\n", .{byte});
}
$ zig build-exe test.zig
$ ./test
thread 1640016 panic: integer overflow
docgen_tmp/test.zig:5:5: 0x2101b4 in main (test)
byte += 1;
^
/home/andy/tmp/zig/lib/std/start.zig:596:22: 0x20f83b in posixCallMainAndExit (test)
root.main();
^
/home/andy/tmp/zig/lib/std/start.zig:368:5: 0x20f301 in _start (test)
@call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});
^
(process terminated by signal)
標準ライブラリで提供されるこれらの関数は、起こりうるエラーを返します。
@import("std").math.add
@import("std").math.sub
@import("std").math.mul
@import("std").math.divTrunc
@import("std").math.divFloor
@import("std").math.divExact
@import("std").math.shl
加算のオーバーフローをキャッチする例:
const math = @import("std").math;
const print = @import("std").debug.print;
pub fn main() !void {
var byte: u8 = 255;
byte = if (math.add(u8, byte, 1)) |result| result else |err| {
print("unable to add one: {s}\n", .{@errorName(err)});
return err;
};
print("result: {}\n", .{byte});
}
$ zig build-exe test.zig
$ ./test
unable to add one: Overflow
error: Overflow
/home/andy/tmp/zig/lib/std/math.zig:484:5: 0x210023 in add__anon_2890 (test)
return if (@addWithOverflow(T, a, b, &answer)) error.Overflow else answer;
^
docgen_tmp/test.zig:8:9: 0x20ff3d in main (test)
return err;
^
これらの組み込み関数は、オーバーフローが発生したかどうかを bool
で返し、またオーバーフローしたビットを返します。
- @addWithOverflow
- @subWithOverflow
- @mulWithOverflow
- @shlWithOverflow
addWithOverflowの例です:
const print = @import("std").debug.print;
pub fn main() void {
var byte: u8 = 255;
var result: u8 = undefined;
if (@addWithOverflow(u8, byte, 10, &result)) {
print("overflowed result: {}\n", .{result});
} else {
print("result: {}\n", .{result});
}
}
$ zig build-exe test.zig
$ ./test
overflowed result: 9
Zig 言語はプログラマに代わってメモリ管理を行いません。このため、Zig にはランタイムがなく、Zig のコードはリアルタイム・ソフトウェア、OSカーネル、組み込みデバイス、低遅延サーバなど、多くの環境でシームレスに動作します。その結果、Zig のプログラマは常にこの問いに答えられなければなりません。
Zigと同じく、C言語でもメモリ管理は手動で行います。しかし Zig とは異なり、C にはデフォルトのアロケータ、つまり malloc
、realloc
、free
があります。libc とリンクするとき、Zig はこのアロケーターを std.heap.c_allocator
で公開します。しかし、慣習として、Zig にはデフォルトのアロケータはありません。代わりに、割り当てが必要な関数は Allocator パラメータを受け取ります。同様に、std.ArrayList
のようなデータ構造もその初期化関数で Allocator
パラメータを受け付けます。
const std = @import("std");
const Allocator = std.mem.Allocator;
const expect = std.testing.expect;
test "using an allocator" {
var buffer: [100]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const result = try concat(allocator, "foo", "bar");
try expect(std.mem.eql(u8, "foobar", result));
}
fn concat(allocator: Allocator, a: []const u8, b: []const u8) ![]u8 {
const result = try allocator.alloc(u8, a.len + b.len);
std.mem.copy(u8, result, a);
std.mem.copy(u8, result[a.len..], b);
return result;
}
$ zig test allocator.zig
1/1 test.using an allocator... OK
All 1 tests passed.
上記の例では、100 バイトのスタックメモリが FixedBufferAllocator
の初期化に使われ、それが関数に渡されます。便宜上、グローバルな FixedBufferAllocator
が std.testing.allocator
で用意されており、基本的なリーク検出も行うことができます。
Zig には std.heap.GeneralPurposeAllocator
でインポート可能な汎用アロケータがあります。しかし、やはり、アロケータの選択ガイドに従うことが推奨されます。
どのアロケーターを使用するかは、様々な要因によって決まります。以下にフローチャートを示しますので、判断の参考にしてください。
- ライブラリを作っているのですか?この場合、アロケータをパラメータとして受け取り、ライブラリのユーザがどのアロケータを使うかを決定するのがベストです。
- libc をリンクしていますか? この場合、少なくともメインのアロケータは
std.heap.c_allocator
が正しい選択だと思われます。 - 必要なバイト数の最大値は、コンパイル時に分かっている数で制限されていますか? この場合、スレッドセーフが必要かどうかによって
std.heap.FixedBufferAllocator
かstd.heap.ThreadSafeFixedBufferAllocator
を使ってください。 - あなたのプログラムはコマンドラインアプリケーションで、基本的な循環パターンを持たずに最初から最後まで実行され(ビデオゲームのメインループやウェブサーバのリクエストハンドラなど)、最後にすべてを一度に解放することに意味があるようなものでしょうか?このような場合、このパターンに従うことをお勧めします。
const std = @import("std");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const ptr = try allocator.create(i32);
std.debug.print("ptr={*}\n", .{ptr});
}
$ zig build-exe cli_allocation.zig
$ ./cli_allocation
ptr=i32@7f194545d018
この種のアロケータを使用する場合、手動で何かを解放する必要はありません。arena.deinit()
を呼び出せば、すべてが一度に解放されます。
- アロケーションはビデオゲームのメインループや Web サーバのリクエストハンドラのような周期的なパターンの一部になっていますか?例えば、ビデオゲームのフレームが完全にレンダリングされた後や、ウェブサーバーのリクエストが処理された後など、サイクルの終わりにすべてのアロケーションを一度に解放できる場合、
std.heap.ArenaAllocator
は素晴らしい候補となります。前の箇条書きで示したように、これによってアリーナ全体を一度に解放することができます。また、メモリの上限を設定できる場合は、std.heap.FixedBufferAllocator
を使用すると、さらに最適化できることに注意しましょう。 - テストを書いていて、
error.OutOfMemory
が正しく処理されることを確認したいですか?この場合はstd.testing.FailingAllocator
を使ってください。 - テストを書いていますか?この場合は
std.testing.allocator
を使ってください。 - 最後に、上記のどれにも当てはまらない場合は、汎用のアロケータが必要です。Zig の汎用アロケータは、設定オプションの comptime 構造体を受け取り、型を返す関数として提供されている。一般的には、メイン関数に
std.heap.GeneralPurposeAllocator
をひとつセットアップし、アプリケーションの様々な部分にそのアロケータやサブアロケータを渡していくことになる。 - また、アロケータの実装を検討することもできます。
"foo" のような文字列リテラルは、グローバル定数データセクションにあります。このため、このように文字列リテラルをミュータブルスライスに渡すとエラーになります。
fn foo(s: []u8) void {
_ = s;
}
test "string literal to mutable slice" {
foo("hello");
}
$ zig test test.zig
docgen_tmp/test.zig:6:9: error: expected type '[]u8', found '*const [5:0]u8'
foo("hello");
^~~~~~~
docgen_tmp/test.zig:6:9: note: cast discards const qualifier
しかし、スライスを一定にすれば、うまくいくのです。
fn foo(s: []const u8) void {
_ = s;
}
test "string literal to constant slice" {
foo("hello");
}
$ zig test strlit.zig
1/1 test.string literal to constant slice... OK
All 1 tests passed.
文字列リテラルと同様に、コンパイル時に値が分かっている const
宣言は、グローバル定数データセクションに格納されます。また、コンパイル時変数もグローバル定数データセクションに格納されます。
関数内の var
宣言は、その関数のスタックフレームに格納されます。関数が戻ると、関数のスタックフレームにある変数へのポインタは無効な参照となり、その参照解除は未確認の未定義動作となります。
トップレベルまたは構造体宣言のvar宣言は、グローバルデータセクションに格納されます。
allocator.alloc
や allocator.create
で確保されたメモリの格納場所は、アロケータの実装によって決定される。
Zig プログラマは Allocator インターフェースを満たすことで、自分自身のアロケーターを実装することができます。そのためには、std/mem.zig にあるドキュメントコメントをよく読んで、 allocFn
と resizeFn
を用意する必要があります。
インスピレーションを得るために、多くのアロケータの例を見ることができます。std/heap.zig と std.heap.GeneralPurposeAllocator
を見てください。
多くのプログラミング言語では、ヒープ割り当てに失敗した場合、無条件にクラッシュすることで対処しています。Zigのプログラマは、慣習として、これが満足のいく解決策であるとは考えていません。その代わりに、error.OutOfMemory
はヒープ割り当ての失敗を表し、Zigライブラリはヒープ割り当ての失敗で処理が正常に完了しなかったときはいつでもこのエラーコードを返します。
Linuxなどの一部のOSでは、デフォルトでメモリのオーバーコミットが有効になっているため、ヒープ割り当ての失敗を処理することは無意味であると主張する人もいます。この理由には多くの問題があります。
- オーバーコミット機能を持つのは一部のオペレーティング・システムだけです。
- Linuxはデフォルトでオーバーコミットが有効になっていますが、設定可能です。
- Windows はオーバーコミットしません。
- 組み込みシステムにはオーバーコミットがありません。
- 趣味のOSでは、オーバーコミットがある場合とない場合があります。
- リアルタイムシステムの場合、オーバーコミットがないだけでなく、通常、アプリケーションごとにメモリの最大量があらかじめ決められています。
- ライブラリを書くときの主な目的の1つは、コードの再利用です。アロケーションの失敗を正しく処理することで、ライブラリはより多くのコンテキストで再利用されるようになります。
- オーバーコミットが有効であることに依存するようになったソフトウェアもありますが、その存在は数え切れないほどのユーザ体験の破壊の原因になっています。オーバーコミットを有効にしたシステム、例えばデフォルト設定のLinuxでは、メモリが枯渇しそうになると、システムがロックして使えなくなる。このとき、OOM Killer はヒューリスティックに基づき kill するアプリケーションを選択します。この非決定的な判断により、重要なプロセスが強制終了されることが多く、システムを正常に戻すことができないことがよくあります。
再帰は、ソフトウェアのモデリングにおいて基本的なツールである。しかし、しばしば見落とされがちな問題があります: 無制限のメモリ割り当てです。
再帰は Zig で活発に実験されている分野なので、ここにあるドキュメントは最終的なものではありません。0.3.0のリリースノートで、再帰の状況を要約して読むことができます。
簡単にまとめると、現在のところ再帰は期待通りに動作しています。Zig のコードはまだスタックオーバーフローから保護されていませんが、Zig の将来のバージョンでは、Zig のコードからのある程度の協力が必要ですが、そのような保護を提供することが予定されています。
ポインタの指すメモリが利用できなくなったときに、ポインタがアクセスされないようにするのは、Zigプログラマの責任です。スライスは他のメモリを参照するという点で、ポインタの一種であることに注意してください。
バグを防ぐために、ポインタを扱うときに従うと便利な規約がいくつかあります。一般に、関数がポインターを返す場合、その関数のドキュメントでは、誰がそのポインターを「所有」しているかを説明する必要があります。この概念は、プログラマがポインタを解放することが適切である場合、そのタイミングを判断するのに役立ちます。
例えば、関数のドキュメントに「返されたメモリは呼び出し元が所有する」と書かれていた場合、その関数を呼び出すコードは、いつそのメモリを解放するかという計画を持っていなければなりません。このような場合、おそらく関数は Allocator
パラメータを受け取ります。
時には、ポインタの寿命はもっと複雑な場合があります。例えば、std.ArrayList(T).items
スライスは、新しい要素を追加するなどしてリストのサイズが次に変更されるまで有効である。
関数やデータ構造のAPIドキュメントでは、ポインタの所有権と有効期限について細心の注意を払って説明する必要があります。所有権とは、ポインタが参照するメモリを解放する責任が誰にあるかということであり、寿命とは、メモリがアクセス不能になる時点(未定義動作が発生しないように)を決めることである。
Zig Build Systemは、プロジェクトをビルドするために必要なロジックを宣言するための、クロスプラットフォームで依存性のない方法を提供します。このシステムでは、プロジェクトをビルドするためのロジックは build.zig ファイルに記述され、Zig Build System API を使ってビルドアーチファクトやその他のタスクを宣言し、設定することができます。
ビルドシステムが支援するタスクの例をいくつか挙げます。
- Zigコンパイラの実行によるビルドの成果物の作成。これには、CやC++のソースコードだけでなく、Zigのソースコードのビルドも含まれます。
- ユーザが設定したオプションを取得し、そのオプションを使用してビルドを設定する。
- Zigのコードからインポート可能なファイルを提供することで、ビルド構成をcomptimeの値として表面化させる。
- ビルド内容をキャッシュし、不要なステップの繰り返しを回避します。
- ビルド・アーティファクトやシステムにインストールされたツールの実行
- テストを実行し、ビルド・アーティファクトの実行による出力が期待値と一致することを確認する。
- コードベースまたはそのサブセットに対してzig fmtを実行する。
- カスタムタスク。
ビルドシステムを使用するには、zig build --helpを実行すると、コマンドライン使用法のヘルプメニューが表示されます。これには、build.zigスクリプトで宣言されたプロジェクト固有のオプションが含まれます。
この build.zig ファイルは、zig init-exe によって自動的に生成されます。
const Builder = @import("std").build.Builder;
pub fn build(b: *Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("example", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
この build.zig ファイルは、zig init-lib によって自動的に生成されます。
const Builder = @import("std").build.Builder;
pub fn build(b: *Builder) void {
const mode = b.standardReleaseOptions();
const lib = b.addStaticLibrary("example", "src/main.zig");
lib.setBuildMode(mode);
lib.install();
var main_tests = b.addTest("src/main.zig");
main_tests.setBuildMode(mode);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
}
lib.addCSourceFile("src/lib.c", &[_][]const u8{
"-Wall",
"-Wextra",
"-Werror",
});
これらのコーディング規約はコンパイラによって強制されるものではありませんが、誰かが Zig コーディングスタイルについて合意した権威を指摘したい場合に参照するポイントを提供するために、コンパイラと一緒にこの文書に収録されています。
- 4スペースインデント
- 中括弧は、折り返す必要がない限り、同じ行に開く
- リストが2より長い場合、各項目を独立した行に置き、最後に余分なカンマを置く機能を行使する
- 行の長さ: 100を目安に、常識的な範囲で
大雑把に言うと、camelCaseFunctionName
、 TitleCaseTypeName
、snake_case_variable_name
です。より正確には
x
が型の場合、x
はTitleCase
にすべきです。ただし、フィールドが 0 個の構造体で、インスタンス化されることがない場合は除きます。この場合、それは「名前空間」であるとみなされ、snake_case
が使用されますx
が呼び出し可能で、x
の戻り値がtype
である場合、x
はTitleCase
であるべきです。x
が他に呼び出し可能な場合、x
はcamelCase
であるべきである。- そうでなければ、
x
はsnake_case
であるべきです。
頭字語、イニシャリズム、固有名詞など、英語の書き言葉で大文字と小文字の区別があるものは、他の単語と同じように命名規則が適用されます。たった2文字の頭字語であっても、命名規則が適用されます。
ファイル名は、型と名前空間の2つのカテゴリに分類されます。ファイル(暗黙のうちに構造体)がトップレベルのフィールドを持つ場合、フィールドを持つ他の構造体と同様に TitleCase
を使って命名する必要があります。そうでない場合は、snake_case
を使用します。ディレクトリ名は snake_case
を使用します。
これらは一般的な経験則であり、もし異なることをするのが理にかなっているのであれば、理にかなっていることをするのが良いでしょう。例えば、ENOENTのような確立された慣習がある場合は、その慣習に従います。
const namespace_name = @import("dir_name/file_name.zig");
const TypeName = @import("dir_name/TypeName.zig");
var global_var: i32 = undefined;
const const_name = 42;
const primitive_type_alias = f32;
const string_alias = []u8;
const StructName = struct {
field: i32,
};
const StructAlias = StructName;
fn functionName(param_name: TypeName) void {
var functionPointer = functionName;
functionPointer();
functionPointer = otherFunction;
functionPointer();
}
const functionAlias = functionName;
fn ListTemplateFunction(comptime ChildType: type, comptime fixed_size: usize) type {
return List(ChildType, fixed_size);
}
fn ShortList(comptime T: type, comptime n: usize) type {
return struct {
field_name: [n]T,
fn methodName() void {}
};
}
// The word XML loses its casing when used in Zig identifiers.
const xml_document =
\\<?xml version="1.0" encoding="UTF-8"?>
\\<document>
\\</document>
;
const XmlParser = struct {
field: i32,
};
// The initials BE (Big Endian) are just another word in Zig identifier names.
fn readU32Be() u32 {}