- はじめに — 8か月、1183コミット、そしてstdlibを塗り替えたリリース
- Ioを引数で渡す — Allocatorが歩んだ道
- 実装リストの読み方 — 何が完成で何が実験か
- asyncは何を約束し、何を約束しないか
- キャンセルという、本当に難しい部分
- 「Juicy Main」— mainがIoを作る
- 関数の色問題は解決したのか — リリースノートはその言葉を口にしない
- コンパイラ側 — 増分コンパイルと新ELFリンカ
- バックエンドの現況 — x86はデフォルト、aarch64は止まった
- アップグレードの代償 — 何が壊れるか
- 今、Zigを使うべきか
- おわりに
- 参考資料
はじめに — 8か月、1183コミット、そしてstdlibを塗り替えたリリース
Zig 0.16.0が2026年4月14日に発表されました。公式アナウンスは短く — 8か月の作業、244名のコントリビューター、1183件のコミット。(ダウンロードインデックスに記載されたtarballの日付は1日早い2026-04-13です。)リリースノートが自ら掲げる見出しは一つだけです。「I/O as an Interface」。
一文でまとめるとこうです。リリースノートの表現をそのまま借りれば、Zig 0.16.0以降、あらゆる入出力機能はIoインスタンスを引数として受け取らなければなりません。ファイルを読むにせよ、ソケットに書き込むにせよ、スリープするにせよ、標準出力に1行出すにせよ同じです。判断基準もノートに書かれています — 制御フローをブロックしうる、あるいは非決定性を持ち込むものは、おおむねI/Oインターフェースが所有すべき対象だということです。
このブログは少し前にHashimotoがGhosttyをZigで作る理由を取り上げました。そこで彼は0.15が「出力インターフェース、そして何かを出力するすべて」を変えたと語り、その破壊的変更をむしろ歓迎していました。0.16.0はその物語の次の章であり、はるかに大きな章です。今回は出力だけでなく、入出力全体です。
本稿は宣伝文句ではなくリリースノートを読みます。何が実際に完成し、何が「実験的」と書かれ、どの数字がどんな条件で測定されたのかを追います。結論を先に言えば — 設計は興味深く、請求書は大きい。
Ioを引数で渡す — Allocatorが歩んだ道
Zigを少しでも触ったことがある人には、この設計は見慣れたものに映るはずです。Zigにはグローバルなアロケータがありません。メモリを必要とする関数はAllocatorを引数として受け取り、それを実際に何で満たすかは呼び出し側が決めます。ライブラリは自分がアリーナの上で動いているのかGPAの上で動いているのか知る必要がありません。
0.16.0は同じ手をI/Oに置きます。I/Oを必要とする関数はIoを引数に取ります。そしてそれがスレッドプールなのか、イベントループなのか、io_uringなのかは呼び出し側 — 正確にはアプリケーションのmain — が決めます。
リリースノートのHTTP例は、この設計の野心をよく示しています。ドメイン一つにHEADリクエストを送る、一見なんの変哲もないコードです。
const std = @import("std");
const Io = std.Io;
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
const args = try init.minimal.args.toSlice(init.arena.allocator());
const host_name: Io.net.HostName = try .init(args[1]);
var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
defer http_client.deinit();
var request = try http_client.request(.HEAD, .{
.scheme = "http",
.host = .{ .percent_encoded = host_name.bytes },
.port = 80,
.path = .{ .percent_encoded = "/" },
}, .{});
defer request.deinit();
try request.sendBodiless();
var redirect_buffer: [1024]u8 = undefined;
const response = try request.receiveHead(&redirect_buffer);
std.log.info("received {d} {s}", .{ response.head.status, response.head.reason });
}
ノートは、このコードが — 1行も書き足していないのに — 次の性質を持つと述べます。設定された各ネームサーバーへDNSクエリを非同期に送り、応答が届いた端からそのIPへのTCP接続を非同期に試み、最初のTCP接続が成功した瞬間、進行中の他の接続試行をDNSクエリごと全部キャンセルします。Windowsでは、これらすべてがws2_32.dllへの依存なしに起こります。
ここで本当に面白いのは次の一文です。ノートはこのコードが-fsingle-threadedでコンパイルしても動くと書いています — ただしその場合は演算が逐次的に起こるという注釈つきで。同じソース、異なる実行モデル、それでも正しい動作。これがこの設計が売っているものです。
実装リストの読み方 — 何が完成で何が実験か
インターフェースは実装があってこそ意味を持ちます。そしてこのリストこそが、0.16.0をめぐる誇張と現実を分ける場所です。ノートに書かれた修飾語をそのまま移すとこうなります。
Io.Threaded— スレッドベース。ファイルシステム操作がread、write、open、closeを直接呼びます。0.15.xからコードを移す際に同等の挙動を与える実装です。ノートの表現では機能が完全でよくテストされており、キャンセルにも対応しています。「Juicy Main」が現在選んでいる実装でもあります。Io.Evented— ノートの表現そのままに作業中で実験的であり、インターフェースの進化の方向性を探るためのものです。ユーザースペースのスタックスイッチングとワークスティーリングが基盤 — M:Nスレッディング、グリーンスレッド、スタックフルコルーチンと呼ばれるものです。Io.Uring— 今回のリリースサイクルの焦点ではなかったとノートは明かします。概念実証レベルで、ネットワーキングとエラー処理とテストカバレッジを欠いています。Io.Kqueue— 概念実証専用です。Io.Dispatch— macOSのGrand Central Dispatchベース。Io.failing— 一切の操作をサポートしないシステムをシミュレートします。
このリストを正直に読めば、結論は一つです。0.16.0がプロダクションに届けたのはスレッドベースのI/O一つだけであり、人々がこのリリースについて語り興奮している対象 — io_uringの上で動くZig非同期 — はまだ届いていません。インターフェースはそれを収める形を作っただけです。
この区別は皮肉ではありません。むしろZigチームがリリースノートに自らこう書いていること自体が印象的です。多くのプロジェクトは概念実証を「サポート」と呼びます。
asyncは何を約束し、何を約束しないか
io.asyncはFuture(T)を作ります。ここでTは呼び出された関数の戻り値の型です。ノートが説明する意味論は微妙で重要です。
io.asyncは非同期性を表現します — この関数呼び出しが他のロジックから独立しているという事実を。だからこのタスク生成は失敗しえず、並行性の仕組みが一切ない制限されたIo実装の上でも移植可能です。ノートはここで極めて率直な一文を付け加えます — Io実装がasync呼び出しを、関数を直接呼んで返すだけの形で実装しても合法だということです。
これはよく噛みしめる価値があります。io.asyncは「同時に実行せよ」という命令ではありません。「これは独立している」という宣言です。実際に同時に動かすかどうかは実装が決めます。だから-fsingle-threadedビルドでもコードがコンパイルでき、正しく動作するのです — ただ逐次的に動くだけで。
本当に並行性が必要なときのためにio.concurrentが別に用意されています。ノートの表現では、これは正しさのために必ず同時に実行されなければならないことを伝えます。そしてこれは必然的にメモリ割り当てを要求し — 何かを同時に行うことの本質がそうだとノートは説明します — したがって失敗しえます。error.ConcurrencyUnavailableがそれです。
つまりZigは「非同期でもよいもの」と「必ず同時でなければならないもの」を型システムのレベルで別々の関数に分けました。前者は無限に移植可能で失敗せず、後者はリソースを要求し失敗しえます。これはほとんどのasyncランタイムが曖昧にしてしまう区別であり、個人的にはこのリリースで最もよく設計された部分だと思います。
Futureには2つのメソッドがあります。awaitはタスクが終わるまで論理的に制御フローをブロックし、戻り値を返します。cancelはawaitと同じですが、実装に対して演算を中断しerror.Canceledを返すよう要求します。
複数のタスクの寿命が同じときにはGroupがあり、ノートはN個のタスクを起動するのにO(1)のオーバーヘッドだと書いています。このほかQueue(T)、Select、Batch、そして単位を型として強制するClock、Duration、Timestamp、Timeoutが一緒に入りました。
キャンセルという、本当に難しい部分
非同期ランタイムを作ったことがある人なら知っています。難しいのはタスクを起動することではなく、殺すことです。0.16.0はここに紙幅をかなり割いています。
最も印象的な実装ディテールはこれです。ノートによればIo.Threadedでさえキャンセルに対応しており、方法はスレッドにシグナルを送ってブロッキングシステムコールがEINTRを返すようにし、そのエラーコードに反応してシステムコールを再試行する前にキャンセル要求があったか確認するというものです。イベントループもコルーチンもない普通のスレッドプールでキャンセルを実装する方法としてはなかなか賢いやり方です。
同時にノートは重要な注釈を添えます — キャンセルを要求しても、その要求が受け入れられるとは限りません。キャンセルは要求であって命令ではありません。受け入れられた要求だけがI/O操作にerror.Canceledを返させます。
そしてerror.Canceledの扱い方が3通りに整理されています。よくある順に — そのまま伝播させるか、io.recancel()を呼んでキャンセル要求を再武装したうえで伝播させないか、io.swapCancelProtection()で到達不能にするか。キャンセル要求を自ら出したロジックだけがerror.Canceledを無視しても安全だというルールも一緒に書かれています。
リソースリークを避けるパターンもノートが直接示しています。
var foo_future = io.async(foo, .{args});
defer if (foo_future.cancel(io)) |resource| resource.deinit() else |_| {}
var bar_future = io.async(bar, .{args});
defer if (bar_future.cancel(io)) |resource| resource.deinit() else |_| {}
const foo_result = try foo_future.await(io);
const bar_result = try bar_future.await(io);
cancelが必須である理由が説明されています — エラーが返されるとき(error.Canceledを含む)、非同期タスクのリソースを解放するのはまさにこの呼び出しだからです。つまりdefer cancelはキャンセルのためではなく、リソース回収のためのものであり、これはZigに初めて触れる人が必ず踏む地雷です。
「Juicy Main」— mainがIoを作る
Ioが引数だとすれば、その最初のIoは誰が作るのでしょうか。0.16.0の答えはmainです。
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
const ptr = try gpa.create(i32);
defer gpa.destroy(ptr);
try std.Io.File.stdout().writeStreamingAll(io, "Hello, world!\n");
}
mainにstd.process.Initパラメータを付けると、あらかじめ初期化されたものが付いてきます — プロセス全体の寿命を持つアリーナ、デフォルトで選ばれた汎用アロケータ、ターゲット設定に合わせて選ばれたデフォルトのIo実装、環境変数マップ、そしてWASI用のpreopens。Debugモードでは可能な場合リーク検査までセットしてくれるとノートは書いています。
mainの最初のパラメータは3つのうちどれかになりえます。なし(その場合CLI引数と環境変数にアクセスできません)、process.Init.Minimal(argvとenvironを生のまま)、あるいはprocess.Init。
環境変数がここに来た理由もノートに書かれており、説得力があります。環境がグローバル状態であるというのは非常によくある抽象化ですが、問題が多いということです — Cでsetenvのような関数をスレッドの文脈で呼ぶのは健全でなく、environが何のロックもなく直接アクセスされることがよくあるからです。さらにZig標準ライブラリには大きな落とし穴がありました。std.os.environはCのenvironと等価であることを意図していましたが、libcをリンクしないライブラリではそれを埋める方法がありませんでした。
Ioインスタンスがない場所で急いで一つ作る方法もノートが教えてくれますが、添えられた警告は正直です。
var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.io();
ノートは、これはタスクレベルの並行性が不要な限り動くが理想的とは言えない回避策だと述べます。たとえがうまい — Allocatorが必要なのにないからとstd.heap.page_allocatorを掴むようなものだということです。テストではstd.testing.allocatorと同じようにstd.testing.ioを使うよう勧めています。
関数の色問題は解決したのか — リリースノートはその言葉を口にしない
このリリースをめぐって最もよく出回っている要約が「Zigは関数の色問題を解決した」です。そこで確認してみました。0.16.0リリースノート全文に「color」という単語は一度も登場しません。
その一文の出所は別にあります。Zigコアチームの Loris Cro が2025年7月13日に書いたZig's New Async I/Oです。彼はそこで3段階で主張を積み上げます — 一つのライブラリが同期モードと非同期モードの両方で最適に動作しうる、以前はソースコードがasync/awaitの伝染から自由であっても実行時にはスタックレスコルーチンが強制されていたが、今ではio.asyncとFuture.awaitをさまざまな実行モデルとともに使える、そして — 彼の文章そのままに — この最後の改善によってZigは関数の色問題を完全に打ち負かした、というものです。
ここには注釈が要ります。あの記事は0.16.0ではなく当時の計画を語った2025年7月の記事であり、標準ライブラリの書き直しがまだ残っていて0.15.0にはそのごく一部しか入らないと自ら書いています。そして計画は実際に変わりました。Croが言及したスタックレスコルーチンは0.16.0のノートに一度も出てこず、実際に入ったIo.Eventedはスタックフルコルーチンベースです。
反対側の主張もあります。ivnjという書き手が同じ日、2025年7月13日に書いた記事の論旨はこうです — 意味論的にすべての関数にstd.Ioを渡すことは、すべてのNode.js関数をasyncにしてPromiseを返させることと変わらず、Zigは関数の色をブロッキング/ノンブロッキングの選択からio/non-ioの選択へ移しただけだということです。I/Oを行う関数はstd.Ioパラメータを要求し、そのような関数はstd.Ioを受け取る別の関数からしか呼べないので、パラメータがコールチェーン全体に広がっていくという指摘です。
私の読み方はこうです。両方の主張とも正しく、違うことを語っています。色の伝染性は残っています — Ioはコールチェーンを遡っていきます。消えたのは色の硬直性です。RustやJavaScriptでは関数の色がコンパイル時に実行モデルを固定します。ZigでIoを受け取る関数は実行モデルを固定しません — 呼び出し側が後で選び、並行性のない実装を選んでもコードは依然として正しい。そもそもAllocatorがコールチェーンを遡っていくことを、Zig開発者たちはすでに受け入れて生きてきました。色が一つ増えたのではなく、すでにあった色の隣に同じ種類の色がもう一つ付いた、というのが実情です。
ただしこれはタダではありません。ノートの「Lazy Field Analysis」の項目がそのコストの一片を示しています — I/Oをインターフェースにしたあとで見つかった問題で、std.Io.Writerをどんな形であれ使うとstd.Ioのvtableが付いてきて、一部のケースでは不要なコード生成につながりバイナリを膨らませることがあった、というものです。0.16.0は型解決を遅延させることでこれを直しましたが、インターフェースにはもともとvtableの値が付くという事実そのものは残ります。
コンパイラ側 — 増分コンパイルと新ELFリンカ
I/Oが見出しをさらいましたが、コンパイラ側の変化のほうが実務には直接効いてくるかもしれません。
増分コンパイルは今回のサイクルで大きく改善しました。ノートが挙げる代表例が印象的です — Zigコンパイラ自体に増分コンパイルを使うと、以前はコンパイラのほぼ全体を再コンパイルしていた変更が、今ではミリ秒単位で終わるというのです。功績は「Reworked Type Resolution」に帰せられます。型解決を作り直したことで、コンパイラ内部の依存グラフが(依存ループの場合を除き)非循環になり、そのおかげで「過剰分析」— 必要以上に多くのコードを再ビルドすること — がほとんどの場合で消えました。
増分ビルドが非増分ビルドにはない「dependency loop」コンパイルエラーを吐いていた問題も解決しました。ノートはこれが従来のリリースにおける増分と非増分の最大の不一致だったと書いています。LLVMバックエンドも増分コンパイルに対応しましたが、ここには正直な注釈がつきます — これが「LLVM Emit Object」段階を速くすることはない。その段階は完全にLLVMの領分であり、Zigにできることはほとんどないということです。代わりにZig側でLLVMビットコードを作る過程が速くなり、コンパイルエラーがある場合には — エラーがあるとEmit Objectがまるごと省略されるので — LLVMバックエンドでもほぼ即座のフィードバックが得られます。
そしてここにこのリリースで最も正直な一文があります。増分コンパイルには誤コンパイルを含む既知のバグがまだあり、だから0.16.0でもデフォルトはオフです。 ところがノートは次の文で、それでも有効にすることを勧めています。ユーザーが節約される時間にしばしば驚くからだそうです。有効にする方法はzig build -fincremental --watchです。
この緊張関係はどう読むべきでしょうか。私はこう見ます — コンパイルエラーのフィードバックだけに使うなら危険はほぼありません。誤コンパイルは結局のところ間違った機械語を作る問題であり、コンパイルが失敗するコードには機械語がないからです。一方、増分で作ったバイナリをそのままテストしたりデプロイしたりするなら、ノートが警告するその誤コンパイルを正面から食らいかねません。「開発ループではオンに、出て行く成果物はクリーンビルドで」というあたりが、このリリースにおける合理的な線でしょう。
新ELFリンカも入りました。-fnew-linkerで有効化するか、ビルドスクリプトでexe.use_new_linker = trueとするか、-fincrementalとELFターゲットの組み合わせではすでにデフォルトです。ノートが示す性能データポイントは、Zigコンパイラをビルドしたあと、ある関数に1行だけ変更を加え、さらにもう1行変更を加えたシナリオです。
シナリオ: Zigコンパイラをビルド -> 1行変更 -> さらに1行変更
旧リンカ: 14s, 194ms, 191ms
新リンカ: 14s, 65ms, 64ms (66%速い)
リンクを丸ごと省略: 14s, 62ms, 62ms (68%速い)
この表で本当に面白いのは3行目です。新リンカは、リンクをまるごと省略した場合とわずか3msしか差がありません。だからノートは-Dno-binビルドステップを公開する理由がもうあまりないと述べます — コンパイル速度の差が無視できるほど小さいなら、コード生成とリンクを常にオンにしておいたほうがよく、そうすれば最後に実行ファイルまでおまけで手に入るからです。
代償は明らかです。ノートはこの新リンカが旧リンカに対しても、LLDに対しても機能が完全ではないと明記しています。挙げられた例が痛いところを突いています — こうして作られた実行ファイルにはDWARF情報がありません。だから旧リンカとLLDは両方まだ残っており、新リンカが機能的に完全になったら旧リンカを消しLLDを依存関係から外す、というのが計画です。
つまり66%という数字は「デバッグを諦めれば」という条件付きです。あの3msの差が語っているのも同じ話です — リンクがほぼタダになった理由の少なからぬ部分は、リンカがかつてやっていた仕事の一部をまだやっていないからです。
バックエンドの現況 — x86はデフォルト、aarch64は止まった
セルフホストバックエンドの話はリリースのたびに出てきますが、0.16.0の現況は冷静に見る必要があります。
x86バックエンドは今回バグが11件修正され、定数memcpyのコードをよりよく生成します。ノートがLLVMバックエンドと比較して書いている文をそのまま移すと — このバックエンドは動作テストをより多く通過し、コンパイル速度が著しく速く、デバッグ情報が優れており、機械語の品質は劣ります。Debugモードのデフォルトのままです。
この4項目が、なぜDebug専用なのかを正確に説明しています。開発ループではコンパイル速度とデバッグ情報がすべてで、機械語の品質はほとんど関係ありません。リリースビルドではちょうど逆です。参考までにLLVMバックエンドは動作テスト2010件中2004件を通過します(ノートはこれを100%に丸めて書いています)。x86バックエンドはそれよりさらに多く通過します。
aarch64バックエンドは悪いニュースです。ノートの表現そのままに — 依然として作業中であり、今回のリリースサイクルでは「I/O as an Interface」の余波で進行が止まっており、現在動作テストを走らせるとクラッシュします。標準ライブラリの変動が落ち着けば進行が再び付くと期待していると書かれています。
これはこのリリースの機会費用をそのまま示しています。I/Oインターフェースはタダで来たのではなく、aarch64セルフホストバックエンドの1サイクル分を食って来ました。Apple SiliconやARMサーバーでセルフホストバックエンドの高速コンパイルを待っていた人にとって、0.16.0は前進ではなく停止です。WebAssemblyバックエンドは動作テスト1970件中1813件(92%)を通過します。
ツールチェーン側にも痛い項目が一つあります。0.16.0はLLVM 21.1.0に上がりましたが、ループベクトル化をまるごと切りました。 理由は深刻です — その回帰が一般的な設定でコンパイラ自身を誤コンパイルしてしまうからです。特定のCPU機能をオフにする回避策は脆すぎるとされ、バグが直ったLLVMバージョンに上がるまでベクトル化そのものをオフにすることにした、ということです。ノートはこれが一部のケースでコード生成を悪化させることを認めつつ、誤コンパイルよりはましだと述べます。そしてこの性能回帰は0.16.xだけでなく0.17.xにも影響し、0.18.xでようやく解消される見込みだと書かれています。つまり0.16.0に上げると、このペナルティを2リリース分背負うことになります。
明るいニュースもあります。translate-cがlibclangの代わりにaroccベースに変わり、コンパイラのソースツリーからC++が5,940行消えました(3,763行が残ります)。LLVMへのライブラリ依存をClangへのプロセス依存へと変えていく旅の一歩です。スライスを回るforループの安全性チェックはコードをおよそ30%少なく生成するよう改善され、新たに入ったdeflate圧縮はデフォルトの圧縮レベルでzlibより壁時計時間が9.7% ± 0.2%速くなりました(ただしzlibのほうが圧縮率は1.00%良く、命令数はむしろ18.9%多いです — キャッシュミスと分岐予測ミスが減った分、時間で勝った例です)。
アップグレードの代償 — 何が壊れるか
「あらゆる入出力がIoを受け取る」という文は、アップグレードの観点では「入出力を行うすべてのコードが壊れる」と同じ意味です。
まず削除されたものから見ましょう。Io.GenericWriter、Io.AnyWriter、Io.null_writer、Io.CountingReaderが消えました。SegmentedList、meta.declList、Thread.Mutex.Recursiveもなくなりました。フォーマットAPIはfmt.formatがstd.Io.Writer.printに、FormatterがAltに、FormatOptionsがOptionsに、bufPrintZがbufPrintSentinelに変わりました。
エラーセットも整理されました。error.RenameAcrossMountPointsとerror.NotSameFileSystemがerror.CrossDeviceに統合され、error.SharingViolationがerror.FileBusyに、error.EnvironmentVariableNotFoundがerror.EnvironmentVariableMissingに変わりました。DynLibはWindowsサポートが外されましたが、ノートのコメントが面白いです — もうユーザーがLoadLibraryExWとGetProcAddressを直接使う必要があり、どうせみんなすでにそうしていただろう、ということです。
言語自体も動きました。@Typeが個別の型生成ビルトインに置き換えられ、@cImportはビルドシステムへ移行中で(当面ビルトインは残りますがAroが裏で支えます)、実行時ベクトルインデックスが禁止され、packed structとunionでのポインタが禁止されました。「Reworked Type Resolution」は全体として寛容になりましたが、厳密に寛容になったわけではなく、以前は通っていた一部のコードが今は依存ループエラーを出します。
ここでGhosttyの話がまた思い出されます。Hashimotoが0.15で「何かを出力するすべて」が変わったと語ったとき、それは終わりではなく始まりでした。0.16.0はそのwriter系列をまた塗り替えました。彼がこれを歓迎した理由 — Andrew Kelleyが必要だと判断した変更を、後ずさりせず押し通すということ — がここでもそのまま確認できます。そしてその姿勢を歓迎できるかどうかこそが、この言語を使えるかどうかを分ける本当の基準です。
リリースノートには「This Release Contains Bugs」という題の節が別に用意されています。このサイクルでバグレポート345件が閉じられましたが、Zigには既知のバグと誤コンパイルと回帰があると書かれています。そして決定的な一文 — 0.16.xでもZigで些末でないプロジェクトを行うことは、開発プロセスへの参加を要求しうる。 1.0.0に到達すればTier 1サポートにバグポリシーが追加要件として付くだろうとも書かれています。裏を返せば、今はそのポリシーがない、ということです。
今、Zigを使うべきか
まとめると判断基準はこう分かれます。
理にかなう場合
- ツールやインフラのコードを作っていて、ツールチェーンの破壊的変更を吸収する余力がチームにある — アップグレードがイベントではなく日常である組織。
- I/O抽象を引数として注入する設計が実際に必要である — 同じライブラリをCLIではシングルスレッドで、サーバーではスレッドプールで動かす必要がある場合。これは0.16.0が実際に届けたものです。
- クロスコンパイルとC相互運用が中核の要件である。この領域でのZigの強みは0.16.0の騒ぎとは無関係にそのままです。
- コンパイル速度が開発体験のボトルネックであり、
-fincremental --watchの速いエラーフィードバックだけでも価値がある。
まだ早い場合
- 安定したAPIが必要である。0.15がwriterを変え、0.16がI/O全体を変えました。0.17が何を変えるかは誰も約束していません。1.0以前のZigで「今回が最後の大きな変更」だと期待する根拠はありません。
- io_uringの上で動くプロダクションの非同期I/Oが今必要である。0.16.0が与えたのは概念実証です — ネットワーキングもエラー処理もテストカバレッジもまだです。
- aarch64でセルフホストバックエンドの高速コンパイルが必要である。今回のサイクルは進行が止まっており、今は動作テストでクラッシュします。
- リリースビルドの機械語品質が最優先である。セルフホストx86バックエンドは機械語品質が劣るとノート自身が書いており、しかもLLVMのループベクトル化が0.17.xまでオフです。
- チームが「言語のバグに出会ったらアップストリームに参加する」を負担できない。これは私の評価ではなく、リリースノートに書かれた警告です。
おわりに
0.16.0で私が学んだのは、言語についてというより設計についてのことに近いです。
Allocatorを引数として渡すZigの習慣は、もともとメモリの話ではありませんでした。「方針は呼び出し側が決め、ライブラリは仕組みだけを提供する」という話でした。0.16.0はその原則をI/Oに押し広げ、その過程でほとんどのasyncランタイムが曖昧にしてしまう区別 — 非同期でもよいものと、必ず同時でなければならないもの — をio.asyncとio.concurrentに分けました。キャンセルを命令ではなく要求として扱ったことも、スレッドプールでシグナルとEINTRによってキャンセルを実装したことも、同じ肌合いの正直さです。
関数の色問題を「打ち負かした」かどうかは、定義次第だと思います。Ioは依然としてコールチェーンを遡っていくのですから。ただし、遡っていくのが実行モデルではなく選択権であるという点で、これは意味のある違いだと私は思います。そしてリリースノート自身がその勝利を宣言しなかったこと — その一文はコアチームメンバーの個人ブログにあり公式ノートにはありません — も、このチームへの信頼をむしろ高めます。
同時に請求書は実在します。完成した実装は一つで、aarch64は止まり、増分コンパイルは誤コンパイルのためデフォルトでオフで、新リンカはDWARFを作れず、LLVMベクトル化は2リリース分オフで、I/Oに触れるコードは全部書き直さなければなりません。リリースノートはこのどれも隠していません — むしろ私がこの記事に書いたネガティブな事実のほとんどは、そのままノートから持ってきたものです。
それがおそらくこのリリースの最も正確な要約でしょう。Zigは依然として「完成した製品」ではなく「進行中の議論」であり、リリースノートはその議論の議事録です。その議論に参加する気があるなら0.16.0は興味深いリリースです。製品を買いに来たのなら、ノート一番下の「This Release Contains Bugs」の節を先に読んでください。その節がそこにあるのは形式的な免責ではなく、この言語が今どこに立っているかについての正確な陳述です。
参考資料
- Zig 0.16.0 Release Notes — 公式リリースノート(本稿のほぼすべての事実と数字の出典)
- 0.16.0 Released — 公式アナウンス(2026年4月14日、244名のコントリビューター、1183コミット)
- ziglang.org ダウンロードインデックス — バージョン別配布日(0.16.0: 2026-04-13)
- Zig's New Async I/O — Loris Cro、2025年7月13日(「関数の色を完全に打ち負かした」の出典、当時の計画に基づく)
- Zig's new I/O: function coloring is inevitable? — ivnj、2025年7月13日(反対側の論旨)
- ターミナルへ戻った創業者 — HashimotoのGhostty、Zigという賭け、そして「義務のない」オープンソース(関連記事)
현재 단락 (1/129)
Zig 0.16.0が[2026年4月14日に発表されました](https://ziglang.org/news/0.16.0-released/)。公式アナウンスは短く — 8か月の作業、244名の...