Со времени публикации первых двух статей мой проект сменил имя и концепцию. Теперь он называется 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, если на этапе чтения мы его прочтем тогда, когда этого делать было не нужно, то следующее поле (и все дальнейшие) уже будет прочитано неправильно, что приведет к ошибке.

А третий вопрос задавайте в комментариях.

Комментарии (0)