Zig v0.14.0 リリース

2025/03/07に公開

イントロ

寒すぎると最近話題の3月です。自分が住んでいる京都はどうも夏と冬しかないらしく、春というものは永遠に来ないと聞きました。外に出るときにはマフラーとファミチキが手放せないですね。
さて、日本時間の2025年03月05日に Zig v0.14.0 がリリースされました。当初の01月01日のリリースが延期されて、さらに 02月14日のリリースも延期されて、当初の予定から2ヶ月遅れのめでたいリリースとなりました。

本記事では、0.14.0 における変更点のうち気になったものをピックアップして紹介していきます。詳細な変更点については リリースノート を参照してください。

UEFI が Tier System から外れる

Zig では各プラットフォームごとのサポート状況を Tier System として分類しています。Tier 1 サポートは非 experimental な機能が問題なく動作することが保証されるレベルであり、0.14.0 の時点での Tier 1 は x86_64-linux だけです。x64 におけるそれ以外の代表的プラットフォーム (Mac/Windows) は Tier 2 となっています。
Zig は UEFI アプリケーションのビルドにも対応しており、通常のユーザアプリをビルドするのと同じ感覚で UEFI を書くことができます。OS に依存しない標準ライブラリも勿論利用することができます。x64 の UEFI プラットフォームのサポートは、0.13.0 の時点では Tier 2 でした。0.14.0 からは、UEFI プラットフォームは 通常の Tier System からは除外されるようになった ようです。とはいえこれらのプラットフォームが使えなくなるというわけではなく、単純に Tier System から外れただけのようです。

標準ライブラリの多くのシンボルの改名

0.13.0 から 0.14.0 にマイグレートするにあたり、おそらく最も大きな労力が必要なのが標準ライブラリのシンボルのリネームです。Zig はリファレンスに Style Guide を含めており、そこでは変数名の命名規則について定めています。もちろん言語仕様に組み込まれているわけではないため従う必要はありません。ただし、命名規則を定めているものの 0.13.0 では 標準ライブラリの多くの変数名がこの命名規則に従っていませんでした。 0.14.0 では、この命名規則に従うように多くのシンボルがリネームされました。

とりわけ影響が大きいのは、std.builtin.Type enum です。これまでは .Int.Pointer のようにタイトルケースだったフィールドが、.int.pointer のようにリネームされました。また、structunion のような予約ごと衝突するフィールドは @"struct"@"union" のように Quoted Identifier を使うようになりました。
その他だと、std.os.uefi 以下のシンボルも命名規則に従うように一括で変更されました。ブートローダなんかを自作している方はご注意ください。

build.zig.zon

build.zig.zon ファイルは、Zig におけるパッケージ管理に使う定義ファイルです。Rust の Cargo.toml や、JS の package.json なんかに相当します。Zig には一応公式のパッケージマネージャである Zon があり、これに依存パッケージを記述することでビルド時に勝手に依存をダウンロードして来てくれます。あまり使っている人は多くない印象です。ドキュメントは全く無いです。

さて、そんな build.zig.zon ファイルは Zig の構造体リテラルを使って記述されます。0.14.0 からは、必要なフィールドとして fingerprint が増えました。これはパッケージの一意な ID となります。これまではあるパッケージを追加したい場合には、そのパッケージの README に書いてあるハッシュ値をコピーしてくるか、もしくは黒魔術を使って依存にしたいパッケージのハッシュを計算する必要がありました。この finterprint は、そのような不便を解消してくれるのではないかと期待されます。ただし、現在のところは fingerprint を利用するツールは (Zig公式では) 一切用意されていないと明記されています
そんな fingerprint ですが、計算方法は (これまでと比べて) 遥かに簡単です。最初に fingerprint を定義せずに zig build すると、エラーメッセージに使うべきハッシュの値が表示されます:

