ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1126
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 19-31: Код, который потребуется в большинстве процедурных макро крейтов для обработки Rust
кода
Обратите внимание, что мы разделили код на функцию hello_macro_derive
, которая отвечает за синтаксический анализ
TokenStream и функцию impl_hello_macro
, которая отвечает за преобразование синтаксического дерева: это делает написание процедурного макроса удобнее. Код во внешней функции ( hello_macro_derive в данном случае) будет одинаковым для почти любого процедурного макрос крейта, который вы видите или создаёте. Код, который вы указываете в теле внутренней функции (в данном случае impl_hello_macro
) будет отличаться в зависимости от цели вашего процедурного макроса.
Мы представили три новых крейта: proc_macro syn и quote
. Макрос proc_macro поставляется с Rust, поэтому нам не нужно было добавлять его в зависимости внутри
Cargo.toml. Макрос proc_macro
- это API компилятора, который позволяет нам читать и манипулировать Rust кодом из нашего кода.
Крейт syn разбирает Rust код из строки в структуру данных над которой мы может выполнять операции. Крейт quote превращает структуры данных syn обратно в код
Rust. Эти крейты упрощают разбор любого вида Rust кода, который мы хотели бы обрабатывать: написание полного синтаксического анализатора для кода Rust не является простой задачей.
Функция hello_macro_derive будет вызываться, когда пользователь нашей библиотеки указывает своему типу
#[derive(HelloMacro)]
. Это возможно, потому что мы аннотировали функцию hello_macro_derive с помощью proc_macro_derive и указали имя
HelloMacro
, которое соответствует имени нашего типажа; это соглашение, которому следует большинство процедурных макросов.
Функция hello_macro_derive сначала преобразует input из
TokenStream в структуру данных, которую мы можем затем интерпретировать и над которой выполнять операции. Здесь крейт syn вступает в игру. Функция parse в syn принимает
TokenStream и возвращает структуру
DeriveInput
, представляющую разобранный код use proc_macro::TokenStream; use quote::quote; use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive
(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate let ast = syn::parse(input).unwrap();
// Build the trait implementation impl_hello_macro(&ast)
}
Rust. Листинг 19-32 показывает соответствующие части структуры
DeriveInput
, которые мы получаем при разборе строки struct Pancakes;
:
1 ... 51 52 53 54 55 56 57 58 ... 62
Листинг 19-32: Экземпляр
DeriveInput
получаемый, когда разбирается код имеющий атрибут макроса из
Листинга 19-30
Поля этой структуры показывают, что код Rust, который мы разобрали, является блок структуры с ident
(идентификатором, означающим имя) для
Pancakes
. Есть больше полей в этой структуре для описания всех видов кода Rust; проверьте документацию syn о структуре
DeriveInput для получения дополнительной информации.
Вскоре мы определим функцию impl_hello_macro
, в которой построим новый,
дополнительный код Rust. Но прежде чем мы это сделаем, обратите внимание, что выводом для нашего выводимого (derive) макроса также является
TokenStream
Возвращаемый
TokenStream добавляется в код, написанный пользователями макроса,
поэтому, когда они соберут свой крейт, они получат дополнительную функциональность,
которую мы предоставляем в изменённом
TokenStream
Возможно, вы заметили, что мы вызываем unwrap чтобы выполнить панику в функции hello_macro_derive
, если вызов функции syn::parse потерпит неудачу. Наш процедурный макрос должен паниковать при ошибках, потому что функции proc_macro_derive должны возвращать
TokenStream
, а не тип
Result для соответствия
API процедурного макроса. Мы упростили этот пример с помощью unwrap
, но в рабочем коде вы должны предоставить более конкретные сообщения об ошибках, если что-то пошло не правильно, используя panic!
или expect
Теперь, когда у нас есть код для преобразования аннотированного Rust кода из
TokenStream в экземпляр
DeriveInput
, давайте сгенерируем код реализующий типаж
HelloMacro у аннотированного типа, как показано в листинге 19-33.
Файл: hello_macro_derive/src/lib.rs
DeriveInput {
// --snip-- ident: Ident { ident:
"Pancakes"
, span: #
0
bytes(
95 103
)
}, data: Struct(
DataStruct { struct_token: Struct, fields: Unit, semi_token:
Some
(
Semi
)
}
)
}
Листинг 19-33. Реализация типажа
HelloMacro
с использованием проанализированного кода Rust.
Мы получаем экземпляр структуры
Ident содержащий имя (идентификатор)
аннотированного типа с использованием ast.ident
. Структура в листинге 19-32
показывает, что когда мы запускаем функцию impl_hello_macro для кода из листинга 19-
30, то получаемый ident будет иметь поле ident со значением "Pancakes"
. Таким образом, переменная name в листинге 19-33 будет содержать экземпляр структуры
Ident
, что при печати выдаст строку "Pancakes"
, что является именем структуры в листинге 19-30.
Макрос quote!
позволяет определить код Rust, который мы хотим вернуть. Компилятор ожидает что-то отличное от прямого результата выполнения макроса quote!
, поэтому нужно преобразовать его в
TokenStream
. Мы делаем это путём вызова метода into
,
который использует промежуточное представление и возвращает значение требуемого типа
TokenStream
Макрос quote!
также предоставляет очень классную механику шаблонов: мы можем ввести
#name и quote!
заменит его значением из переменной name
. Вы можете даже сделать некоторое повторение, подобное тому, как работают обычные макросы.
Проверьте документацию крейта quote для подробного введения.
Мы хотим, чтобы наш процедурный макрос генерировал реализацию нашего типажа
HelloMacro для типа, который аннотировал пользователь, который мы можем получить,
используя
#name
. Реализация типажа имеет одну функцию hello_macro
, тело которой содержит функциональность, которую мы хотим предоставить: напечатать
Hello,
Macro! My name is с именем аннотированного типа.
Макрос stringify!
используемый здесь, встроен в Rust. Он принимает Rust выражение,
такое как
1 + 2
и во время компиляции компилятор превращает выражение в строковый литерал, такой как "1 + 2"
. Он отличается от макросов format!
или println!
, которые вычисляют выражение, а затем превращают результат в виде типа
String
. Существует возможность того, что введённый
#name может оказаться выражением для печати буквально как есть, поэтому здесь мы используем stringify!
Использование stringify!
также сохраняет выделение путём преобразования
#name в
строковый литерал во время компиляции.
fn impl_hello_macro
(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let gen = quote! { impl
HelloMacro for
#name { fn hello_macro
() { println!
(
"Hello, Macro! My name is {}!"
, stringify!
(#name));
}
}
}; gen.into()
}
На этом этапе команда cargo build должна завершиться успешно для обоих hello_macro и hello_macro_derive
. Давайте подключим эти крейты к коду в листинге 19-
30, чтобы увидеть процедурный макрос в действии! Создайте новый бинарный проект в каталоге ваших проектов с использованием команды cargo new pancakes
. Нам нужно добавить hello_macro и hello_macro_derive в качестве зависимостей для крейта pancakes в файл Cargo.toml. Если вы публикуете свои версии hello_macro и hello_macro_derive на сайт crates.io
, они будут обычными зависимостями; если нет, вы можете указать их как path зависимости следующим образом:
Поместите код в листинге 19-30 в src/main.rs и выполните cargo run
: он должен вывести
Hello, Macro! My name is Pancakes!
. Реализация типажа
HelloMacro из процедурного макроса была включена без необходимости его реализации крейтом pancakes
;
#
[derive(HelloMacro)]
добавил реализацию типажа.
Далее давайте рассмотрим, как другие виды процедурных макросов отличаются от пользовательских выводимых макросов.
подобные атрибутам макросы
Подобные атрибутам макросы похожи на пользовательские выводимые макросы, но вместо генерации кода для derive атрибута, они позволяют создавать новые атрибуты.
Они являются также более гибкими: derive работает только для структур и перечислений; атрибут-подобные могут применяться и к другим элементам, таким как функции. Вот пример использования атрибутного макроса: допустим, у вас есть атрибут именованный route который аннотирует функции при использовании фреймворка для веб-приложений:
Данный атрибут
#[route]
будет определён платформой как процедурный макрос.
Сигнатура функции определения макроса будет выглядеть так:
Здесь есть два параметра типа
TokenStream
. Первый для содержимого атрибута: часть
GET, "/"
. Второй это тело элемента, к которому прикреплён атрибут: в данном случае fn index() {}
и остальная часть тела функции.
Кроме того, атрибутные макросы работают так же как и пользовательские выводимые макросы: вы создаёте крейт с типом proc-macro и реализуете функцию, которая hello_macro = { path =
"../hello_macro"
} hello_macro_derive = { path =
"../hello_macro/hello_macro_derive"
}
#[route(GET, "/")]
fn index
() {
#[proc_macro_attribute]
pub fn route
(attr: TokenStream, item: TokenStream) -> TokenStream {
генерирует код, который хотите!
Функционально подобные макросы
Функционально подобные макросы выглядят подобно вызову функций. Они аналогично макросам macro_rules!
и являются более гибкими, чем функции; например, они могут принимать неизвестное количество аргументов. Тем не менее, макросы macro_rules!
можно объявлять только с использованием синтаксиса подобного сопоставлению,
который мы обсуждали ранее в разделе "Декларативные макросы macro_rules!
для общего мета программирования"
. Функционально подобные макросы принимают параметр
TokenStream и их определение манипулирует этим
TokenStream
, используя код
Rust, как это делают два других типа процедурных макроса. Примером подобного функционально подобного макроса является макрос sql!/code6}, который можно вызвать так:
Этот макрос будет разбирать SQL оператор внутри него и проверять, что он синтаксически правильный, что является гораздо более сложной обработкой, чем то что может сделать макрос macro_rules!
. Макрос sql!
мог бы быть определён так:
Это определение похоже на сигнатуру пользовательского выводимого макроса: мы получаем токены, которые находятся внутри скобок и возвращаем код, который мы хотели сгенерировать.
Итоги
Уф! Теперь у вас есть некоторые возможности Rust, которые вы не будете часто использовать, но вы будете знать, что они доступны в особых обстоятельствах. Мы представили несколько сложных тем, чтобы при появлении сообщения с предложением исправить ошибку или в коде других людей, вы могли бы распознать эти концепции и синтаксис. Используйте эту главу как справочник, который поможет вам в решениях.
Далее мы применим все, что мы обсуждали в книге и сделаем ещё один проект!
let sql = sql!(SELECT * FROM posts WHERE id=
1
);
#[proc_macro]
pub fn sql
(input: TokenStream) -> TokenStream {
Функционально подобные макросы
Функционально подобные макросы выглядят подобно вызову функций. Они аналогично макросам macro_rules!
и являются более гибкими, чем функции; например, они могут принимать неизвестное количество аргументов. Тем не менее, макросы macro_rules!
можно объявлять только с использованием синтаксиса подобного сопоставлению,
который мы обсуждали ранее в разделе "Декларативные макросы macro_rules!
для общего мета программирования"
. Функционально подобные макросы принимают параметр
TokenStream и их определение манипулирует этим
TokenStream
, используя код
Rust, как это делают два других типа процедурных макроса. Примером подобного функционально подобного макроса является макрос sql!/code6}, который можно вызвать так:
Этот макрос будет разбирать SQL оператор внутри него и проверять, что он синтаксически правильный, что является гораздо более сложной обработкой, чем то что может сделать макрос macro_rules!
. Макрос sql!
мог бы быть определён так:
Это определение похоже на сигнатуру пользовательского выводимого макроса: мы получаем токены, которые находятся внутри скобок и возвращаем код, который мы хотели сгенерировать.
Итоги
Уф! Теперь у вас есть некоторые возможности Rust, которые вы не будете часто использовать, но вы будете знать, что они доступны в особых обстоятельствах. Мы представили несколько сложных тем, чтобы при появлении сообщения с предложением исправить ошибку или в коде других людей, вы могли бы распознать эти концепции и синтаксис. Используйте эту главу как справочник, который поможет вам в решениях.
Далее мы применим все, что мы обсуждали в книге и сделаем ещё один проект!
let sql = sql!(SELECT * FROM posts WHERE id=
1
);
#[proc_macro]
pub fn sql
(input: TokenStream) -> TokenStream {
Финальный проект: создание
многопоточного веб-сервера
Это был долгий путь, но мы дошли до финала книги. В этой главе мы создадим ещё один проект для демонстрации некоторых концепций, которые мы рассмотрели в последних главах, а также резюмировать некоторые предыдущие уроки.
Для нашего финального проекта мы создадим веб-сервер, который говорит “hello” и выглядит как рисунке 20-1 в веб-браузере.
Рисунок 20-1: Наш последний совместный проект
Вот план по созданию веб-сервера:
1. Узнать немного о протоколах TCP и HTTP.
2. Прослушивать TCP соединения у сокета.
3. Разобрать небольшое количество HTTP-запросов.
4. Создать правильный HTTP ответ.
5. Улучшите пропускную способность нашего сервера с помощью пула потоков.
Но прежде чем мы начнём, мы должны упомянуть одну деталь. Способ который мы будем использовать не является лучшим способом построения веб-сервер в Rust.
Несколько готовых к использованию крейтов доступны на crates.io,
и способны обеспечить более полную реализацию веб-сервера и пула потоков, чем сделаем мы сами.
Однако в этой главе мы хотим помочь вам научиться, а не выбирать лёгкий путь.
Поскольку Rust является языком системного программирования, мы можем выбрать тот уровень абстракции на котором мы хотим работать и можем перейти на более низкий уровень, чем возможно или практично для использования в других языках. Мы напишем
базовый HTTP сервер и пул потоков вручную, чтобы вы могли изучить общие идеи и техники из крейтов, которые вы могли бы использовать в будущем.
Создание однопоточного веб-сервера
Начнём с однопоточного веб-сервера. Перед тем, как начать, давайте рассмотрим краткий обзор протоколов, задействованных в создании веб-серверов. Детальное описание этих протоколов выходит за рамки этой книги, но краткий обзор даст вам необходимую информацию.
Два основных протокола, задействованных в веб-серверах, - это протокол передачи
гипертекста (HTTP) и протокол управления передачей (TCP). Оба протокола являются протоколами типа запрос-ответ, что означает, что клиент инициирует запросы, а сервер
слушает запросы и предоставляет ответ клиенту. Содержание этих запросов и ответов определяется протоколами.
TCP - это протокол нижнего уровня, который описывает детали того, как информация передаётся от одного сервера к другому, но не определяет, что это за информация. HTTP
строится поверх TCP, определяя содержимое запросов и ответов. Технически возможно использовать HTTP с другими протоколами, но в подавляющем большинстве случаев
HTTP отправляет свои данные поверх TCP. Мы будем работать с необработанными байтами в TCP и запросами и ответами в HTTP.
Прослушивание TCP соединения
Нашему веб-серверу необходимо прослушивать TCP-соединение, так что это первая часть, над которой мы будем работать. Стандартная библиотека предлагает для этого модуль std::net
. Сделаем новый проект обычным способом:
Теперь введите код из Листинга 20-1 в src/main.rs, чтобы начать. Этот код будет использовать адрес
127.0.0.1:7878
для входящих TCP-потоков. Когда он получит входящее соединение, он напечатает
Connection established!
,
Файл: src/main.rs
$
cargo new hello
Created binary (application) `hello` project
$
cd hello use std::net::TcpListener; fn main
() { let listener = TcpListener::bind(
"127.0.0.1:7878"
).unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println!
(
"Connection established!"
);
}
}
Листинг 20-1: Приём и прослушивание входящих потоков, печать сообщения, когда мы получаем поток
Используя
TcpListener
, можно прослушивать TCP соединения по адресу
127.0.0.1:7878
. В адресе, в его части перед двоеточием, сначала идёт IP-адрес представляя ваш компьютер (он одинаковый на каждом компьютере и не представляет конкретный компьютер автора), а часть
7878
является портом. Мы выбрали этот порт по двум причинам: HTTP обычно может принимать на этом порту, и 7878 - это слово rust
набранное на телефоне.
Функция bind в этом сценарии работает так же, как функция new
, поскольку она возвращает новый экземпляр
TcpListener
. Причина, по которой функция называется bind заключается в том, что в сетевой терминологии подключение к порту для прослушивания называется «привязка к порту».
Функция bind возвращает
Result
, который указывает, что привязка может завершиться ошибкой. Например, для подключения к порту 80 требуются права администратора (не администраторы могут прослушивать только порты выше 1024),
поэтому, если мы попытаемся подключиться к порту 80, не будучи администратором,
привязка не сработает. Другой пример: привязка не сработает, если мы запустили два экземпляра нашей программы, и поэтому две программы будут прослушивать один и тот же порт. Поскольку мы пишем базовый сервер только в учебных целях, мы не будем беспокоиться об обработке таких ошибок; вместо этого мы используем unwrap чтобы остановить программу в случае возникновения ошибок.
Метод incoming в
TcpListener возвращает итератор, который даёт нам последовательность потоков (конкретнее, потоков типа
TcpStream
). Один поток
представляет собой открытое соединение между клиентом и сервером. Соединение - это полный процесс запроса и ответа, в котором клиент подключается к серверу, сервер генерирует ответ, и сервер закрывает соединение. Таким образом,
TcpStream позволяет прочитать из себя, то что отправил клиент, а затем позволяет записать наш ответ в поток. В целом, цикл for будет обрабатывать каждое соединение по очереди и создавать серию потоков, которые мы будем обрабатывать.
На данный момент обработка потока состоит из вызова unwrap для завершения программы, если поток имеет какие-либо ошибки, а если ошибок нет, то печатается сообщение. Мы добавим больше функциональности для случая успешной работы в следующем листинге кода. Причина, по которой мы можем получить ошибки из метода incoming при подключении клиента к серверу, является то, что мы на самом деле не перебираем соединения. Вместо этого мы перебираем попытки подключения.
Соединение может быть не успешным по ряду причин, многие из них специфичны в операционной системе. Например, многие операционные системы имеют ограничение количества одновременных открытых соединений, которые они поддерживают; попытки создания нового соединения, превышающее это число, будут приводить к ошибкам до тех пор, пока некоторые из ранее открытых соединений не будут закрыты.