Escribir macros de procedimiento en óxido

Escrito por:

El software de Qumulo se ha escrito completamente en C durante un tiempo. En los últimos años hemos estado coqueteando con Rust y los muchos beneficios que tiene para ofrecer; he escrito sobre esto anteriormente. aquí.

Recientemente hemos estado trabajando en escribir nuestro propio cliente LDAP, y parte de eso ha sido escribir nuestra propia biblioteca de serialización ASN.1 BER. Hemos podido proporcionar una forma idiomática de generar serialización gracias a una característica asesina a menudo pasada por alto de Rust: las macros.

Las macros han sido un elemento de muchos lenguajes de programación mucho antes de que Rust existiera. Tienden a ser evaluados antes de la fase de compilación normal y, a veces, por un programa completamente separado (llamado preprocesador). Se utilizan para generar más código no macro para ser compilado por la fase de compilación normal. Aquí en Qumulo estamos bastante familiarizados con las macros de C, aunque las macros de C son muy diferentes de las macros de Rust. Las macros C funcionan según los principios del análisis de texto, mientras que las macros Rust funcionan fichas.

// Macro de C #define LOCATION () (“file:” __FILE__) // ¡Macro macro_rules de oxidación! ubicación {() => {concat! ("archivo:", archivo! ())}}

De hecho, hay dos tipos diferentes de macros en Rust: los que están definidos y se ejecutan de manera similar a las macros C (que se muestran arriba), y otro tipo llamado macros de procedimiento. Estas macros están escritas en Rust (en lugar de un lenguaje macro). Se compilan en complementos (bibliotecas dinámicas) y el compilador los ejecuta al compilar. Esto significa que podemos escribir macros significativamente más complicadas con facilidad. Alimentan bibliotecas populares como serde o rocket, y son el tipo de macros que hemos usado en nuestra biblioteca de serialización. Vienen en un par de tipos, me voy a centrar en derivar macros de procedimiento, que generalmente se usan para implementar automáticamente un rasgo para un tipo.

rasgo Hola {fn hola (); } // Foobar implementa el rasgo Hello ahora, ¡y no tuvimos que escribir // un bloque impl! # [derivar (Hola)] struct FooBar;

En el núcleo, una macro de procedimiento es solo una función que toma una secuencia de tokens como entrada y proporciona una secuencia de tokens como salida. Tienden a tener dos fases distintas que llamaré una fase de análisis y una fase de generación.

Dado que solo se nos da un flujo de tokens, si queremos obtener información estructural sobre los tokens, primero tenemos que analizarlos. Esto se puede hacer usando syn. Durante la fase de análisis, los tokens se analizan en tipos de sincronización y luego en cualquier tipo intermedio de la macro.

Durante la fase de generación, los tipos de la fase de análisis se aprovechan para generar código nuevo. los cotización inicial La biblioteca se puede utilizar para crear plantillas y generar una secuencia de tokens. Generalmente con derivar macros de procedimiento, están generando código que implementa un rasgo.

use proc_macro :: TokenStream; use syn :: {parse_macro_input, DeriveInput}; use quote :: quote; # [proc_macro_derive (Hola)] pub fn derive_hello (entrada: TokenStream) -> TokenStream {// Fase de análisis let derive_input = parse_macro_input! (entrada como DeriveInput); let ident = & derive_input.ident; let name = derive_input.ident.to_string (); // Generar fase (quote! {Impl Hola para #ident {fn hello () {println! ("Hola desde {}", #name);}}}). Into ()}


Derivar macros de procedimiento puede ahorrar mucho trabajo. En lugar de implementar el mismo rasgo para muchos tipos diferentes, podemos escribir una macro que implemente el rasgo para cualquier tipo.

use ber :: {Codificar, Decodificar, DefaultIdentifier}; # [derive (Encode, PartialEq, Debug, DefaultIdentifier, Decode)] struct StructPrimitives {x: u32, is_true: bool, negative: i32,} # [prueba] fn encode () {let s = StructPrimitives {x: 42, is_true : verdadero, negativo: -42}; // ¡Tenemos un método de codificación gracias a las macros! deje mut codificado = vec! []; s.encode (& mut codificado) .unwrap (); }

Nuestra biblioteca de serialización puede codificar la estructura StructPrimitives aunque no escribimos explícitamente la implementación de Encode. Gracias a la macro de procedimiento, la biblioteca puede escribirla ella misma.

El soporte del compilador para macros de procedimiento, así como las excelentes bibliotecas que lo acompañan, como syn y quote, hacen que escribirlas en Rust sea sencillo. Las macros de procedimiento son una de las muchas razones por las que estoy feliz de haber elegido Rust. Ya los hemos aprovechado para una variedad de usos, y estoy seguro de que seguiremos encontrando nuevos usos.

¡También! ¡Marque sus calendarios! Únase a nosotros en Qumulo en Martes, agosto 13 en 6: 30 pm para Meetup mensual de Seattle Rust. ¡Se proporcionará comida y bebida!

Collin Wallace de Qumulo discutirá lo siguiente: “Qumulo tenía una gran base de código C con un poco de magia para permitir métodos, interfaces y una forma limitada de rasgos / genéricos. Ahora tenemos una base de código C + Rust mixta, y hemos sido exigentes para que el nuevo código de Rust sea idiomático * sin * sentirnos fuera de lugar con el código C con el que se vincula. En esta charla exploraré el camino que usamos para lograr esto, que aprovecha macros de procedimiento, complementos del compilador, encabezados C generados automáticamente y algunos enfoques bien pensados ​​para las pruebas ".

Contáctanos aquí si quieres programar una reunión o solicitar una demostración. Y suscríbase a nuestro blog para obtener mejores prácticas y recursos más útiles.

0 0 votos
Valoración del artículo
Suscríbete
Notificarme sobre
invitado
0 Comentarios
Más antiguo
Más Nuevos Más votados
Comentarios en línea
Ver todos los comentarios

Artículos Relacionados

0
Me encantaría tus pensamientos, por favor comenta.x
Ir al Inicio