/home/smallkirby/norn/build.zig.zon:1:2: error: missing top-level 'fingerprint' field; suggested value: 0xe35841a04984176
.{
 ^

また、プロジェクト名を表す name が文字列から enum literal になりました。これに伴い、Zig の identifier として不正な文字・記号は name に使えなくなりました。

ZON ファイルのパース

build.zig.zon に関連してもう一つ。ZON ファイルはせっかく Zig の構造体として定義されるのに、これまでは ZON ファイルを読み込んでパースする機能がありませんでした。0.14.0 では、ZON ファイルをインポートできるようになります。実行時に読み込んでパースすることも可能です。
ただし、読み込む構造体の型は予め既知である必要があります。例として、build.zig から build.zig.zon のバージョン情報を読み込みたいと思ったら、以下のように書くことになります:

const ZonStruct = struct {
    version: []const u8,
    name: @Type(.enum_literal),
    fingerprint: u64,
    minimum_zig_version: []const u8,
    dependencies: struct {},
    paths: []const []const u8,
};

const zon: ZonStruct = @import("build.zig.zon");
const norn_version = zon.version;

結構めんどくさいですね。というか、build.zig.zon の定義くらいは標準ライブラリに入れておいてほしいですね。

Decl Literals

新しいシンタックスである Decl Literals が導入されました。こいつが結構便利そうでありつつ、ちょっと気持ち悪いです:

const S = struct {
    x: u32,

    const default: S = .{ .x = 123 };
};

このような構造体の定義と定数が存在していた場合、0.13.0 までは以下のようにして default を使う必要がありました:

var value = S.default;

これが、Decl Literals を使うと以下のように書けます:

var value: S = .default;

これまで単なる Enum Literals だった .default という記法が拡大され、.default と書くことができます。結果の型が S であるため、この literal は S.default と同義になります。正直この例だけだと S を書く場所が変わっただけに見えますが、すでに存在する S 型の変数に代入する場合には、value = .default と書くだけで済むようになります。実際にこれが有効活用されているのが以下の Calling Convention の例です。

Calling Convention

Zig では関数の Calling Convention を指定することができます。Zig のデフォルトの .Stdcall や、C の呼び出し規約に沿う .C、プロローグ・エピローグを持たない .Naked などがありました:

// Zig が勝手に最適化していい呼び出し
fn zigFunction() void {}
// C の呼び出し規約に従う
fn cFunction() callconv(.C) void {}

0.14.0 からは、CallingConvention は Tagged Union になり、Zig がサポートする様々なプラットフォームの呼び出し規則を定義しています。.x86_64_sysv をはじめとして、.x86_64_interrupt.avr_signal なんてものもあります:

https://github.com/ziglang/zig/blob/4cefd1bd1bac7059f7e00162a579f659e5f59412/lib/std/builtin.zig#L169

Tagged Union なので、ユニオン値ごとに異なる型をもたせることが可能です。これにより、一部の convention は、オプションを取ることが可能になりました。例として、スタックのアラインメントや (x86だけですが) 引数渡しに使うレジスタの個数も指定できるようになっています。

なお、.Naked.C が Naming Convention に従っていないのではと思ったあなた、正解です。これらは .naked.c にリネームされました。一応 0.14.0 の段階では 従来の .Naked.C も使えるようにエイリアスされていますが、deprecated されており将来のリリースで削除される予定 なので気をつけてください。なお、以下の記法は先ほど紹介した Decl Literals を使っています。最初のユーザですね:

    /// Deprecated; use `.c`.
    pub const C: CallingConvention = .c;
    /// Deprecated; use `.naked`.
    pub const Naked: CallingConvention = .naked;

@branchHint

0.13.0 まで存在していた @setCold() ビルトイン関数が削除されました。自分は OS のパニックハンドラ(復帰不可)関数で設定したりしていました。
それに代わり、上位互換の @branchHint() ビルトインが追加されました。これは @setCold() を完全に包含しており、その他にも .likely.unlikely といった分岐予測ヒントも指定することができます。これまでの @setCold()@branchHint(.cold) に置き換えることが可能です。

Allocator API

std.mem.Allocator は、std.mem.Allocator.VTable という関数テーブルを持つインタフェースであり、バックエンド実装によらず共通の API でメモリアロケータを利用することができます。これまでは、alloc() / free() / resize() という3つの関数を持っていました。これに、新しく remap() という関数が追加されました
resize()remap() の違いは、リサイズ後のメモリ領域を指すポインタが変化する可能性がないか・あるかです。resize() は指定された領域のリサイズができない場合にはエラーを返します。一方、新しく追加された remap() は領域のリサイズができない場合には、新しい領域を確保して古い領域の中身をコピーし、もとの領域を解放します。もとの領域のアドレスを変えることなくリサイズできる場合には、両者は同一になります / なるように実装されるべきです。

グローバル変数のアドレス

待望の修正です!これまではバグにより、グローバル変数のアドレスを使ってグローバル変数の初期化をすることができませんでした:

const a: u64 = 3;

const S = struct { ptr: *u64 };
const b: S = .{ .ptr = &a }; // LLVM がクラッシュする

これが 0.14.0 からはできるようになります。それに加え、グローバル変数が互いのアドレスを参照することも可能になりました:

const Node = struct {
    next: *const Node,
};

const a: Node = .{ .next = &b };
const b: Node = .{ .next = &a };

今までこれのワークアラウンドのために一度 bundefined で初期化しておいて、プログラムの起動時に &a を代入するというめんどくさいことをする必要があったので、治って嬉しいですね。

Packed Struct の比較

Zig では Struct の比較は std.meta.eql() を使って行います。もしくは、@bitCast() 型を使って構造体を単なるスカラー値にし、それを比較することもできます (Zig では任意のビット幅の整数型が許されます)。
0.14.0 からは、Packed Struct を通常の比較演算子を使って比較することが可能になりました。地味に便利そうですね:

test {
  const S = packed struct(u32) { x: u32 };
    const a = S{ .x = 123 };
    const b = S{ .x = 123 };
    try testing.expect(std.meta.eql(a, b));
    try testing.expect(a == b); // <== NEW!
}

気になるバグ

さて、最後にちょっと気になるバグについてご紹介します。0.14.0 では、以下のテストが通らなくなりました:

const S1 = packed struct {
    x: u16,
    y: *u64,
};

var variable: u64 = 3;

test {
  const value1 = S1{ .x = 42, .y = &variable };
  try testing.expectEqual(&variable, value1.y);
  try test1(value1);
}

noinline fn test1(value: S1) !void {
    try testing.expectEqual(&variable, value.y);
}

https://github.com/ziglang/zig/issues/23101

どうやら、Packed Struct にグローバル変数のポインタを入れるとバグるようです。この問題自体は LLVM 19 にアップグレードした際に発生したらしく、自動テストで去年の時点で判明していたものの、一時的に当該テストを無効化したまま、修正することなく又は無効化したことを忘れたままリリースされてしまったようです。このユースケースは結構使うこともある (自分は自作OSで GDTR を設定する時に使っていて気がついた) ため、修正されると嬉しいですね。なお、この課題には 0.14.1 マイルストーンがつけられていますが、Zig ってパッチバージョンのアップデートなんて今までありましたっけ...?

アウトロ

Zig は、LLVM Project への依存からの脱却を目指しているようです。リンクした課題にもあるように、LLVM / Clang / LLD 等のすべてのコンポーネントへの依存を無くす そうです。理由としては、LLVM のバグに振り回されたくない・Zig をビルドするのに LLVM をインストールするのがめんどくさい、コンパイラのバイナリサイズもでかいし速度も遅いといったことが挙げられています。そのための第一歩として、0.14.0 の x64 ネイティブバックエンドは 98% のテストを通過したそうです。このリリースノートでは、次のリリースにおいてデバッグビルドのデフォルトがネイティブバックエンドになるということも書かれています。
みなさんも、CI のテストでは -fno-llvm をつけてネイティブバックエンドの到来に備えておきましょう。

GitHubで編集を提案

Discussion