1.83K subscribers
3.29K photos
130 videos
15 files
3.56K links
Блог со звёздочкой.

Много репостов, немножко программирования.

Небольшое прикольное комьюнити: @decltype_chat_ptr_t
Автор: @insert_reference_here
Download Telegram
#prog #rust #моё

Опытные Rust-программисты знают, что в Rust можно сделать мапу, которая индексируется не рантайм-ключами, а типами. Сделать это довольно просто поверх обычной мапы: достаточно хранить значения в виде Box<dyn Any>, а в качестве ключей использовать TypeId (хотя и есть немного неочевидный нюанс). В геттере мы конструируем TypeId от переданного типа, индексируем им мапу, и если значение есть, то приводим к нужному типа через downcast_ref/downcast_mut.

#[derive(Default)]
struct TypeMap(HashMap<TypeId, Box<dyn Any>>);

impl TypeMap {
fn insert<T: 'static>(&mut self, val: T) {
self.0.insert(TypeId::of::<T>(), Box::new(val));
}

fn get<T: 'static>(&self) -> Option<&T> {
self.0.get(&TypeId::of::<T>())?.downcast_ref::<T>()
}
}

fn main() {
let mut map = TypeMap::default();
map.insert(42_i32);
assert_eq!(map.get::<i32>(), Some(&42));
}


С дополнительными обёртками можно делать мапы с отображениями из T в тип, параметризованный T:

T -> Option<T>
T
-> [T; 2]
T
-> Vec<T>

В общем, принцип понятен. Но что делать, если мы хотим хранить в параметризованной типами мапе коллбеки, связанные с типом?

Пусть для определённости мы хотим хранить коллбеки вида FnMut(&mut T). Как я уже говорил, в Rust нет общего типа для замыканий одной сигнатуры — есть лишь общий трейт. Для того, чтобы сохранить замыкание в TypeMap, нам потребуется каким-то образом стереть тип дважды: сначала от конкретного типа замыкания до dyn FnMut(&mut T), а потом от него до dyn Any.

Наиболее простой способ сделать это — забоксить замыкание дважды:

// определение `TypeMap` выше

#[derive(Default)]
struct CallbackMap(TypeMap);

impl CallbackMap {
fn insert<T: 'static>(&mut self, f: impl FnMut(&mut T) + 'static) {
let boxed = Box::new(f) as Box<dyn FnMut(&mut T) + 'static>;
self.0.insert(boxed);
}

fn get_by_arg<T: 'static>(&mut self) -> Option<&mut (dyn FnMut(&mut T) + 'static)> {
self.0.get_mut::<Box<dyn FnMut(&mut T) + 'static>>().map(|f| &mut **f)
}
}

fn main() {
let mut map = CallbackMap::default();
let multiplier = 7;
let f = move |x: &mut i32| *x *= multiplier;
map.insert(f);
let mut x = 6;
map.get_by_arg().unwrap()(&mut x);
assert_eq!(x, 42);
}
👍72🤔2
Решение выше полностью безопасно, но неудовлетворительно ввиду двойной аллокации и двойной индирекции (даже тройной, считая саму мапу). Можно ли сделать лучше? Можно... Но для этого нам придётся задействовать фичи nightly, а именно — ptr_metadata. Почему? Потому что — забегая вперёд — нам потребуется стирать типы у жирных толстых указателей на трейт-объекты, и без этого API это невозможно сделать, не закладываясь на детали реализации текущей версии.

Что можно сказать наверняка — так это то, что один раз боксить всё-таки придётся. Вне зависимости от того, что мы сконструируем, это что-то должно будет хранить !Sized значения. И это что-то само по себе должно быть Sized, чтобы его можно было положить в мапу. Таким образом, нам потребуется что-то, что будет хранить указатели. Мы, очевидно, не можем хранить просто Box<dyn FnMut(&mut T)>, поскольку это разные типы для разных T, а для хранения в мапе тип нужен одинаковый.

