Макросы в tentacli. Часть один

от автора

Со времени публикации первых двух статей мой проект сменил имя и концепцию. Теперь он называется 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *