Со времени публикации первых двух статей мой проект сменил имя и концепцию. Теперь он называется TentaCLI и это название, являющееся игрой слов tentacle и cli, полностью отражает новую суть проекта. Хотя tentacli по прежнему может быть скачан с github и использоваться, как отдельное клиентское приложение, он и его части так же доступны в виде крэйтов. Внедряемость, а так же возможность добавлять собственные модули в tentacli делает его подходящим для создания собственных приложений. В частности, у меня таких два: мини wow сервер для тестирования tine и скрытый проект binary army, в котором tentacli полностью раскрывает свой потенциал как щупальца-исполнителя — и для управления которыми я пишу сердце.
А сердце tentacli — это чтение и обработка TCP пакетов и для облегчения работы с ними я использую макросы.
Мотивация
Давным-давно, в далекой-предалекой первой версии парсинг и создание пакетов представляли собой весьма муторную задачу:
// чтение пакета с опкодом SMSG_MESSAGECHAT let mut reader = Cursor::new(input.data.as_ref().unwrap()[4..].to_vec()); let message_type = reader.read_u8()?; let language = reader.read_u32::<LittleEndian>()?; let sender_guid = reader.read_u64::<LittleEndian>()?; // skip reader.read_u32::<LittleEndian>()?; // условное поле раз let mut channel_name = Vec::new(); if message_type == MessageType::CHANNEL { reader.read_until(0, &mut channel_name)?; } let channel_name = match channel_name.is_empty() { true => String::new(), false => { String::from_utf8( channel_name[..(channel_name.len() - 1) as usize].to_owned() ).unwrap() }, }; let target_guid = reader.read_u64::<LittleEndian>()?; let size = reader.read_u32::<LittleEndian>()?; // условное поле два let mut message = vec![0u8; (size - 1) as usize]; reader.read_exact(&mut message)?; let message = String::from_utf8_lossy(&message);
Возможно, вы подумали, что это специальный код для наказаний и, вобщем-то, так оно и было — масштабировать подобный код было тем еще испытанием. Как итог, были реализованы обработчики только для самых базовых пакетов. Но этой ситуации суждено было измениться.
Начало великого перехода
В процессе исследований я нашел крэйты serde и bincode. Однако, концепция, которую я задумал, не могла быть реализована с помощью данных крэйтов — мне нужна была условная десериализация. Пример кода выше — идеальный для отражения проблемы, поскольку в нем представлены сразу два случая условной десериализации: когда поле (channel_name) может быть прочитано только в случае, если совпало некое условие и когда чтение поля (message) зависит от ранее прочитанного поля (size). Я размышлял над максимально лаконичной формой описания таких полей.
Итогом моих экспериментов и исследований, а так же значительной помощи со стороны официального Rust комьюнити стал вот такой макрос — который заменил код выше:
#[derive(WorldPacket, Serialize)] struct Incoming { message_type: u8, language: u32, sender_guid: u64, skip: u32, #[conditional] channel_name: String, target_guid: u64, message_length: u32, #[depends_on(message_length)] message: String, } impl Incoming { fn channel_name(instance: &mut Self) -> bool { instance.message_type == MessageType::CHANNEL } }
Устройство макроса
Теперь по порядку разберем, как он устроен. Фундаментом для чтения/записи данных служит трейт BinaryConverter :
pub trait BinaryConverter { fn write_into(&mut self, buffer: &mut Vec<u8>) -> AnyResult<()>; fn read_from<R: BufRead>( reader: &mut R, dependencies: &mut Vec<u8> ) -> AnyResult<Self> where Self: Sized; }
Этот трейт я имплеменчу на каждый тип, который хочу использовать в полях сериалайзера:
impl BinaryConverter for u8 { fn write_into(&mut self, buffer: &mut Vec<u8>) -> AnyResult<()> { buffer.write_u8(*self).map_err(|e| FieldError::CannotWrite(e, "u8".to_string()).into()) } fn read_from<R: BufRead>(reader: &mut R, _: &mut Vec<u8>) -> AnyResult<Self> { reader.read_u8().map_err(|e| FieldError::CannotRead(e, "u8".to_string()).into()) } }
В некоторых случаях требуется чуть больше кода, к примеру, для строк:
impl BinaryConverter for String { fn write_into(&mut self, buffer: &mut Vec<u8>) -> AnyResult<()> { buffer.write_all(self.as_bytes()) .map_err(|e| FieldError::CannotWrite(e, "String".to_string()))?; Ok(()) } fn read_from<R: BufRead>( reader: &mut R, dependencies: &mut Vec<u8> ) -> AnyResult<Self> { let mut cursor = Cursor::new(dependencies.to_vec()); let size = match dependencies.len() { 1 => ReadBytesExt::read_u8(&mut cursor) .map_err(|e| FieldError::CannotRead(e, "String u8 size".to_string()))? as usize, 2 => ReadBytesExt::read_u16::<LittleEndian>(&mut cursor) .map_err(|e| FieldError::CannotRead(e, "String u16 size".to_string()))? as usize, 4 => ReadBytesExt::read_u32::<LittleEndian>(&mut cursor) .map_err(|e| FieldError::CannotRead(e, "String u32 size".to_string()))? as usize, _ => 0, }; let buffer = if size > 0 { let mut buffer = vec![0u8; size]; reader.read_exact(&mut buffer) .map_err(|e| FieldError::CannotRead(e, "String".to_string()))?; buffer } else { let mut buffer = vec![]; reader.read_until(0, &mut buffer) .map_err(|e| FieldError::CannotRead(e, "String".to_string()))?; buffer }; let string = String::from_utf8(buffer) .map_err(|e| FieldError::InvalidString(e, "String".to_string()))?; Ok(string.trim_end_matches(char::from(0)).to_string()) } }
То же самое справедливо и для пользовательских типов. Благодаря этому при объявлении сериалайзера можно использовать тип Player напрямую в качестве типа для поля:
// пакет с опкодом SMSG_CHAR_ENUM #[derive(WorldPacket, Serialize, Debug)] struct Incoming { characters_count: u8, #[depends_on(characters_count)] characters: Vec<Player>, }
Теперь, при получении пакета с сервера, с помощью сериалайзера из примера выше мы можем прочитать список персонажей — в переменную characters:
let (Incoming { characters, .. }, json) = Incoming::from_binary(&input.data)?;
Метод from_binary возвращает tuple из двух элементов — инстанс текущего struct и json представление его полей.
Рассмотрим, откуда взялся этот метод и при чем же здесь trait BinaryConverter.
Изнанка сериалайзера
Есть два макроса: один для Login сервера, второй — для World сервера. Но выбирать из них мы не будем и рассмотрим только один, поскольку они сильно похожи.
#[proc_macro_derive(WorldPacket, attributes(depends_on, conditional))] pub fn world_packet(input: TokenStream) -> TokenStream { let ItemStruct { ident, fields, .. } = parse_macro_input!(input); // формируем список полей // формируем список зависимостей для полей // формируем список значений // формируем то, что вернет макрос TokenStream::from(output) }
Любой proc-macro скорее всего будет выглядеть как-то так.
Начать я хотел бы с конца, а именно — с объяснения, что такое output. Если вкратце, то это — переменная, которая содержит код, обернутый с помощью макроса quote!. Т.е. для того, чтобы struct, к которому я применяю мой макрос, получал некий метод, назовем его from_binary, понадобится добавить следующие строки в эту переменную:
#[proc_macro_derive(WorldPacket, attributes(depends_on, conditional))] pub fn world_packet(input: TokenStream) -> TokenStream { let ItemStruct { ident, fields, .. } = parse_macro_input!(input); // формируем список полей // формируем список зависимостей для полей // формируем список значений let output = quote! { impl #ident { pub fn from_binary(buffer: &[u8]) -> #result<(Self, String)> { println!("It works !"); // а здесь нужно вернуть результат } } }; TokenStream::from(output) }
В коде выше ident — это идентификатор того struct, к которому применен макрос. Знак решетки служит для интерполяции выражений — таким образом, в контексте текущей серии примеров, ident означает Incoming, как если бы я написал:
impl Incoming { pub fn from_binary(buffer: &[u8]) -> AnyResult<(Self, String)> { println!("It works !"); // а здесь нужно вернуть результат } }
Помимо переменных, интерполировать можно так же и импорты, к примеру, переменнаяresult — это не что иное, как quote!(anyhow::Result).
Теперь добавим формирование списка полей и списка значений. Поскольку задача метода from_binary — сформировать struct из пакета байт (ну, а так же json), нужно, чтобы внутри метода было что-то вроде:
let binary_converter = quote!(tentacli_traits::BinaryConverter); let cursor = quote!(std::io::Cursor); let output = quote! { impl #ident { pub fn from_binary(buffer: &[u8]) -> #result<(Self, String)> { println!("It works !"); let mut reader = #cursor::new(buffer); let json = String::new(); let instance = Self { characters_count: #binary_converter::read_from(&mut reader, &mut vec![]), characters: #binary_converter::read_from(&mut reader, &mut vec![]), }; Ok((instance, json)) } } };
Благодаря этому коду получился одноразовый макрос.
Теперь нужно сделать, чтобы он обрабатывал любой набор полей:
// эту строку я уже указывал в примерах выше, но просто добавлю ее // для ясности - откуда взялся fields let ItemStruct { ident, fields, .. } = parse_macro_input!(input); let field_names = fields.iter().map(|f| { // в этом случае ident - это уже идентификатор поля ! f.ident.clone() }).collect::<Vec<Option<Ident>>>(); let initializers = fields.iter() .map(|f| { let field_name = f.ident.clone(); let field_type = f.ty.clone(); quote! { { let value: #field_type = #binary_converter::read_from(&mut reader, &mut vec![])?; value } } }); let binary_converter = quote!(tentacli_traits::BinaryConverter); let cursor = quote!(std::io::Cursor); let output = quote! { impl #ident { pub fn from_binary(buffer: &[u8]) -> #result<(Self, String)> { println!("It works !"); let mut reader = #cursor::new(buffer); let json = String::new(); // а теперь магия развертывания let mut instance = Self { #(#field_names: #initializers),* }; Ok((instance, json)) } } };
Вот этот код (развертывание):
let mut instance = Self { #(#field_names: #initializers),* };
Будет компилятором преображен примерно в такой:
let mut instance = Self { field1: { let value: i32 = binary_converter::read_from(&mut reader, &mut vec![])?; value }, field2: { let value: String = binary_converter::read_from(&mut reader, &mut vec![])?; value }, // ... };
Т.е. иными словами, благодаря развертыванию происходит сопоставление каждого элемента из field_names элементу с тем же порядковым номером из initializers , затем каждая пара подставляется в Self — и разделяется запятой.
Атрибуты depends_on и conditional
Чтобы сформировать список полей, которые содержат заданные атрибуты, можно использовать обычный вектор или какой-нибудь hashmap/btreemap:
// этот struct объявлен вне макроса struct DependsOnAttribute { pub name: Ident, } impl Parse for DependsOnAttribute { fn parse(input: ParseStream) -> syn::Result<Self> { let name: Ident = input.parse()?; Ok(Self { name }) } } // дальнейший код уже внутри макроса let mut depends_on: BTreeMap<Option<Ident>, Vec<Ident>> = BTreeMap::new(); let mut conditional: Vec<Option<Ident>> = vec![]; for field in fields.iter() { let ident = field.ident.clone(); if field.attrs.iter().any(|attr| attr.path().is_ident("depends_on")) { let mut dependencies: Vec<Ident> = vec![]; field.attrs.iter().for_each(|attr| { if attr.path().is_ident("depends_on") { let parsed_attrs = attr.parse_args_with( Punctuated::<DependsOnAttribute, Token![,]>::parse_terminated ).unwrap(); for a in parsed_attrs { dependencies.push(a.name); } } }); depends_on.insert(ident.clone(), dependencies); } if field.attrs.iter().any(|attr| attr.path().is_ident("conditional")) { conditional.push(ident); } }
С помощью переменных depends_on и conditional мы просто формируем списки идентификаторов, которые будут использованы в дальнейшем (см. в конце статьи).
Но перед тем, как мы перейдем к завершающей фазе, хочу рассмотреть еще одну вещь.
В свое время parse_terminated и парсинг атрибутов в целом вызвал у меня много вопросов и непоняток, поэтому рассмотрим его подробнее с примерами.
Как вообще парсить атрибуты
Метод parse_terminated принимает два generic параметра: то, что мы ищем и то, чем это разделяется (separator).
Для начала сделаем макрос, атрибут которого будет принимать список чисел, которые затем можно будет вывести в консоль:
#[derive(Simple)] #[numbers(1, 2, 3, 4)] struct MyStruct; fn main() { MyStruct::output() } // и код макроса: #[proc_macro_derive(Simple, attributes(numbers))] pub fn simple(input: TokenStream) -> TokenStream { let DeriveInput { ident, attrs, .. } = parse_macro_input!(input); let mut numbers = vec![]; for attr in attrs { if attr.path().is_ident("numbers") { let number_list = attr.parse_args_with( Punctuated::<LitInt, Token![,]>::parse_terminated ).unwrap(); for number in number_list { numbers.push(number.base10_parse::<i32>().unwrap()); } } } // поскольку на вектор при интерполяции накладываются некоторые ограничения // для вывода мы можем предварительно привести его к строке let numbers_str = format!("{:?}", numbers); let output = quote! { impl #ident { pub fn output() { println!("{:?}", #numbers_str); // либо можно вывести вектор вот так: println!("{:?}", [ #( #numbers ),* ]); } } }; TokenStream::from(output) }
Каждый атрибут мы парсим с помощью attr.parse_args_with, который принимает парсер в качестве параметра. Собственно, парсером в нашем случае выступает вышеупомянутый parse_terminated .
Можно немного облагородить процесс парсинга и создать кастомный struct:
struct NumberList { numbers: Punctuated<LitInt, Token![,]>, } impl syn::parse::Parse for NumberList { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Ok(NumberList { numbers: Punctuated::parse_terminated(input)? }) } } // и в самом макросе number_list будет читаться как-то так: let number_list = attr.parse_args::<NumberList>().unwrap().numbers;
В таком случае использование parse_terminated можно вынести из общего кода. Концепция кастомного struct нам понадобится далее.
Теперь усложним задачу. Будем парсить список параметров, где есть пары ключ и значение:
#[derive(Middle)] #[values(tentacli=works, join=us, on=discord)] struct BetterStruct; // для этого я применю уже рассмотренный выше подход с кастомным struct struct ValuesList { pub items: Vec<(String, String)>, } impl syn::parse::Parse for ValuesList { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { let mut items = vec![]; while !input.is_empty() { let key: Ident = input.parse()?; input.parse::<Token![=]>()?; let value: Ident = input.parse()?; items.push((key.to_string(), value.to_string())); if input.peek(Token![,]) { input.parse::<Token![,]>().expect(","); } } Ok(Self { items }) } } #[proc_macro_derive(Middle, attributes(values))] pub fn middle(input: TokenStream) -> TokenStream { // ... for attr in attrs { if attr.path().is_ident("values") { let items_list = attr.parse_args::<ValuesList>().unwrap(); // ... } } // ... TokenStream::from(output) }
То, что мы парсим — это по сути просто набор токенов, поэтому можно воспринимать процесс парсинга как их последовательный перебор — и если какой-то элемент в последовательности будет пропущен (скажем, в нашем случае — пропущен знак «=») — произойдет ошибка на этапе компиляции.
Поскольку параметры, указанные в скобках атрибута, передаются без кавычек, они воспринимаются парсером, как идентификаторы, в противном случае каждый key/value нужно было бы парсить как строку с помощью LitStr вместо Ident.
Это все были атрибуты для struct. Для полноты картины рассмотрим так же атрибуты полей. С ними все то же самое, единственное отличие — эти атрибуты парсятся из fields.
#[derive(Hard)] #[values(tentacli=works, join=us, on=discord)] struct TopStruct { #[value("Tentacli")] name: String, #[value("https://github.com/idewave/tentacli")] github_link: String, #[value("https://crates.io/crates/tentacli")] crates_link: String, } #[proc_macro_derive(Hard, attributes(values, value))] pub fn hard(input: TokenStream) -> TokenStream { // чтобы получить fields вместо DeriveInput используем ItemStruct let ItemStruct { ident, fields, attrs, .. } = parse_macro_input!(input); for field in fields.iter() { field.attrs.iter().for_each(|attr| { if attr.path().is_ident("value") { let value = attr.parse_args::<LitStr>().unwrap(); values.push(value.value()); } }); } TokenStream::from(output) }
Я создал репу на гитхабе, в которой содержатся все три примера.
Заключение
Теперь с полным (я надеюсь) пониманием, как функционирует макрос — предлагаю дописать код для переменной initializers и для метода from_binary:
let initializers = fields .iter() .map(|f| { let field_name = f.ident.clone(); let field_type = f.ty.clone(); let output = if let Some(dep_fields) = depends_on.get(&field_name) { quote! { { let mut data: Vec<u8> = vec![]; #( #binary_converter::write_into( &mut cache.#dep_fields, &mut data, )?; )* #binary_converter::read_from(&mut reader, &mut data)? } } } else { quote! { { let value: #field_type = #binary_converter::read_from( &mut reader, &mut vec![] )?; cache.#field_name = value.clone(); value } } }; if conditional.contains(&field_name) { quote! { { if Self::#field_name(&mut cache) { #output } else { Default::default() } } } } else { output } }); let output = quote! { impl #ident { pub fn from_binary(buffer: &[u8]) -> #result<(Self, String)> { println!("It works !"); let mut cache = Self { #(#field_names: Default::default()),* }; let mut reader = #cursor::new(buffer); let mut instance = Self { #(#field_names: #initializers),* }; let details = instance.get_json_details()?; Ok((instance, details)) } } };
Первый вопрос, который может у вас появиться: что это за reader, cache и прочие разные переменные, которые не объявлены перед initializers, но почему-то используются внутри этой переменной. Ответ достаточно прост: содержимое переменной initializers будет подставлено в том месте переменной output, где мы ее указали. А все, что мы передали внутрь TokenStream::from(output) — будет скомпилировано одним куском. Таким образом, в коде выше, — переменная cache объявлена на 52 строке, переменная reader — на 56 и все они — объявлены ДО того, как initializers попал в код.
Второй вопрос: что есть cache. Это реплика инстанса текущего struct за исключением того, что запись туда ведется до первого поля с атрибутом depends_on. Благодаря этому подходу можно сделать запрос к ранее прочитанным полям, не дожидаясь окончания чтения всех полей. И на этапе билда решить, как правильно читать следующее поле. К примеру, возьмем самый первый код, где описан пакет SMSG_MESSAGECHAT. Есть там условное поле channel_name, если на этапе чтения мы его прочтем тогда, когда этого делать было не нужно, то следующее поле (и все дальнейшие) уже будет прочитано неправильно, что приведет к ошибке.
А третий вопрос задавайте в комментариях.
ссылка на оригинал статьи https://habr.com/ru/articles/835240/
Добавить комментарий