Немного подумаем о том, что из себя представляет указатель на трейт-объект. Так как это — толстый указатель, он состоит из указателя на данные и... DynMetadata. DynMetadata — это особый тип, который хранит в себе указатель на таблицу. Если DynMetadata инстанцирован неким dyn-типом, то таблица содержит в себе размер и выравнивание реального значения, его деструктор и методы соответствующего трейта. Методы трейт-объекта являются тонкой обёрткой, которые просто загружают нужный метод из этой таблицы и вызывают его на указателе данных. Что приятно — DynMetadata имеет одно и то же представление вне зависимости от подставленного типа... И да, именно на это мы и будем полагаться (еееее, больше ансейфа!).

Казалось бы, дело в шляпе: стираем типы у указателей на данные и у vtable, а при запросе кастим в нужный тип:

type ErasedVtable = DynMetadata<()>;

struct ErasedDyn {
data: *mut (),
vtable: ErasedVtable,
}

impl ErasedDyn {
fn new<T, F>(f: F) -> Self
where
T: 'static,
F: FnMut(&mut T) + 'static,
{
let boxed = Box::new(f) as Box<dyn FnMut(&mut T)>;
let (data, vtable) = Box::into_raw(boxed).to_raw_parts();
Self {
data,
vtable: unsafe { std::mem::transmute(vtable) },
}
}

unsafe fn downcast_mut_unchecked<T: 'static>(&mut self) -> &mut dyn FnMut(&mut T) {
let data_ptr = self.data;
let vtable = std::mem::transmute(self.vtable);
&mut *std::ptr::from_raw_parts_mut(data_ptr, vtable)
}
}

#[derive(Default)]
struct CallbackMap {
map: HashMap<TypeId, ErasedDyn>,
}

impl CallbackMap {
fn insert<T: 'static>(&mut self, f: impl FnMut(&mut T) + 'static) {
self.map.insert(TypeId::of::<T>(), ErasedDyn::new(f));
}

fn get<T: 'static>(&mut self) -> Option<&mut dyn FnMut(&mut T)> {
self.map
.get_mut(&TypeId::of::<T>())
.map(|cb| unsafe { cb.downcast_mut_unchecked() })
}
}

Из-за стирания типов мы не знаем, какой реально тип мы храним, и должны при даункасте спрашивать его у окружения, но, по крайней мере, этот код работает.

Есть только одна небольшая проблема.

Мы не можем нормально дропать ErasedDyn.

И действительно, если мы скопируем код с main выше и запустим его, то он будет работать, но если запустить его с miri, то он услужливо подскажет, что у нас утекла память.

Кажется, мы зашли в тупик. Мы можем легко получить TypeId по T, но не наоборот. Возможно, можно было бы хранить деструктор вместе с ErasedDyn, но если мы добавим возможность удалять коллбеки из мапы, то у нас не выйдет автоматически дропать его вне мапы. Можно добавить деструктор и в сам ErasedDyn, но тогда он будет занимать размер трёх usize против двух usize у нативных толстых указателей. Видимо, выход один: хранить деструктор в памяти, на который указывает ErasedDyn.
👍32
Но сначала немного о том, как это деструктор должен выглядеть. С одной стороны, в нём должен быть код для дропа конкретного типа. С другой стороны, он должен принимать на вход стёртые аргументы. Кажущеюся противоречие разрешается за счёт двух вещей: лямбды без захватов могут быть приведены к функциональным указателям и захват типа не ведёт к захвату значения:

type Destructor = unsafe fn(*mut (), ErasedVtable);

fn make_drop_fn<T>() -> Destructor {
|data, vtable| unsafe {
let dyn_ptr: *mut ??? =
std::ptr::from_raw_parts_mut(data, std::mem::transmute(vtable));
drop(Box::from_raw(dyn_ptr))
}
}

Возникает небольшой вопрос, что подставить вместо ???. Это не может быть dyn FnMut(&mut T), так как нам ещё нужно как-то добавить деструктор. Что ж, сделаем под это тип:

#[repr(C)]
struct WithDrop<T: ?Sized> {
destructor: Destructor,
data: T,
}

В make_drop_fn вместо ??? подставим WithDrop<dyn FnMut(&mut T)>. Очевидно, конструктор ErasedDyn придётся немного поменять:

impl ErasedDyn {
fn new<T, F>(f: F) -> Self
where
T: 'static,
F: FnMut(&mut T) + 'static,
{
let with_drop = WithDrop {
destructor: make_drop_fn::<T>(),
data: f,
};
let boxed = Box::new(with_drop) as Box<WithDrop<dyn FnMut(&mut T)>>;
let (data, vtable) = Box::into_raw(boxed).to_raw_parts();
Self {
data,
vtable: unsafe { std::mem::transmute(vtable) },
}
}
}

Ну и, раз уж мы сделали деструктор, то, очевидно, надо сделать надлежащую реализацию Drop:

impl Drop for ErasedDyn {
fn drop(&mut self) {
unsafe {
let destructor = self.data.cast::<Destructor>().read();
destructor(self.data, self.vtable);
}
}
}

Каст из data в указатель на Destructor валиден за счёт того, что WithDrop имеет фиксированную раскладку в памяти — #[repr(C)] — и адрес первого поля совпадает с адресом структуры в целом. Окей, с дропом мы разобрались, но теперь нам нужно поменять метод для даункаста. Теперь нам надо каким-то образом получить из указателя на WithDrop<dyn FnMut(&mut T)> указатель на data. Кажется, достаточно просто сместить указатель на размер Destructor?..

impl ErasedDyn {
unsafe fn downcast_mut_unchecked<T: 'static>(&mut self) -> &mut dyn FnMut(&mut T) {
let data_ptr = self
.data
.byte_offset(std::mem::size_of::<Destructor>() as isize);
let vtable = std::mem::transmute(self.vtable);
&mut *std::ptr::from_raw_parts_mut(data_ptr, vtable)
}
}

А вот и нет.

Именно, этот unsound код полагается на то, что data располагается в памяти сразу после destructor, без промежутков. И это абсолютно необоснованное предположение! Именно, unsized coercion добавляет vtable к указателям на данные, но, разумеется, никак не трогает данные за указателем. Раскладка в памяти данных за указателем зависит от исходного типа, из которого произошло unsized coercion — и конкретно в случае WithData тип поля data вполне мог иметь выравнивание большее, чем у Destructor! Как следствие, между destructor и data может быть паддинг, и, как несложно понять, подсчитать его статически после стирания типов нельзя.

Покажу наглядно, как ломается текущий подход:

fn main() {
#[repr(align(16))]
struct OverAligned(i32);

let mut map = CallbackMap::default();
let multiplier = OverAligned(7);
// захватываем multiplier целиком, а не только поле
let f = move |x: &mut i32| *x *= { &multiplier }.0;
map.insert(f);
let mut x = 6;
map.get().unwrap()(&mut x);
assert_eq!(x, 42);
}

Неправильная реализация downcast_mut выдаёт указатель куда-то в паддинг, в неинициализированные данные. Попытка их прочитать приводит к неопределённому поведению. На практике это означает, что ассерт у вас почти наверняка выстрелит (а в дебажной сборке — может и свалиться с паникой из-за переполнения при умножении). Если запустить этот пример в miri, то он выдаёт ошибку на момент создания ссылки — некорректное выравнивание.
3👍3
Так как же всё починить? Очевидно, нам нужно хранить не только деструктор, но и смещение до данных. Поменяем WithDrop на WithHeader:

#[derive(Clone, Copy)]
struct Header {
data_offset: isize,
destructor: Destructor,
}

#[repr(C)]
struct WithHeader<T: ?Sized> {
header: Header,
data: T,
}

В конструкторе ErasedDyn нужно также дополнительно высчитывать смещение до собственно данных:

impl ErasedDyn {
fn new<T, F>(f: F) -> Self
where
F: FnMut(&mut T) + 'static,
{
let mut with_hdr = WithHeader {
header: Header {
data_offset: 0,
destructor: make_drop_fn::<T>(),
},
data: f,
};
unsafe {
with_hdr.header.data_offset =
<*const _>::byte_offset_from(&with_hdr.data, &with_hdr);
}
let boxed = Box::new(with_hdr) as Box<WithHeader<dyn FnMut(&mut T)>>;
let (data, vtable) = Box::into_raw(boxed).to_raw_parts();
Self {
data,
vtable: unsafe { std::mem::transmute(vtable) },
}
}
}

