Блог*
#prog #rust #rustlib #amazingopensource Dtolnay продолжает доставлять https://github.com/dtolnay/itoa https://github.com/dtolnay/dtoa
Кажется, надо #github переименовать в #amazingopensource. Точнее изначальный смысл, который я вкладывал, отражает
Forwarded from The After Times
Мне нужна бронзовая статуя русалки!
Linux: «Вот вам бронзовый куб 1x1x1 м и напильник»
Gentoo: «Вот вам медь, олово, доменная печь»
FreeBSD: «Вот вам кирка и каска с фонариком. Шахта с медью — в той стороне»
Ubuntu: «Вот вам статуя русалки»
Пользователь: «Но здесь же только хвост!»
Ubuntu: «Введите, пожалуйста apt-get install туловище русалки, apt-get install голова русалки, apt-get install руки русалки...»
MacOS: «Вот вам красивые голые парни»
Пользователь: «Ух ты! А можно потрогать?»
MacOS: «Заплатите 200 евро и активируйте функцию „Потрогать голых парней“»
Windows: «Нет. Я думаю — Вам определенно нужна чугунная статуя Чапаева...»
Linux: «Вот вам бронзовый куб 1x1x1 м и напильник»
Gentoo: «Вот вам медь, олово, доменная печь»
FreeBSD: «Вот вам кирка и каска с фонариком. Шахта с медью — в той стороне»
Ubuntu: «Вот вам статуя русалки»
Пользователь: «Но здесь же только хвост!»
Ubuntu: «Введите, пожалуйста apt-get install туловище русалки, apt-get install голова русалки, apt-get install руки русалки...»
MacOS: «Вот вам красивые голые парни»
Пользователь: «Ух ты! А можно потрогать?»
MacOS: «Заплатите 200 евро и активируйте функцию „Потрогать голых парней“»
Windows: «Нет. Я думаю — Вам определенно нужна чугунная статуя Чапаева...»
#prog #rust #моё
Во время решения одной из задач Advent of Code мне пришлось решать довольно заковыристую в плане владения задачу: требовалось связать цепочку интерпретаторов так, чтобы данные, выдаваемые одним экземпляром интерпретатора, использовались в качестве входных данных следующего интерпретатора.
Дополнительной сложности придал тот факт, что цепочка замкнута в петлю, поэтому просто пройтись по цепочке один раз было недостаточно. Здесь напрашиваются сопрограммы, но их в Rust нет, поэтому мне пришлось сделать дек очередей в качестве буферов для данных. В итоге в моём решении были строки вида:
Мало того, что это выглядит уродливо и многословно, так ещё и
Что же мне нужно было на самом деле? Кольцевой буфер очередей, которой обеспечивают мутабельный доступ к парам идущих подряд элементов.
В тот раз я пошёл по пути наименьшего распределения и оставил тот код, который я показал выше, но потом у меня возникла мысль, что нужного мне поведения можно было бы достичь через обёртку, которая предоставляла бы нужный мне доступ через unsafe-преобразования. Такую я в итоге и написал: https://gist.github.com/AnthonyMikh/bb41179b20938ba329c3d411ddc1c679. Вот как она выглядит:
Здесь хранится только один из индексов пары, потому что второй всегда можно подсчитать, зная его и длину
Здесь
Считаю ли я итоговый код завершённым? Почти: я бы хотел хранить impl BorrowMut<VecDeque<T>>, чтобы MutPair можно было использовать тогда, когда невозможно отдать владение деком.
Во время решения одной из задач Advent of Code мне пришлось решать довольно заковыристую в плане владения задачу: требовалось связать цепочку интерпретаторов так, чтобы данные, выдаваемые одним экземпляром интерпретатора, использовались в качестве входных данных следующего интерпретатора.
Дополнительной сложности придал тот факт, что цепочка замкнута в петлю, поэтому просто пройтись по цепочке один раз было недостаточно. Здесь напрашиваются сопрограммы, но их в Rust нет, поэтому мне пришлось сделать дек очередей в качестве буферов для данных. В итоге в моём решении были строки вида:
buffers.push_back(input_buffer);
input_buffer = output_buffer;
output_buffer = buffers.pop_front().unwrap();
Мало того, что это выглядит уродливо и многословно, так ещё и
unwrap
здесь совершенно лишний: после того, как в дек положили элемент, в нём обязательно есть как минимум один элемент, поэтому pop_front
заведомо вернёт Some(_)
.Что же мне нужно было на самом деле? Кольцевой буфер очередей, которой обеспечивают мутабельный доступ к парам идущих подряд элементов.
std::collections::VecDeque
обеспечивает доступ по индексам, но моей проблемы это бы не решает, так как таким образом можно получить только доступ к отдельным элементам, но не к, скажем, внутреннему слайсу, а взять две мутабельные ссылки одновременно даже по двум заведомо разным индексам мне не дал бы borrow checker.В тот раз я пошёл по пути наименьшего распределения и оставил тот код, который я показал выше, но потом у меня возникла мысль, что нужного мне поведения можно было бы достичь через обёртку, которая предоставляла бы нужный мне доступ через unsafe-преобразования. Такую я в итоге и написал: https://gist.github.com/AnthonyMikh/bb41179b20938ba329c3d411ddc1c679. Вот как она выглядит:
struct MutPair<T> {
inner: VecDeque<T>,
first: usize,
}
Здесь хранится только один из индексов пары, потому что второй всегда можно подсчитать, зная его и длину
inner
. Основной метод этого типа - get
, который возвращает две мутабельные ссылки, на элемент с текущим индексом и следующим за ним:
pub fn get(&mut self) -> (&mut T, &mut T) {
let first = self.first;
let second = self.incremented();
let (head, tail, head_len) = {
let (head, tail) = self.inner.as_mut_slices();
let len = head.len();
(head.as_mut_ptr(), tail.as_mut_ptr(), len)
};
let first = unsafe { &mut *get_mut(head, tail, head_len, first) };
let second = unsafe { &mut *get_mut(head, tail, head_len, second) };
(first, second)
}
Здесь
as_mut_slices
возвращает мутабельные ссылки на содержимое дека (слайсов два, потому что логически единая последовательность элементов может начинаться перед концом внутреннего вектора и продолжаться от его конца). Borrow checker не даёт нам в лоб взять две мутабельные ссылки, поэтому мы используем стандартный трюк: кастуем ссылки в сырые указатели, которые компилятором уже не отслеживаются, сдвигаем их и преобразуем обратно в ссылки. Вспомогательная функция get_mut
возвращает нужный нам элемента по индексу, учитывая, что последовательность по факту состоит из двух слайсов.Считаю ли я итоговый код завершённым? Почти: я бы хотел хранить impl BorrowMut<VecDeque<T>>, чтобы MutPair можно было использовать тогда, когда невозможно отдать владение деком.
Gist
Mutable view for VecDeque
Mutable view for VecDeque. GitHub Gist: instantly share code, notes, and snippets.
Что же делает эту функцию корректной (sound)? Так как компилятор нам тут не помогает, ответственность за ненарушение правил языка ложится на нас.
1. Вычисление индексов гарантирует, что нужный нам элемент действительно лежит в деке (собственно,
2.
3. Когда берётся
1. Вычисление индексов гарантирует, что нужный нам элемент действительно лежит в деке (собственно,
incremented
фактически возвращает (self.first + 1) % self.inner.len()
, только более работает немного более эффективно).2.
MutPair::new
проверяет, что дек содержит как минимум два элемента, поэтому ссылки, возвращаемые get
, гарантированно не алиасятся.3. Когда берётся
&mut
от значения, полученного путём разыменовывания сырого указателя, полученная ссылка имеет lifetime, на который не накладывается никаких ограничений. Фактический lifetime приписывается исходя из вывода типов. Я не писал явных lifetime-ов, поэтому за счёт lifetime elision полученные ссылки имеют тот же lifetime, что и self
. Borrow checker гарантирует нам, что, пока возвращённые ссылки используются, get
нельзя вызвать снова, поэтому размножить мутабельные ссылки и заалиасить их не получится.А теперь ложка дёгтя:
Корректность
Также нам приходится следить за тем, чтобы мы меняли элементы дека, но не сам дек как таковой. В идеале, стоило бы хранить соответствующий адаптер над деком, но такого адаптера (пока?) нет.
Корректность
get
довольно существенным образом зависит от проверки в new
, то есть от кода, напрямую не связанного с get
. В данном случае уследить за условиями было несложно, но в каком-то более сложно устроенном коде это было бы не так просто заметить. Отличная иллюстрация того, что unsafe может протекать.Также нам приходится следить за тем, чтобы мы меняли элементы дека, но не сам дек как таковой. В идеале, стоило бы хранить соответствующий адаптер над деком, но такого адаптера (пока?) нет.
Несмотря на то, что компилятор не следит за сырыми указателями, есть интерпретатор кода miri, который позволяет проверять корректность кода динамически. Мой первый вариант кода использовал
std::mem::transmute
для того, чтобы принудительно перезаписать lifetime, а ссылки брались через get_unchecked_mut
. Этот вариант давал ошибку в miri. Я не был уверен, кто был в данном случае неправ, поэтому задал соответствующий вопрос в официальном Rust чате в Discord. Очень хороший человек @udoprog потратил время на то, чтобы разобраться в моём коде и указать мне на то, что я действительно провоцирую подобным образом UB (так что miri таки был прав), и предложил использовать сырые указатели. Спасибо, @udoprog