macros are a way of writing a code that writes other codes. also know as meta programming.
example:
macro_rules! gcd {
($a: expr, $b: expr) => {{
let mut m = $a;
let mut n = $b;
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
n
}};
}
used to generate code at compile time by taking aa piece of code as input and returning a transformed output
theres 3 types of macros:
- function like macro: used to defined functions that takes in a set of args and generate a block of code hat is included in the program
- attribute: used to add metadata to items in rust like structs, enums.
- derive macros: automatically implement traits on structs or enums
TokenStreams: are types that represents a sequence of tokens, used to represent the output of a procedural macro. Quote: is a macro that can generate a token stream from a syntax tree. allows you to construct rust code using rust syntax syn create: third party rust parser that passes a rust code into a syntax that can be manipulated by a procedural macro
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
# [proc_macro_attribute]
pub fn debug_print(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut item_fn = parse_macro_input!(item as ItemFn);
let ident = &item_fn.sig.ident;
item_fn.block.stmts.insert(
0,
syn::parse_quote!(println!("entering the function: {}", stringify!(#ident));
),
);
TokenStream::from(quote! {
#item_fn
})
}
declarative macros are used to match a given pattern and replace it with the specified code.
they can be used to define new controls structures or simplify repetitive code.
use macros::debug_print;
macro_rules! average {
($(,)*) => {
0.0
};
($($val:expr), + $(,)*) => {{
let count = 0usize $(+ {let _ = stringify!($val); 1})*;
let sum = 0.0 $(+ $val as f64)*;
sum / count as f64
}};
}
declarative macro uses a set of rules to match and transform input tokens to output tokens. similar to regex that matches patterns and replaces with new patterns. they are defined using macro_rules
and are evaluated compile time.
function like macro are also evaluated at compile time. operates like a regular function and accepts input arguments and returns output tokens. they can do more complex transformations and more flexible.
unsafe code allows us to bypass rust memory safety check in our code that are enforced during compile time.
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is {:?}", *r1);
println!("r2 is {:?}", *r2);
}
-
when should i use
unsafe code
: situations where the safe abstractions provided by the language are not sufficient for the task at hand. example when interfacing with low level system api, hardware level tasks or implementing performant algorithms. -
interfacing with c libraries
-
doing low level programming (where you need direct access to memory)
-
performance critical algorithm
-
custom data structure.
-
risks: can lead to undefined behaviors and memory safe issues.