返回

文章详情

Zig:数组列表的指针稳定性

Hacker News2026年8月30日 14:41

本页面包含了主分支 Zig 的最近更改的精选列表。本页记录了2026年的条目。其他年份的记录可在开发日志归档页面中找到。2026年8月27日 数组列表的指针稳定性 作者:Robbie Lyman 在2024年,std的哈希映射容器中添加了指针稳定性锁。最初由Leo Emar-Kar在2025年提出的一个拉取请求现在将这种确保内存安全的技术引入了std.ArrayList。为了在代码中使用此功能,请在第一次存储对由ArrayList支持的元素或元素切片的指针时调用lockPointers(),并在不再需要这些指针时调用unlockPointers()。这里有一个有点牵强的示例。假设我们管理两个ArrayList,一个保持了一些输入的内容,而另一个存储了感兴趣的片段;也许是每一行。以下是此过程的一个版本,其中存在一个bug;看看你能否发现它。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, ' '); while (it.next()) |line| { try ctx.lines.append(allocator, line); } } }; 你发现了这个bug吗?问题在于Context.lines.items的元素依赖于Context.history.items的位置,但是如果Context.history需要超过其当前容量的增长,则该位置可能会改变。以下是该bug的重现:test "Context.parse" { const input = "I'm first!\n"; const input_two = \"但这段文本\是足够长的,以至于它\引起了问题!\而问题可能是我们发生了段错误!\遇到这种情况真是不愉快。; 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运行这段代码,我会得到以下输出(还有一些额外信息)。 ====== 预期此输出: ========= I'm first!␃ ======== 而发现的是: ========= UUUUUUUUUU␃ ====================================== 第一个差异发生在第1行:预期:I'm first! ^ ('\x49') 发现:UUUUUUUUUU ^ ('\x55') 1/1 blah.test.Context.parse...FAIL (TestExpectedEqual) 不太好,对吧?这确实告诉我们我们有一个bug,但是根据你调试内存问题的舒适程度(以及你选择的分配器,这将改变bug的表现方式!),在找到修复之前你可能会迷失很长一段时间。由于我们在测试的第一次调用parse之后存储了指针,如果我们进行以下更改会发生什么? 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]); 我们会得到一个panic和一个堆栈跟踪,告诉我们我们关于指针稳定性的假设在哪里被违反了! 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是有序的,这意味着对列表的操作可能会移动元素。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