Деструктор почти не поменялся, только вместо вычитывания деструктора напрямую по указателю указатель сначала кастуется в Header и из него уже достаётся destructor. Немного сложнее даункаст:

impl ErasedDyn {
unsafe fn downcast_mut_unchecked<T>(&mut self) -> &mut dyn FnMut(&mut T) {
let data_offset = (*self.data.cast::<Header>()).data_offset;
let data_ptr = self.data.byte_offset(data_offset);
let vtable = std::mem::transmute(self.vtable);
&mut *std::ptr::from_raw_parts_mut(data_ptr, vtable)
}
}

И вот это уж корректно работает. miri не жалуется даже на это:

fn main() {
#[repr(align(128))]
struct A(String);

impl A {
fn get(&self) -> &str {
&self.0
}
}

let mut a = A(String::from("a"));
let f = move |x: &mut i32| {
*x += a.get().len() as i32;
a.0 += "a";
};
let mut map = CallbackMap::default();
map.insert(f);
let mut x = 4;
map.get().unwrap()(&mut x);
assert_eq!(x, 5);
map.get().unwrap()(&mut x);
assert_eq!(x, 7);
}


===============

БОНУС: если мы хотим обойтись одной аллокацией и не хотим быть менее эффективным, чем нативные толстые указатели, но готовы немного поступиться типобезопасностью, то хранить коллбеки со стиранием типа можно и сильно проще:

#[derive(Default)]
struct ErasedCallbackMap(HashMap<TypeId, Box<dyn FnMut(&mut dyn Any)>>);

fn wrap<T: 'static>(mut f: impl FnMut(&mut T) + 'static) -> Box<dyn FnMut(&mut dyn Any)> {
Box::new(move |arg| f(arg.downcast_mut().unwrap()))
}

impl ErasedCallbackMap {
fn insert<T: 'static>(&mut self, f: impl FnMut(&mut T) + 'static) {
self.0.insert(TypeId::of::<T>(), wrap(f));
}

fn get<T: 'static>(&mut self) -> Option<&mut (dyn FnMut(&mut dyn Any) + 'static)> {
self.0.get_mut(&TypeId::of::<T>()).map(|f| &mut **f)
}
}

fn main() {
let mut map = ErasedCallbackMap::default();
let multiplier = 7;
let f = move |x: &mut i32| *x *= multiplier;
map.insert(f);
let mut x = 6;
// аргумент приводится к &mut dyn Any
map.get::<i32>().unwrap()(&mut x);
assert_eq!(x, 42);
}
🔥8👍21
#politota

Тык

В рамках реализации полномочий Минюста России в Верховный Суд Российской Федерации подано административное исковое заявление о признании Международного общественного движения ЛГБТ экстремистским и о запрете его деятельности на территории Российской Федерации.
🤡17❤‍🔥3👍2😁2🤬21
МЕЖДУНАРОДНОЕ ОБЩЕСТВЕННОЕ ДВИЖЕНИЕ ЛГБТ
🤡15🔥3😍1
Forwarded from Rotten Kepken
Минюст направил в суд иск о признании международного общественного движения ЛГБТ экстремистской организацией и запрете ее в РФ — ведомство.

(Бриф)

А когда уже в России запретят международную общественную организацию «Люди»?
😁13🤡4
Сегодня одному из моих подписчиков исполнилось 18 лет.

С днём рождения, папищек.
12🎉7🥰2😱2👎1
Как указано в исковом заявлении, если в аббревиатуре ЛГБТ передвинуть по одной букве, получится слово КВАС. Поэтому движение угрожает основам культуры России.
😁8👍7🤡2
За полгода в Армении я так и не натурализовался
🤡18🍌7😁5❤‍🔥4
Forwarded from Код, коты и карандаш
13😁4🤡2
Я тупо умный
🤡15💯7
#game

В Steam сейчас раздача слонов первой Half-Life:
https://t.iss.one/welovegames/18558
7🔥1😁1
Дело о запрете "международного общественного движения ЛГБТ" будет слушаться в закрытом судебном заседании. Значит, кому именно и какие претензии предъявляются мы не узнаем
😁13🤡21🤬1
Валидация на кассовом чеке
6
Forwarded from Neural Machine
Это не очень хороший год, но следующий будет хуже
😭14👎9😱5❤‍🔥4😍1
#prog #rust #rustreleasenotes

Вышла версия Rust 1.74.0! В этот раз довольно минорный релиз, изменения в основном в тулинге. Как всегда, полный ченджлог отдельно, а тут лишь выдержки.

▪️Компилятор теперь позволяет использовать в непрозрачных возвращаемых типах проекции из Self, в которые входят лайфтаймы не из сигнатуры функции. На практике это означает, что функции с impl Trait и async-функции, в возвращаемом типе которых есть Self, теперь работают всегда, а не наталкиваются на произвольные ограничения компилятора. Подробнее вместе с примерами кода, который не компилировался раньше и стал приниматься сейчас, смотри в соответствующем PR.

▪️Ранее замыкания, которые захватывали по ссылке поля #[repr(packed)] структур, захватывали их по разному в зависимости от того, являлась ли поле корректно выровненным или нет. Из-за этого смена типа поля в packed структуре — даже не того, что было захвачено! — могло привести к смене раскладке замыкания и, как следствие, изменению поведения из-за смены порядка дропа полей. В этой версии компилятора решили избавиться от столь странного поведения: теперь поля packed структур захватываются по ссылке одинаково вне зависимости от того, насколько выровнены поля.

▪️Насчёт repr: теперь можно явно писать #[repr(Rust)]

▪️Поменяли линты о приватных определениях внутри публичных определений (например, публичная функция, возвращающая приватный тип). Старый линт (private_in_public) страдал от того, что принимал во внимание исключительно номинальную видимость — ту, которая ставится перед именем (тип pub(in foo)). Из-за этого линт имел формально простое, но довольно неинтуитивное поведение, причём ещё и неполное — из-за вывода типов приватный тип мог утечь способом, который старый линт не ловил. Теперь его заменили пачкой новых линтов, которые работают на эффективной видимости, т. е. принимая во внимание видимость объемлющих определений. Это даёт более полезное для людей поведение. Подробнее в соответствующем RFC.

▪️Насчёт линтов: задавать их теперь можно через секцию в манифесте Cargo.toml. С учётом того, что эта секция наследуется в workspace, это позволяет убедиться, что в группе связанных проектов используется идентичный набор глобальных линтов, без необходимости синхронизировать их руками.

▪️cargo clean теперь поддерживает --dry-run.

▪️Стабилизировали пачку API в стандартной библиотеке, в том числе:
🔸core::num::Saturating — адаптер для примитивных числовых типов, реализующий насыщающую семантику для арифметических операций:
    assert_eq!(Saturating(u32::MAX) + Saturating(1), Saturating(u32::MAX));
🔸пачку методов для перевода в/из байты для OsStr{, ing}: as_encoded_bytes/from_encoded_bytes_unchecked. Ранее это было возможно только на Unix-системах через std::os::unix::ffi::OsStrExt.
🔸Реализации From из ссылок (обоих видов) на массивы в векторы и из массивов в {Arc, Rc}<[T]>.

Дополнительно следующие API теперь могут быть использованы в константном контексте:
🔸core::mem::transmute_copy
🔸str::is_ascii, [u8]::is_ascii 😙👌

▪️Как я уже писал, Cell::swap теперь паникует на частично перекрывающихся значениях.

▪️rustdoc теперь позволяет добавлять свои CSS-классы к блокам кода и отдельные блоки для предупреждений.

▪️В сгенерированной rustdoc документации теперь можно искать с использованием типовых параметров.
Для примера, это означает, что Option::or можно найти по запросу

option<T>, option<T> -> option<T>
👍8❤‍🔥3
Forwarded from Labrats
#от_подписчика

В чём различие между учёным и курицей-гриль?

Курица-гриль может прокормить семью из четырёх человек.
🔥9😭6😢3