В этой статье мы немного потешимся с языком программирования Rust, а в частности, с типаж-объектами. Когда я знакомился с Rust, одна из деталей реализации типаж-объектов мне показалась интересной. А именно то, что виртуальная таблица функций находится не в самих данных, а в "толстом" указателе на них. Каждый указатель на типаж-объект ) содержит указатель на сами данные, а так же ссылку на виртуальную таблицу, где будут находиться адреса функций, реализующий данный типаж-объект для заданной структуры (но так как это деталь реализации, то поведение может поменяться. Начнём с простого примера, демонстрирующего "толстые" указатели. Следующий код выведет на 64-разрядной архитектуре 8 и 16: Чем это интересно? Когда я занимался энтерпрайз-Java, одна из задач, которая довольно-таки регулярно возникала была адаптация существующих объектов под заданные интерфейсы. То есть объект вот уже есть, выдан в виде ссылки, а надо его адаптировать к заданному интерфейсу. И менять входной объект нельзя, он есть такой, какой есть. Приходилось делать как-то так: У такого подхода вылазили разные проблемы. Например, если один и тот же объект "адаптируется" два раза, то получим два разных Person (с точки зрения сравнения ссылок). Да и сам факт, что приходится создавать новые объекты каждый раз как-то некрасиво. Когда я увидел типаж-объекты в Rust, у меня возникла мысль, что в Rust это можно сделать гораздо более элегантно! Можно же взять и приписать данным другую виртуальную таблицу и получить новый типаж-объект! И не надо выделять память на каждый экземпляр. При этом, вся логика "заимствования" остаётся на месте — наша функция адаптирования будет иметь вид что-то вроде fn adapt<'a>(value: &'a Json) -> &'a Person (то есть мы как бы заимствуем из исходных данных). Даже более того, можно "заставить" один и тот же тип (например, String) реализовать наш типаж-объект несколько раз, с разным поведением. Зачем? Да мало ли чего может понадобиться в энтерпрайзе?! Давайте попробуем это реализовать. Поставим задачу таким образом: сделать функцию annotate, которая "припишет" обычному типу String следующий типаж-объект: И сама функция annotate: Напишем сразу тест. Во-первых, убедимся, что "приписанный" тип совпадает с ожидаемым. Во вторых, убедимся, что мы можем достать исходную строку и это будет всё та же строка (с точки зрения указателей): Для начала попробуем сделать совсем наивную реализацию. Просто завернём наши данные в "обёртку", которая будет дополнительно содержать type_name: Пока ничего особенного. Всё как в Java. Но у нас же нет сборщика мусора, где мы хранить-то будет эту обёртку? Нам же надо ссылку вернуть, да так, чтобы она осталась действительной после вызова функции annotate. Ничего, страшного засунем в коробку (Box), чтобы обёртка (Wrapper) была выделена на куче. А потом вернём на неё ссылку. А чтобы обёртка осталась жить после вызова функции annotate, мы эту коробку "утечём": … и тест проходит! Но это какое-то сомнительное решение. Мало того, что мы всё так же выделяем память при каждом "аннотировании", так ещё и память утекает (Box::leak возвращает ссылку на данные, сохранённые на куче, но при этом "забывает" саму коробку, то есть автоматического освобождения не произойдет). Для начала попробуем куда-нибудь сохранить эти обёртки так, чтобы они всё-таки высвобождались в какой-то момент. Но при этом сохранив сигнатуру annotate как она есть. То есть вернуть ссылку с подсчётом ссылок (например, Rc<Wrapper>) — не подходит. Самый простой вариант — завести вспомогательную структуру, "система типов", которая будет отвечать за хранение этих обёрток. А когда мы закончим, мы высвободим эту структуру и все обёртки вместе с ней. Как-то так. Используется библиотека typed-arena для хранения обёрток, но можно было обойтись и типом Vec<Box<Wrapper>>, главное, гарантировать, что Wrapper никуда не перемещается (в ночном Rust можно для этого было использорвать pin API): Но куда же делся параметр, отвечающий за время жизни ссылки у типа Wrapper? От него пришлось избавиться, так как мы не можем приписать какое-то фиксированное время жизни в типе typed_arena::Arena<Wrapper<'?>>. Каждая обёртка имеет уникальный параметр, зависящий от input! Вместо этого, мы присыпем немного небезопасного Rust, чтобы избавиться от параметра-времени жизни: И тесты снова проходят, тем самым давая нам уверенность в правильности решения. В дополнение к ощущению лёгкой неловкости из-за unsafe (как и должно быть, с небезопасным Rust лучше не шутить!). Но всё же, а как же обещанный вариант, не требующий дополнительных выделений памяти на обёртки? Идея. Для каждого уникального "типа" ("Widget", "Gadget"), мы создадим виртуальную таблицу. Руками, во время выполнения программы. И припишем её к переданной нам ссылке на сами данные (которые у нас, как мы помним, просто String). Для начала небольшое описание, того что же нам нужно получить. Итак, ссылка на типаж объект, как она устроена? По сути, это просто два указателя, один на сами данные, а другой — на виртуальную таблицу. Так и запишем: (#[repr(C)] нам нужен, чтобы гарантировать правильное расположение в памяти). Вроде всё просто, мы сгенерируем новую таблицу для заданных параметров и "соберём" ссылку на типаж-объект! Но из чего же состоит эта таблица? Правильный ответ на этот вопрос будет "это деталь реализации". Но мы сделаем так; создадим файл rust-toolchain в корне нашего проекта и запишем туда: nightly-2018-12-01. Ведь зафиксировання сборка может считаться стабильной, правда? Теперь, когда мы зафиксировали версию Rust (на самом деле, ночная сборка нам понадобится для одной из библиотек чуть ниже). После некоторого поиска в интернете выясняем, что формат таблицы простой: сначала идёт ссылка на деструктор, затем два поля связанных с выделением памяти (размер типа и выравнивание), а потом идут функции, одна за другой (порядок — на усмотрение компилятора, но у нас всего две функции, поэтому вероятность угадать довольно велика, 50%). Так и запишем: Аналогично, #[repr(C)] нужен, чтобы гарантировать правильное расположение в памяти. Я разделил на две структуры, чуть позже это нам пригодится. Попробуем теперь написать нашу систему типов, которая будет предоставлять функцию annotate. Нам нужно будет кешировать сгенерированные таблицы, поэтому заведём кеш: Мы используем внутреннее состояние RefCell чтобы наша функция TypeSystem::annotate могла получать &self как разделяемую ссылку. Это важно, так как мы "заимствуем" у TypeSystem, чтобы гарантировать, что сгенерированные нами виртуальные таблицы жили дольше, чем ссылка на типаж-объект, который мы возвращаем из annotate. Так как мы хотим, чтобы можно было аннотировать много экземляров, мы не можем заимствовать &mut self, как изменяемую ссылку. И набросаем вот такой код: Откуда же мы возьмём эту таблицу? Первые три записи в ней будут совпадать с записями для любой другой виртуальной таблицы для заданного типа. Поэтому, просто возьмём и скопируем их. Сначала заведём вот такой типаж: Он нам пригодится, чтобы получить эту самую "любую другую виртуальную таблицу". А потом, скопируем у него эти три записи: В принципе, размер и выравнивание мы могли бы получить через std::mem::size_of::<String>() и std::mem::align_of::<String>(). Но вот откуда еще можно "украсть" деструктор, я не знаю. Хорошо, но где же мы возьмём адреса этих функций, type_name_fn и as_string_fn? Можно заметить, что as_string_fn в общем-то и не нужна, указатель на данные-то всегда идёт первой записью в представлении типаж-объекта. То есть это функция всегда одна и та же: Но вот со второй функцией уже не так просто! Она же зависит от нашего имени "типа", type_name. Не беда, мы же можем просто сгенерировать эту функцию в рантайме. Возьмём для этого библиотеку dynasm (на текущий момент, требует ночной сборки Rust). Почитаем про
соглашения о вызове функций.  Для простоты предположим, что нас интересует только Mac OS и Linux (после всех этих веселых трансформаций, совместимость нас уже не особо волнует, правильно?). А, да, исключительно x86-64, конечно. Вторую функцию, as_string, реализовать легко. Нам обещают, что первый параметр будет в регистре RDI. А вернуть значение в RAX. То есть код функции будет что-то вроде: А вот первая функция немного хитрее. Во-первых, нам надо вернуть &str, а это толстый указатель. Его первая часть — указатель на строку, а вторая часть — длина строкового среза. К счастью, соглашение выше позволяет возвращать и 128-разрядные результаты, используя регистр EDX для второй части. Осталось где-то добыть ссылку на строковый срез, который содержит нашу строку type_name. Полагаться на type_name мы не хотим (хотя через аннотации времени жизни можно гарантировать, что type_name будет жить дольше, чем возвращённое значение). Но у нас есть копия этой строки, которую мы помещаем в хеш-таблицу. Скрестив пальцы, мы сделаем предположение что расположение строкового слайса который на вернёт String::as_str не поменяется от перемещения самой строки String (а перемещаться String будет в процессе смены размера HashMap, где эта строка хранится ключём). Не знаю, гарантирует ли стандартная библиотека такое поведение, но мы ж так, поиграть просто? Получаем необходимые компоненты: И пишем такую функцию: И, наконец, финальный код annotate: Для целей dynasm нужно ещё добавить поле buffer в нашу структуру TypeInfo. Это поле управляет памятью, которая хранит код наших сгенерированных функций: И все тесты проходят! Вот так легко и непринуждённо можно генерировать свои реализации типаж-объектов в Rust коде! Последнее решение активно полагается на детали реализации и поэтому к использованию не рекомендуется. Но в реальности приходится делать то, что приходится. Отчаянные времена требуют отчаянных мер! Есть, правда, (ещё) одна особенность, на которую я тут полагаюсь. А именно то, что безопасно высвобождать память, занятую виртуально таблицей после того, как нет ссылок на типаж-объект, её использующий. С одной стороны, это логично, использовать виртуальную таблицу можно только через ссылки типаж-объектов. С другой стороны, таблицы, предоставляемые Rust имеют время жизни 'static. Вполне можно предположить какой-нибудь код, который отделит таблицу от ссылки для каких-то своих целей (мало ли, например, для каких-то своих грязных делишек). Исходный код можно найти тут. Разработчик