Neste tutorial, cobriremos tudo o que você precisa saber sobre macros Rust, incluindo uma introdução às macros em Rust e uma demonstração de como usar macros Rust com exemplos.
Abordaremos o seguinte:
- O que são macros Rust?
- Tipos de macros em Rust
- Macros declarativas em Rust
- Macros procedurais em Rust
O que são macros Rust?
Rust tem excelente suporte para macros. As macros permitem que você escreva código que escreve outro código, o que é conhecido como metaprogramação.
As macros fornecem funcionalidade semelhante a funções, mas sem o custo do tempo de execução. Há algum custo de tempo de compilação, entretanto, já que as macros são expandidas durante o tempo de compilação.
As macros Rust são muito diferentes das macros em C. As macros Rust são aplicadas à árvore de tokens, enquanto as macros C são substituições de texto.
Tipos de macros em Rust
Rust tem dois tipos de macros:
- Macros declarativas permitem que você escreva algo semelhante a uma expressão de correspondência que opera no código Rust que você fornece como argumentos. Ele usa o código que você fornece para gerar o código que substitui a invocação da macro
- Macros procedurais permitem que você opere na árvore de sintaxe abstrata (AST) do código Rust que é fornecido. Uma macro proc é uma função de um
TokenStream
(ou dois) para outroTokenStream
, onde a saída substitui a invocação da macro
Vamos ampliar as macros declarativas e procedurais e explorar alguns exemplos de como usar macros no Rust.
Macros declarativas em Rust
Essas macros são declaradas usando macro_rules!
. As macros declarativas são um pouco menos poderosas, mas fornecem uma interface fácil de usar para criar macros e remover código duplicado. Uma das macros declarativas comuns é println!
. As macros declarativas fornecem uma correspondência
como uma interface em que, na correspondência, a macro é substituída pelo código dentro do braço correspondido.
Criação de macros declarativas
//use macro_rules!{ } macro_rules! adicionar{ //macth like arm para macro ($ a: expr, $ b: expr)=> { //macro expandir para este código { //$ a e $ b serão modelados usando o valor/variável fornecido para macro $ a + $ b } } } fn main () { //chamada para macro, $ a=1 e $ b=2 adicione! (1,2); }
Este código cria uma macro para adicionar dois números. [macro_rules!]
são usados com o nome do macro, add
e o corpo da macro.
A macro não adiciona dois números, apenas se substitui pelo código para adicionar dois números. Cada braço da macro recebe um argumento para funções e vários tipos podem ser atribuídos a argumentos. Se a função add
também pode receber um único argumento, adicionamos outro braço.
macro_rules! adicionar{ //correspondência do primeiro braço add! (1,2), adicione! (2,3) etc ($ a: expr, $ b: expr)=> { { $ a + $ b } }; //Segundo braço macth add! (1), add! (2) etc ($ a: expr)=> { { $ a } } } fn main () { //chama a macro deixe x=0; adicione! (1,2); adicione! (x); }
Pode haver vários ramos em uma única macro que se expande para códigos diferentes com base em argumentos diferentes. Cada branch pode ter vários argumentos, começando com o sinal $
e seguido por um tipo de token:
-
item
-um item, como uma função, estrutura, módulo, etc. -
block
-um bloco (ou seja, um bloco de declarações e/ou uma expressão, entre colchetes) -
stmt
-uma declaração -
pat
-um padrão -
expr
-uma expressão -
ty
-um tipo -
ident
-um identificador -
caminho
-um caminho (por exemplo,foo
,:: std:: mem:: substituir
,transmutar:: <_, int>
,…) -
meta
-um meta item; as coisas que vão dentro dos atributos# [...]
e#! [...]
-
tt
-uma árvore de token único -
vis
-um qualificadorVisibility
possivelmente vazio
No exemplo, usamos o argumento $ typ
com o tipo de token ty
como um tipo de dados como u8
, u16 , etc. Esta macro se converte em um tipo específico antes de adicionar os números.
macro_rules! adicionar como{ //usando um tipo de token Ty para tipos de dados macthing passados para maccro ($ a: expr, $ b: expr, $ typ: ty)=> { $ a as $ typ + $ b as $ typ } } fn main () { println! ("{}", add_as! (0,2, u8)); }
Macros Rust também aceitam um número não fixo de argumentos. Os operadores são muito semelhantes à expressão regular. *
é usado para zero ou mais tipos de token e +
para zero ou um argumento.
macro_rules! adicionar como{ ( //bloco repetido $ ($ a: expr) //seperator , //zero ou mais * )=> { { //para lidar com o caso sem nenhum argumento 0 //bloco a ser repetido $ (+ $ a) * } } } fn main () { println! ("{}", add_as! (1,2,3,4));//=> println! ("{}", {0 + 1 + 2 + 3 + 4}) }
O tipo de token que se repete é colocado entre $ ()
, seguido por um separador e um *
ou um +
, indicando o número muitas vezes o token se repetirá. O separador é usado para distinguir os tokens uns dos outros. O bloco $ ()
seguido por *
ou +
é usado para indicar o bloco de repetição de código. No exemplo acima, + $ a
é um código de repetição.
Se você olhar com atenção, verá que um zero adicional é adicionado ao código para tornar a sintaxe válida. Para remover esse zero e tornar a expressão add
igual ao argumento, precisamos criar uma nova macro conhecida como TT muncher .
macro_rules! adicionar{ //primeiro braço em caso de argumento único e última variável/número restante ($ a: expr)=> { $ a }; //segundo braço no caso de dois argumentos serem passados e parar a recursão no caso de um número ímpar de argumentos ($ a: expr, $ b: expr)=> { { $ a + $ b } }; //adicione o número e o resultado dos argumentos restantes ($ a: expr, $ ($ b: tt) *)=> { { $ a + add! ($ ($ b) *) } } } fn main () { println! ("{}", adicione! (1,2,3,4)); }
O TT muncher processa cada token separadamente de maneira recursiva. É mais fácil processar um único token por vez. A macro tem três braços:
- Os primeiros braços tratam do caso se um único argumento for passado
- O segundo trata do caso se dois argumentos forem passados
- O terceiro braço chama a macro
add
novamente com o resto dos argumentos
Os argumentos da macro não precisam ser separados por vírgulas. Vários tokens podem ser usados com diferentes tipos de tokens. Por exemplo, colchetes podem ser usados com o tipo de token ident
. O compilador Rust pega o braço correspondente e extrai a variável da string do argumento.
macro_rules! ok_or_return { //corresponde a algo (q, r, t, 6,7,8) etc //o compilador extrai o nome e os argumentos da função. Ele injeta os valores nas respectivas variáveis. ($ a: ident ($ ($ b: tt) *))=> { { corresponder a $ a ($ ($ b) *) { Ok (valor)=> valor, Err (errar)=> { return Err (err); } } } }; } fn some_work (i: i64, j: i64)-> Result <(i64, i64), String> { se i + j> 2 { Ok ((i, j)) } outro { Err ("erro".to_owned ()) } } fn main ()-> Resultado <(), String> { ok_ou_retornar! (algum_trabalho (1,4)); ok_ou_retorno! (algum_trabalho (1,0)); Está bem(()) }
A macro ok_or_return
retorna a função se uma operação retornar Err
ou o valor de uma operação retornar Ok
. Ele pega uma função como argumento e a executa dentro de uma instrução match. Para argumentos passados para a função, ele usa repetição.
Freqüentemente, poucas macros precisam ser agrupadas em uma única macro. Nestes casos, são utilizadas regras de macro internas. Isso ajuda a manipular as entradas de macro e escrever TT munchers limpos.
Para criar uma regra interna, adicione o nome da regra começando com @
como o argumento. Agora, a macro nunca corresponderá a uma regra interna até que seja explicitamente especificada como um argumento.
macro_rules! ok_or_return { //regra interna. (@error $ a: ident, $ ($ b: tt) *)=> { { corresponder a $ a ($ ($ b) *) { Ok (valor)=> valor, Err (errar)=> { return Err (err); } } } }; //regra pública pode ser chamada pelo usuário. ($ a: ident ($ ($ b: tt) *))=> { ok_or_return! (@ error $ a, $ ($ b) *) }; } fn some_work (i: i64, j: i64)-> Result <(i64, i64), String> { se i + j> 2 { Ok ((i, j)) } outro { Err ("erro".to_owned ()) } } fn main ()-> Resultado <(), String> { //em vez de colchetes, colchetes também podem ser usados ok_ou_retornar! {algum_trabalho (1,4)}; ok_ou_retorno! (algum_trabalho (1,0)); Está bem(()) }
Análise avançada em Rust com macros declarativas
Às vezes, as macros realizam tarefas que exigem análise da própria linguagem Rust.
Reúna todos os conceitos que abordamos até este ponto, vamos criar uma macro que torne uma estrutura pública com o sufixo da palavra-chave pub
.
Primeiro, precisamos analisar a estrutura Rust para obter o nome da estrutura, os campos da estrutura e o tipo de campo.
Análise do nome e campo de uma estrutura
Uma declaração struct
tem uma palavra-chave de visibilidade no início (como pub
), seguida pela palavra-chave struct
e depois o nome de o struct
e o corpo do struct
.
macro_rules! tornar público{ ( //use vis type para a palavra-chave de visibilidade e ident para o nome da estrutura $ vis: vis struct $ struct_name: ident {} )=> { { pub struct $ struct_name {} } } }
O $ vis
terá visibilidade e $ struct_name
terá um nome de estrutura. Para tornar uma estrutura pública, precisamos apenas adicionar a palavra-chave pub
e ignorar a variável $ vis
.
Um struct
pode conter vários campos com o mesmo ou diferentes tipos de dados e visibilidade. O tipo de token ty
é usado para o tipo de dados, vis
para visibilidade e ident
para o nome do campo. Usaremos *
repetição para zero ou mais campos.
macro_rules! tornar público{ ( $ vis: vis struct $ struct_name: ident { $ ( //vis para visibilidade de campo, ident para nome de campo e tipo para tipo de dados de campo $ field_vis: vis $ field_name: ident: $ field_type: ty ), * } )=> { { pub struct $ struct_name { $ ( pub $ field_name: $ field_type, ) * } } } }
Análise de metadados da struct
Freqüentemente, a struct
possui alguns metadados anexados ou macros procedurais, como # [derive (Debug)]
. Esses metadados precisam permanecer intactos. A análise desses metadados é feita usando o tipo meta
.
macro_rules! tornar público{ ( //meta dados sobre estrutura $ (# [$ meta: meta]) * $ vis: vis struct $ struct_name: ident { $ ( //metadados sobre o campo $ (# [$ field_meta: meta]) * $ field_vis: vis $ field_name: ident: $ field_type: ty ), * $ (,) + } )=> { { $ (# [$ meta]) * pub struct $ struct_name { $ ( $ (# [$ field_meta: meta]) * pub $ field_name: $ field_type, ) * } } } }
Nossa macro make_public
está pronta agora. Para ver como o make_public
funciona, vamos usar Rust Playground para expandir a macro para o código real que é compilado.
macro_rules! tornar público{ ( $ (# [$ meta: meta]) * $ vis: vis struct $ struct_name: ident { $ ( $ (# [$ field_meta: meta]) * $ field_vis: vis $ field_name: ident: $ field_type: ty ), * $ (,) + } )=> { $ (# [$ meta]) * pub struct $ struct_name { $ ( $ (# [$ field_meta: meta]) * pub $ field_name: $ field_type, ) * } } } fn main () { tornar público!{ # [derivar (depurar)] struct Name { n: i64, t: i64, g: i64, } } }
O código expandido se parece com isto:
//algumas importações macro_rules! tornar público { ($ (# [$ meta: meta]) * $ vis: vis struct $ struct_name: ident { $ ($ (# [$ field_meta: meta]) * $ field_vis: vis $ field_name: ident : $ field_type: ty), * $ (,) + })=> { $ (# [$ meta]) * pub struct $ struct_name { $ ($ (# [$ field_meta: meta]) * pub $ field_name: $ tipo de campo,) * } } } fn main () { pub struct name { pub n: i64, pub t: i64, pub g: i64, } }
Limitações de macros declarativas
As macros declarativas têm algumas limitações. Alguns estão relacionados às próprias macros Rust, enquanto outros são mais específicos a macros declarativas.
- Falta de suporte para autocompletar e expansão de macros
- A depuração de macros declarativas é difícil
- Recursos de modificação limitados
- Binários maiores
- Tempo de compilação mais longo
Macros procedurais em Rust
Macros procedurais são uma versão mais avançada de macros. As macros procedurais permitem que você expanda a sintaxe existente do Rust. Ele recebe uma entrada arbitrária e retorna um código Rust válido.
Macros procedurais são funções que recebem um TokenStream
como entrada e retornam outro Token Stream
. As macros procedurais manipulam a entrada TokenStream
para produzir um fluxo de saída.
Existem três tipos de macros procedurais:
- macros semelhantes a atributos
- Derive macros
- macros semelhantes a funções
Examinaremos cada tipo de macro procedimental em detalhes abaixo.
Macros semelhantes a atributos
Macros semelhantes a atributos permitem que você crie um atributo personalizado que se anexa a um item e permite a manipulação desse item. Também pode receber argumentos.
# [algum_atributo_macro (algum_argumento)] fn perform_task () { //algum código }
No código acima, some_attribute_macros
é uma macro de atributo. Ele manipula a função perform_task
.
Para escrever uma macro do tipo atributo, comece criando um projeto usando cargo new macro-demo--lib
. Assim que o projeto estiver pronto, atualize o Cargo.toml
para notificar a carga de que o projeto criará macros procedimentais.
# Cargo.toml [lib] proc-macro=true
Agora, estamos prontos para nos aventurar em macros procedurais.
Macros procedurais são funções públicas que recebem TokenStream
como entrada e retornam outro TokenStream
. Para escrever uma macro procedural, precisamos escrever nosso analisador para analisar TokenStream
. A comunidade Rust tem uma caixa muito boa, syn
, para analisar TokenStream
.
syn
fornece um analisador pronto para a sintaxe Rust que pode ser usado para analisar TokenStream
. Você também pode analisar sua sintaxe combinando analisadores de baixo nível fornecendo syn
.
Adicione syn
e quote
a Cargo.toml
:
# Cargo.toml [dependências] syn={version="1.0.57", features=["full","fold"]} quote="1.0.8"
Agora podemos escrever uma macro do tipo atributo em lib.rs
usando a caixa proc_macro
fornecida pelo compilador para escrever macros procedurais. Uma macro crate procedural não pode exportar nada além de macros procedurais e macros procedurais definidas na crate não podem ser usadas na própria crate.
//lib.rs extern crate proc_macro; use proc_macro:: {TokenStream}; use citação:: {citação}; //usando proc_macro_attribute para declarar um atributo como macro procedural # [proc_macro_attribute] //_metadata é o argumento fornecido para a chamada da macro e _input é o código ao qual o atributo, como a macro, é anexado pub fn my_custom_attribute (_metadata: TokenStream, _input: TokenStream)-> TokenStream { //retendo um TokenStream simples para Struct TokenStream:: from (aspas! {Struct H {}}) }
Para testar a macro que adicionamos, crie um teste de integração criando uma pasta chamada testes
e adicionando o arquivo attribute_macro.rs
na pasta. Neste arquivo, podemos usar nossa macro semelhante a um atributo para teste.
//tests/attribute_macro.rs use macro_demo:: *; //macro converte struct S em struct H # [my_custom_attribute] struct S {} #[teste] fn test_macro () { //devido à macro, temos struct H no escopo deixe demo=H {}; }
Execute o teste acima usando o comando teste de carga
.
Agora que entendemos os fundamentos das macros procedurais, vamos usar syn
para alguma manipulação e análise avançada do TokenStream
.
Para saber como syn
é usado para análise e manipulação, vamos dar um exemplo de ti syn
GitHub repo . Este exemplo cria uma macro Rust que rastreia variáveis quando o valor muda.
Primeiro, precisamos identificar como nossa macro manipulará o código que anexa.
# [trace_vars (a)] fn do_something () { deixe a=9; a=6; a=0; }
A macro trace_vars
pega o nome da variável que precisa para rastrear e injeta uma declaração de impressão cada vez que o valor da variável de entrada, ou seja, a
muda. Ele rastreia o valor das variáveis de entrada.
Primeiro, analise o código ao qual a macro do tipo atributo é anexada. syn
fornece um analisador embutido para a sintaxe da função Rust. ItemFn
analisará a função e lançará um erro se a sintaxe for inválida.
# [proc_macro_attribute] pub fn trace_vars (_metadata: TokenStream, input: TokenStream)-> TokenStream { //analisando a função de ferrugem para uma estrutura fácil de usar deixe input_fn=parse_macro_input! (entrada como ItemFn); TokenStream:: from (quote! {Fn dummy () {}}) }
Agora que temos a input
analisada, vamos passar para os metadados
. Para metadados
, nenhum analisador embutido funcionará, então teremos que escrever um usando o módulo syn
‘s parse
.
# [trace_vars (a, c, b)]//precisamos analisar uma","lista separada de tokens //código
Para que syn
funcione, precisamos implementar a característica Parse
fornecida por syn
. Punctuated
é usado para criar um vetor
de Indent
separado por ,
.
struct Args { vars: HashSet} impl Parse for Args { fn parse (input: ParseStream)-> Result { //analisa a, b, c ou a, b, c onde a, b e c são indentados let vars=Punctuated:: :: parse_terminated (input) ?; Ok (Args { vars: vars.into_iter (). collect (), }) } }
Depois de implementar o traço Parse
, podemos usar a macro parse_macro_input
para analisar metadados
.
# [proc_macro_attribute] pub fn trace_vars (metadados: TokenStream, entrada: TokenStream)-> TokenStream { deixe input_fn=parse_macro_input! (entrada como ItemFn); //usando struct Args recém-criado let args=parse_macro_input! (metadados como Args); TokenStream:: from (quote! {Fn dummy () {}}) }
Agora modificaremos o input_fn
para adicionar println!
quando a variável mudar o valor. Para adicionar isso, precisamos filtrar os contornos que têm uma atribuição e inserir uma instrução de impressão após essa linha.
impl Args { fn should_print_expr (& self, e: & Expr)-> bool { corresponder * e { Expr:: Caminho (ref e)=> { //variável não deve começar com:: if e.path.leading_colon.is_some () { falso //deve ser uma única variável como `x=8` e não n:: x=0 } else if e.path.segments.len ()!=1 { falso } outro { //pegue a primeira parte deixe primeiro=e.path.segments.first (). desembrulhar (); //verifique se o nome da variável está no hashset Args.vars self.vars.contains (& first.ident) && first.arguments.is_empty () } } _=> falso, } } //usado para verificar se imprimir deixe i=0 etc ou não fn should_print_pat (& self, p: & Pat)-> bool { match p { //verifique se o nome da variável está presente no conjunto Pat:: Ident (ref p)=> self.vars.contains (& p.ident), _=> falso, } } //manipula a árvore para inserir a instrução de impressão fn assign_and_print (& mut self, left: Expr, op: & dyn ToTokens, right: Expr)-> Expr { //chamada recorrente à direita da declaração de atribuição let right=fold:: fold_expr (self, right); //retornando a subárvore manipulada parse_quote! ({ #left #op #right; println! (concat! (stringify! (# esquerda),"={:?}"), #esquerda); }) } //manipulando a instrução let fn let_and_print (& mut self, local: Local)-> Stmt { deixe Local {pat, init,..}=local; deixe init=self.fold_expr (* init.unwrap (). 1); //obtém o nome da variável atribuída let ident=match pat { Pat:: Ident (ref p)=> & p.ident, _=> inacessível! (), }; //nova subárvore parse_quote! { let #pat={ # [permitir (unused_mut)] let #pat=#init; println! (concat! (stringify! (# ident),"={:?}"), #ident); #ident }; } } }
No exemplo acima, a macro quote
é usada para modelar e escrever Rust. #
é usado para injetar o valor da variável.
Agora vamos fazer um DFS sobre input_fn
e inserir a instrução de impressão. syn
fornece uma característica Fold
que pode ser implementada para DFS em qualquer Item
. Precisamos apenas modificar os métodos de característica que correspondem ao tipo de token que queremos manipular.
impl Dobra para Args { fn fold_expr (& mut self, e: Expr)-> Expr { match e { //para alterar a atribuição como a=5 Expr:: Atribuir (e)=> { //check should print if self.should_print_expr (& e.left) { self.assign_and_print (* e.left, & e.eq_token, * e.right) } outro { //continue com o travesal padrão usando métodos padrão Expr:: Atribuir (fold:: fold_expr_assign (self, e)) } } //para alterar a atribuição e operação como +=1 Expr:: AssignOp (e)=> { //check should print if self.should_print_expr (& e.left) { self.assign_and_print (* e.left, & e.op, * e.right) } outro { //continua com o comportamento padrão Expr:: AssignOp (fold:: fold_expr_assign_op (self, e)) } } //continua com o comportamento padrão para o resto das expressões _=> fold:: fold_expr (self, e), } } //para declarações let como let d=9 fn fold_stmt (& mut self, s: Stmt)-> Stmt { match s { Stmt:: Local (s)=> { if s.init.is_some () && self.should_print_pat (& s.pat) { self.let_and_print (s) } outro { Stmt:: Local (fold:: fold_local (self, s)) } } _=> fold:: fold_stmt (self, s), } } }
O traço Fold
é usado para fazer um DFS de Item
. Ele permite que você use um comportamento diferente para vários tipos de token.
Agora podemos usar fold_item_fn
para injetar instruções de impressão em nosso código analisado.
# [proc_macro_attribute] pub fn trace_var (args: TokenStream, input: TokenStream)-> TokenStream { //analisa a entrada deixe input=parse_macro_input! (entrada como ItemFn); //analisa os argumentos deixe mut args=parse_macro_input! (args como Args); //cria a saída deixe output=args.fold_item_fn (input); //retorna o TokenStream TokenStream:: from (quote! (# Output)) }
Este exemplo de código é do syn
samples repo , que é um excelente recurso para aprender sobre macros procedurais.
Macros de derivação personalizadas
Macros de derivação personalizadas no Rust permitem características de implementação automática. Essas macros permitem que você implemente características usando # [derive (Trait)]
.
syn
tem excelente suporte para macros derivar
.
# [derivar (Traço)] struct MyStruct {}
Para escrever uma macro de derivação personalizada em Rust, podemos usar DeriveInput
para analisar a entrada para derivar a macro. Também usaremos a macro proc_macro_derive
para definir uma macro de derivação personalizada.
# [proc_macro_derive (Traço)] pub fn derive_trait (input: proc_macro:: TokenStream)-> proc_macro:: TokenStream { deixe input=parse_macro_input! (entrada como DeriveInput); let name=input.ident; vamos expandido=citar! { Traço impl para #nome { fn print (& self)-> usize { println! ("{}","olá de #nome") } } }; proc_macro:: TokenStream:: from (expandido) }
Macros procedurais mais avançados podem ser escritos usando syn
. Confira este exemplo de syn
repo.
Macros semelhantes a funções
As macros semelhantes a funções são semelhantes às macros declarativas porque são chamadas com o operador de invocação de macro !
e se parecem com chamadas de função. Eles operam no código que está entre parênteses.
Veja como escrever uma macro semelhante a uma função no Rust:
# [proc_macro] pub fn a_proc_macro (_input: TokenStream)-> TokenStream { TokenStream:: from (quote! ( fn anwser ()-> i32 { 5 } )) }
Macros semelhantes a funções são executadas não em tempo de execução, mas em tempo de compilação. Eles podem ser usados em qualquer lugar no código Rust. Macros semelhantes a funções também recebem um TokenStream
e retornam um TokenStream
.
As vantagens de usar macros procedurais incluem:
- Melhor tratamento de erros usando
span
- Melhor controle sobre a produção
- Caixas construídas pela comunidade
syn
equote
- Mais poderoso do que macros declarativas
Conclusão
Neste tutorial de macros do Rust, cobrimos o básico das macros no Rust, definimos macros declarativas e procedurais e explicamos como escrever os dois tipos de macros usando várias sintaxes e caixas criadas pela comunidade. Também destacamos as vantagens de usar cada tipo de macro Rust.
A postagem Macros in Rust: um tutorial com exemplos apareceu primeiro no LogRocket Blog .