Автор: Робби Лайман
Блокировки стабильности указателей были добавлены в контейнеры Hash Map стандартной библиотеки в 2024 году. Pull request, изначально открытый Leo Emar-Kar в 2025 году, теперь распространяет эту технику обеспечения безопасности памяти на std.ArrayList.
Чтобы воспользоваться этим механизмом в своём коде, нужно вызвать lockPointers() при первом сохранении указателя на элемент или срез элементов, находящихся в памяти ArrayList, и вызвать unlockPointers(), когда эти указатели больше не нужны.
Вот немного искусственный пример. Предположим, что нужно управлять двумя Нашли баг? Проблема в том, что элементы Если запустить этот код через Не очень приятно, правда? Такой вывод показывает, что баг есть, но в зависимости от привычки отлаживать проблемы с памятью (и от выбранного аллокатора, от которого зависит, как именно проявится баг) можно надолго застрять, прежде чем найти исправление. Поскольку указатели сохраняются после первого вызова Появляется panic со стектрейсом, показывающий, где именно было нарушено предположение о стабильности указателей! Уже существенная помощь: теперь понятно, что среди вероятных причин падения теста стоит рассматривать проблему безопасности памяти в дополнение к логической ошибке (или вместо неё). Пример, конечно, немного искусственный, но такая структура хранения на базе Напоследок стоит отметить тонкий момент: в отличие от ArrayList
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 потребуется вырасти сверх текущей вместимости. Вот воспроизведение бага: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!
^ ('\x49')
found:
UUUUUUUUUU
^ ('\x55')
1/1 blah.test.Context.parse...FAIL (TestExpectedEqual)
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]);
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), может не указывать на последние n элементов списка, если позже вызвать orderedRemove() или pop(). По этой причине, хотя orderedRemove() и pop() никогда не аллоцируют память, они будут вызывать ту же ассерцию после вызова lockPointers().