HN 日本語サマリー

← 一覧へ戻る
プログラミング

Zig: ArrayListにおけるポインタの安定性

Zig: Pointer Stability for ArrayLists (ziglang.org)

42 pointsby tosh10 コメント

要約

Zig言語の標準ライブラリであるArrayListに、ポインタの安定性を保証する機能が追加されました。`lockPointers()`と`unlockPointers()`を使用することで、ArrayListの要素へのポインタがメモリ再配置によって無効になることを防ぎ、メモリ安全性を向上させることができます。この機能は、特に動的にサイズが変更されるリスト内の要素への参照を扱う場合に役立ちます。

全文翻訳

Devlog このページには、mainブランチのZigにおける最近の変更点がまとめられています。RSSフィードとしても利用可能です。このページには2026年のエントリが含まれています。他の年はDevlogアーカイブページで利用可能です。 2026年8月27日 ArrayListにおけるポインタの安定性 著者: Robbie Lyman ポインタの安定性ロックは、2024年にstdのHash Mapコンテナに追加されました。2025年にLeo Emar-Karによって最初に開かれたプルリクエストが、このメモリ安全性を確保する技術をstd.ArrayListにもたらしました。 これをコードで使用するには、ArrayListによってバックアップされた要素または要素のスライスへのポインタを最初に格納する際に`lockPointers()`を呼び出し、それらのポインタが不要になった際に`unlockPointers()`を呼び出します。 ここでは、やや作り話のような例を示します。2つのArrayListを管理していると仮定しましょう。一方は入力の内容をメモリに保持し、もう一方は興味のあるチャンク(例えば各行)を格納しています。ここではバグのあるこのプロセスのバージョンを示します。見つけられますか。 ```zig const std = @import("std"); const Context = struct { history: std.ArrayList(u8), lines: std.ArrayList([]const u8), fn parse(ctx: *Context, allocator: std.mem.Allocator, input: []const u8) !void { const slice = try ctx.history.addManyAsSlice(allocator, input.len); @memcpy(slice, input); var it = std.mem.tokenizeScalar(u8, slice, '\n'); while (it.next()) |line| { try ctx.lines.append(allocator, line); } } }; ``` バグを見つけましたか?問題は、Context.lines.itemsの要素がContext.history.itemsの場所(ロケーション)に依存していることですが、Context.historyが現在の容量を超えて成長する必要がある場合、この場所は変更される可能性があります。 ここでは、バグの再現コードを示します。 ```zig test "Context.parse" { const input = "I'm first!\n"; const input_two = \ "But this text \"is juuuuuuuuuuuuuuuuuuuuuuuuust long enough that it \"causes a problem! \"And the problem could be that we segfault! \"Which is no fun to run into. "; var ctx: Context = .{ .history = .empty, .lines = .empty }; const gpa = std.testing.allocator; defer ctx.history.deinit(gpa); defer ctx.lines.deinit(gpa); try ctx.parse(gpa, input); try ctx.parse(gpa, input_two); try std.testing.expectEqualStrings("I'm first!", ctx.lines.items[0]); } ``` zig testでこのコードを実行すると、次のような出力(その他少し)が得られます。 ``` ======== expected this output: ========= I'm first! ======== instead found this: ========= UUUUUUUUUU ====================================== First difference occurs on line 1: expected: I'm first! ^ ('I') found: UUUUUUUUUU ^ ('U') 1/1 blah.test.Context.parse...FAIL (TestExpectedEqual) ``` 良くないですよね?これはバグがあることを示していますが、メモリ問題のデバッグにどれだけ慣れているか(そしてアロケータの選択によってバグの現れ方が変わる可能性があります!)によっては、修正を見つけるまでにかなりの時間迷ってしまうかもしれません。 テストで最初のparse呼び出し後にポインタを格納したので、この変更を加えるとどうなるでしょうか? ```zig try ctx.parse(gpa, input); ctx.history.lockPointers(); defer ctx.history.unlockPointers(); try ctx.parse(gpa, input_two); try std.testing.expectEqualStrings("I'm first!", ctx.lines.items[0]); ``` スタックトレースが表示され、ポインタの安定性に関する仮定がどこで侵害されたかを示すパニックが発生します! ``` thread 3023222 panic: reached unreachable code /Users/robbie/bin/lib/std/debug.zig:442:14: 0x102d2506f in assert (test) if (!ok) unreachable; // assertion failure ^ /Users/robbie/bin/lib/std/debug.zig:1880:15: 0x102d31ef7 in assertUnlocked (test) assert(l.state == .unlocked); ^ /Users/robbie/bin/lib/std/array_list.zig:1348:50: 0x102e3ced7 in ensureTotalCapacityPrecise (test) self.pointer_stability.assertUnlocked(); ^ /Users/robbie/bin/lib/std/array_list.zig:1341:51: 0x102e3cdff in ensureTotalCapacity (test) return self.ensureTotalCapacityPrecise(gpa, growCapacity(new_capacity)); ^ /Users/robbie/bin/lib/std/array_list.zig:1237:41: 0x102e4e5c3 in resize (test) try self.ensureTotalCapacity(gpa, new_len); ^ /Users/robbie/bin/lib/std/array_list.zig:1461:28: 0x102e4e40f in addManyAsSlice (test) try self.resize(gpa, try addOrOom(self.items.len, n)); ^ /Users/robbie/src/advent-of-code/2024/blah.zig:8:51: 0x102e4dc1f in parse (test) const ptr = try ctx.history.addManyAsSlice(allocator, input.len); ^ /Users/robbie/src/advent-of-code/2024/blah.zig:35:18: 0x102e4e167 in test.Context.parse (test) try ctx.parse(gpa, input_two); ``` 素晴らしい、これはすでに大きな助けになります。これで、ロジックの問題だけでなく、メモリ安全性の問題もテストの失敗の可能性のある原因として考慮すべきであることがわかります。もちろん、この例はやや作り話でしたが、実際のコードでこのようなバックアップストレージとしてstd.ArrayListを使用することがよくありますので、あなた自身も実際のユースケースを見つけられることを願っています。 最後に、微妙な点に注意してください。HashMapとその仲間とは異なり、ArrayListは順序付けられているため、リストのバックアップメモリ全体を移動、リサイズ、または解放することなく、リストの要素を移動させる操作が発生する可能性があります。例えば、`addManyAsSlice(gpa, n)`が返すポインタ(実際にはスライス)は、`orderedRemove()`や`pop()`を呼び出した場合、リストの最後のn個の要素を指さない可能性があります。このため、`orderedRemove()`と`pop()`はメモリを割り当てることはありませんが、`lockPointers()`の呼び出し後には上記と同じアサーションが発生します。 2026年6月30日 パッケージ管理機能のすべてがコンパイラからビルドシステムへ移動 著者: Andrew Kelley ユーザーのbuild.zigスクリプトとビルドシステム自体のための別個のプロセスができた今、パッケージ管理ロジックが存在する場所としてそれが理にかなっています。 これらのサブコマンドをmakerプロセスに移動しました: zig build zig fetch zig init zig libc これにより、コンパイラ実行可能ファイルに含まれていたものの大部分が、ソース形式で出荷されるようになりました。これには以下が含まれます: パッケージ取得ロジック HTTPクライアントとネットワーキング TLS(トランスポート層セキュリティ)および関連する暗号化 Gitプロトコル xz、gzip、zstd、flate、zip build.zig.zonファイルの解析、検証、その他の処理 その結果、この機能はコンパイラを再ビルドせずにパッチできるようになり、ユーザーや貢献者がいじりやすくなりました。 さらに、zigでのパッケージ管理は、maker実行可能ファイルがReleaseSafeモードでコンパイルされているため、ネットワーキング時に安全チェックが有効になったことを意味します。さらに、ネットワーキングやファイルハッシュに使用されるすべての暗号化は、ソフトウェア配布時に通常依存するにはまれすぎるCPU命令であっても、ホストで利用可能な特別なCPU命令を利用できるようになりました。AOTのケーキを食べ、JITも食べることができます! これを行った私の当初の動機は、maker/configurerプロセス分離が--build-runnerフラグに破壊的な変更を加えた後にZLSのブロックを解除するために、ビルドサーバープロトコルを公開することに関連していました。 当初、プロセスツリーは次のようでした。 ``` zig build (zigコンパイラ + パッケージマネージャー) └─ builder (ユーザーのbuild.zigロジック + ビルドシステム実装) ``` プロセス分離の変更により、次のようになりました。 ``` zig build (zigコンパイラ + パッケージマネージャー) ├─ configurer (ユーザーのbuild.zigロジック) └─ maker (ビルドシステム) ``` この時点で、長期間実行されるzig build --watchプロセスを考えてみてください。ファイルを監視し、ソースコードの変更時に再ビルドします。build.zigの変更が検出された場合、またはそのロジックの実行中に観察されたファイルが検出された場合、configurerを再実行する必要があることを意味します。これは、zig buildがパッケージ管理ロジックを繰り返す機会を与えるためにmakerプロセスが終了する必要があることを意味します。 さて、このdevlogエントリで説明された変更の後、次のようになります。 ``` zig build (zigコンパイラ) └─ maker (ビルドシステム + パッケージマネージャー) └─ configurer (ユーザーのbuild.zigロジック) ``` したがって、設定を再実行する必要がある場合、makerプロセスは親プロセスであるため、兄弟プロセスではなく、存続できます。今後のビルドサーバーに関しては、サーバーが終了してクライアントが再接続しなければならないという厄介な状況を回避できることを意味します。クライアントに設定変更を通知するだけです。 これは、ほとんど完全に非破壊的な変更です。