1. Getting Started
- 使用
rustup安装,默认安装在$HOME/.cargo和$HOME/.rustup下 rustup updaterustup self uninstall- cargo 常用命令
cargo new PROJECTcargo build [--release]cargo run
2. Programming a Guessing Game
Cargo.toml里依赖部分的版本号 “MAJOR.MINOR.PATCH” 实际是 “^MAJOR.MINOR.PATCH” 的简写,表示允许语义版本的升级cargo update默认只升级 PATCH 部分cargo doc --open查看文档
3. Common Programming Concepts
- 不可变变量:
let VAR: TYPE = VALUE; - 可变变量:
let mut VAR: TYPE = VALUE; - 常量:
const FOO_BAR: TYPE = VALUE; - Shadowing: 同一作用域里,同名变量可以重复声明,之前声明的变量被遮蔽。
- Scalar types:
- Integer types: i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize,后两者长度跟机器相关,一般用于集合、数组的索引和长度。Debug 模式下溢出回绕会panic,Release模式下溢出回绕不报错。使用标准库里类型 Wrapping 显示表明期望溢出回绕行为。
- Integer literals: 98_222, 0xff, 0o77, 0b1111_0000, b’A’,除了 byte literal,其它字面量都支持类型后缀,比如 57u8 表示一个 u8 类型的值 57。整型字面量默认类型为 i32。
- Floating-point types: f32, f64,默认为 f64。
- Boolean type: bool, true, false。布尔类型长度为一字节。
- Character type: char,四个字节,表示一个 Unicode Scalar Value, 范围为 [U+0000, U+D7FF] 和 [U+E000, U+10FFFF]。
- Compound Types
- Tuple: (x, y, z),类型声明 (t1, t2, t3)
- Array: [x, y, z],类型声明 [t; size]。 使用语法 [x; n] 创建 n 个 x 值的数组。数据访问会检查是否越界。
- Functions:
fn foo(x:t1, y:t2) -> t3 { ... } - Control flow:
if condition { ... } else if condition { ... } else { ... }loop { .... break VALUE; ... }while condition { ... }for VAR in ITER { ... }
4. Understanding Ownership
Ownership rules:没有实现 Copy trait 的类型,变量赋值时是 move 语义,owner 超出作用域时自动调用 drop()
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Reference: borrow 语义
- 多个只读引用可以同时指向一个变量
- 同时只能有一个可写引用指向一个变量,不能有其它只读或者可写引用指向同一个变量
- Non-Lexical Lifetimes(NLL): 引用的作用域从声明开始,直到它最后一次被使用位置,并不是按照词法作用域判断
Slice: borrow 语义
&s[i..j]表示[i, j)范围&str表示 string slice,定义字符串函数时,经常使用&str作为参数类型,以同时支持 String 和 &str- slice 记录第一个元素的引用,以及一个长度字段
5. Using Structs to Structure Related Data
定义结构体:
struct Xxx { f1: t1, f2: t2 }新建实例:
Xxx { f1: v1, f2: v2 }当变量名与字段名同名时可以简写
Xxx { f1, f2: v2}等价于Xxx { f1: f1, f2: v2}。根据已有结构体实例修改部分字段以创建新实例:
Xxx { f1: v1, ..xxx},xxx 为已有实例Tuple Struct:
struct Point(i32, i32, i32)可以使用point.0,point.1来引用字段。Unit-like struct:
(),没有任何字段。在结构体定义时使用 annotation
#[derive(Debug)]可以让它能被println!的{:?}或者{:#?}置位符输出。定义方法:
impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }同一个结构体可以有多个
impl块。
6. Enums and Pattern Matching
定义:
enum Message { Quit, Move { x: i32, y: i32 }, // 匿名 struct Write(String), ChangeColor(i32, i32, i32), }enum 跟 struct 一样,也可以用
impl定义 methodOption类型包含在 prelude 中,可以直接使用Option甚至它的 variantsSome和None。enum Option<T> { Some(T), None, }match 表达式:
match expr { pattern => ..., _ => ..., }if let 表达式:
if let pattern = expr { ...; } else { ...; }
7. Managing Growing Projects with Packages, Crates, and Modules
Rust module system:
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
一个 package 的目录结构如下,src/lib.rs 和 src/main.rs 的 crate name 与 package 同名
package/ 一个 package 最多有一个 library crate,可以有多个 binary crate ├── Cargo.toml └── src ├── bin │ ├── bar.rs # 额外的 binary crate,隐含 module "crate",此文件称为 crate root │ └── foo.rs # 额外的 binary crate,隐含 module "crate",此文件称为 crate root ├── lib.rs # 默认的 library crate,隐含 module "crate",此文件称为 crate root └── main.rs # 默认的 binary crate,隐含 module "crate",此文件称为 crate rootmodule tree 和 directory tree 不是一一对应的,一个
.rs文件里可以定义平行或者嵌套的多个 module。module tree 里的 path 规则:- 绝对路径:当前 crate
crate::...,其它 cratesome_crate::... - 相对路径:
self::...,super::...,some_identifier
- 绝对路径:当前 crate
符号的可见性
- 所有符号,包括 mod, struct, enum, fn, const 默认都是私有的,需要用 pub 修饰符公开
- child mod 可以看到 parent mod 的所有符号,parent mod 只能看到 child mod 公开的符号
- pub struct 的 field 需要额外加 pub 才是公开的,而 pub enum 的 variants 都是公开的
使用
use导入 path,类似于文件系统中ln -s PATH .- 习惯上,导入 function 时,导入到 mod 级别,使用
some_mod::some_fn调用,以容易识别函数来自哪个模块 - 习惯上,导入 struct 和 enum 时,导入到符号本身级别,比如
use std::collectioons::HashMap; - 使用
as关键字导入为别名:use std::io::Result as IoResult; use导入的符号默认为私有,使用pub use达到 re-exporting 为公开的效果。- Nested paths:
use std::{cmp::Ordering, io};等价于use std::cmp::Ordering; use std::io;,use std::io::{self, Write};等价于use std::io; use std::io::Write; - 引入一个 mod 里的所有公开符号:
use std::collections::*;,一般用于单元测试代码里。
- 习惯上,导入 function 时,导入到 mod 级别,使用
切分 modules 到不同文件
mod xxx;等价于mod xxx { ...加载 module/path/to/xxx.rs 文件内容.... }- 在 crate root 里,
mod xxx; 等价于mod xxx { ...load xxx.rs... } - 在 crate::foo::bar 里,
mod xxx;等价于mod xxx { ... load foo/bar/xxx.rs... }
- 在 crate root 里,
use只是做符号连接,并不会加载模块
8. Common Collections
Vec<T>: 创建实例Vec::new(),vec![1, 2, 3, 4]v[i]在 i 越界时会 panic,v.get(i)返回Option<T>,因此在越界时返回 None。字符串的
+调用的fn add(self, s: &str) -> String,第一个参数的所有权会被拿走,后面的参数必须是引用。format!(...)拼接字符串,不拿走任何参数的所有权。字符串的
len()返回的是「字节数」不是「字符数」! 使用s.chars().count()可以拿到 unicode scalar value 个数,而通常人为感知的「字符」指 grapheme cluster,需要用第三方库处理。HashMap:
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); // 如果已经存在,则会覆盖 let team_name = String::from("Blue"); let score = scores.get(&team_name); // 返回 Option<&V> for (key, value) in &scores { ... } // 不存在时才插入,返回 &mut V let score = scores.entry(String::from("Blue")).or_insert(50); *score += 10;HashMap 默认使用 cryptographically strong SipHash,如果感觉性能更重要,可以换用其它 hasher。
9. Error Handling
不可恢复错误:
panic!,默认使用 unwind 策略,释放栈上的对象,在 Cargo.toml 里使用 profile 换成 abort 策略,也即立即退出。[profile.release] panic = 'abort'使用
RUST_BACKTRACE=1环境变量在 panic 时打印调用栈可恢复错误:
enum Result<T, E> { Ok(T), Err(E), }?操作符:someResult?表示如果是 Ok 则返回 T,如果是 Err,则转换成当前函数返回类型(必须是Result)并返回。
10. Generic Types, Traits, and Lifetimes
struct, enum, fn 都可以使用泛型,在类型或者函数名字后面使用
<T1, T2, ...>添加泛型参数。impl<T> Point<T> { ... }中 impl 后的泛型参数,表示Point<T>里的T是泛型参数,而不是具体参数。Trait 很像其它语言的 interface,支持默认实现。
// 定义 trait trait Summary { fn summarize(&self) -> String; fn abstract(&self) -> String { String::from("....") } } // 为某个 struct or enum 实现 trait impl Summary for Tweet { fn summarize(&self) -> String { format!("...", ...) } } // 要求函数参数满足某个 trait,这种写法一般用在参数个数少的情况 fn notify(item: impl Summary) { .... } // 更通用的 Trait 约束语法 fn notify<T: Summary>(item: T) { ... } // 满足多个 trait fn notify(item: impl Summary + Display) { ... } fn notify<T: Summary + Display>(item: T) { ... } fn notify<T>(item: T) where T: Summary + Display { ... } // 更容易阅读函数签名 // 函数返回值满足某个 trait,注意由于 Rust 编译器实现限制, // impl Summary 返回值约束下,函数只能固定返回某种固定的具体类型! fn return_summarizable() -> impl Summary { ... }利用 trait bound 来条件化的实现方法
use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y, } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } }blanket implentation: 为满足某个 trait bound 的所有类型实现 method。
impl<T: Display> ToString for T { fn to_string(&self) -> String { ... } }Lifetime 也是一种泛型参数,修饰于引用类型上,比如
&'a TYPE,lifetime 名字紧接&后面,以单引号开头,任意字符串作为名字,习惯上跟泛型类型参数一样用单个字符。作为泛型参数的一种,其也需要在 struct, enum, fn 的名字后面用<'a, 'b>这样指定上, 对于impl<'a, 'b>'也如此。fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... } struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { ... }函数签名 lifetime 省略的三条规则(Rust 编译器以后可能加入更多规则以方便代码编写)。当 lifetime annotation 没有针对函数的输入参数(对应input lifetime parameter)和返回值(对应output lifetime parameter) 指定时:
- 每个没有指定 lifetime 的输入参数获得自己独有的 lifetime
- 如果只有一个输入参数,且返回值没有指定 lifetime,则返回值和输入参数有同样的 lifetime
- 如果第一个参数是
&selfor&mut self(意味着这个函数是个method),且返回值没有指定 lifetime,则返回值和 self 有同样的 lifetime
'static称为静态生命周期,指代一个引用在整个程序运行期间都有效,比如字面字符串的引用,其隐含了'static生命周期。注意编译器在提示 “lifetime'staticrequired” 时往往是错的,程序员应该正确指定合适的 lifetime。
11. Writing Automated Tests
单元测试代码示例:
#[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "...some error message...")] fn it_works() { assert_eq!(2 + 2, 4); } }assert!,assert_eq!,assert_ne!都可以接受额外的错误消息模板以及参数,类似format!测试函数可以返回 Result<(), String>,成功时返回
Ok(()),失败时返回Err(String::from("..."))对于 library crate,
tests/目录下每个.rs文件被当作一个 crate,cargo test会执行每个文件里的测试函数。通用的 setup 代码可以放到tests/common/mod.rs里,不要放到tests/common.rs因为这样会被当作一个集成测试文件。use adder; mod common; #[test] fn it_adds_two() { common::setup(); assert_eq!(4, adder::add_two(2)); }
12. An I/O Project: Building a Command Line Program
std::env::args()需要命令行参数是合法的 Unicode,否则会 panic。对非 Unicode 字符集使用std::env::args_os()std::fs操作文件std::process::exit(i32)退出进程可能遇到任意错误的返回值:
use std::error::Error; fn main() -> Result<(), Box<dyn Error>> { ... }std::env::var(KEY)获取环境变量eprintln!(...)输出到 stderr
13. Functional Language Features: Iterators and Closures
Closure 语法:
fn add_one_v1 (x: u32) -> u32 { x + 1 } let add_one_v2 = |x: u32| -> u32 { x + 1 }; let add_one_v3 = |x| { x + 1 }; let add_one_v4 = |x| x + 1 ;Closure 的参数、返回值类型在第一次使用 closure 时才推断确定。
Closure 在实现时,实际是创建了一个匿名 struct,实现了 trait
FnOnce,FnMut,Fn中的一个,并保存了捕获的上下文变量。在 closure 参数列表前加
move表示显式的将捕获的环境变量的所有权转移到 closure 上,这种用法一般用在把 closure 传递给另一个线程时。let x = vec![1, 2, 3]; let equal_to_x = move |z| z == x;Iterator trait:
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided }Iterator 的三个构造方法:
- iter(): return immutable reference
- iter_mut(): return mutable reference
- into_iter(): take ownership and return owned value
14. More About Cargo and Crates.io
crate 级别(src/lib.rs) 开头以及 module 开头,使用
//! ...编写 Markdown 文档struct, enum, fn 之类前面使用
/// ...编写 Markdown 文档,一般包含:# Examples: 使用 ``` 包围的代码段,在执行cargo test时会被当成测试用例执行# Panics: 是否调用了 panic!# Errors: 对 Result 返回值什么情况下返回 Err 的阐述# Safefy: 如何使用 unsafe
Cargo workspace: 在顶层目录创建 Cargo.toml,然后在此目录下再
cargo new创建 package,所有 package 共享顶层目录的 Cargo.lock,但各自的 Cargo.toml 完全独立。[workspace] members = [ "pkg1", "pkg2" ]Cargo workspace 里 package 之间的 library crate 依赖:
# pkg1/Cargo.toml [dependencies] pkg2 = { path = "../pkg2" }使用
cargo run -p PKG和cargo test -p PKG局限于单个 package。使用
cargo install PKG安装 binary cratePATH 里带有
cargo-前缀的命令都自动成为cargo的子命令,可以用这个方式扩展 Cargo。
15. Smart Pointers
smart pointers 指实现了 trait
Deref和Drop的 struct,从这个意义上讲,String和Vec也是。Box<T>: 在堆上分配内存Rc<T>: 引用技术,支持多个 ownerRef<T>,RefMut<T>使用RefCell<T>访问,运行时应用 borrowing 规则
实现一个智能指针
use std::ops::Deref; struct MyBox<T>(T); // 定义 tuple struct impl<T> Deref for MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } type Target = T; // 需要类型 fn deref(&self) -> &T { &self.0 // 返回 tuple 第一个元素的引用 } }创建 Box
: let x = Box::new(String::from("xxx"));隐式的 Deref Coercion:
- From
&Tto&UwhenT: Deref - From
&mut Tto&mut UwhenT: DerefMut - From
&mut Tto&UwhenT: Deref
- From
使用
drop函数提前显式地析构对象。引用计数
use std::rc::Rc; let x = Rc::new(String::from("xxx")); let y = Rc::clone(&x); println!("y's strong_count={}", Rc::strong_count(&y));Rc
只能用在单线程里,并且只能共享不可变引用。std::cell::RefCell 也只能用在单线程。 interior mutability指不可变的类型内部通过 unsafe 代码可以安全的修改数据,RefCell是一个典型的例子。The reasons to choose
Box,Rc, orRefCell:Rcenables multiple owners of the same data;BoxandRefCellhave single owners.Boxallows immutable or mutable borrows checked at compile time;Rcallows only immutable borrows checked at compile time;RefCellallows immutable or mutable borrows checked at runtime.- Because
RefCellallows mutable borrows checked at runtime, you can mutate the value inside theRefCelleven when theRefCellis immutable.
RefCell<T>::borrow()返回Ref<T>,RefCell<T>::borrow_mut()返回RefMut<T>,两个函数可以当作&和&mut看待。Rc<T>用于多个 owner 对同一个数据的只读访问,Rc<RefCell<T>>用于多个 owner 对同一个数据的可写访问。Rc::downgrade(&Rc<T>)得到Weak<T>引用,用Rc::upgrade(&Weak<T>)得到Option<Rc<T>>。
16. Fearless Concurrency
新建线程:
use std::thread; fn main() { let handle = thread::spawn(move || { ... }); handle.join().unwrap(); }使用 multiple producer, single consumer channel 跨线程通讯
use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); // tx 指 transmiter, rx 指 reciever let tx2 = mpsc::Sender::clone(&tx); // multiple producer thread::spawn(move || { tx.send(String::from("hello")).unwrap(); thread::sleep(Duration::from_secs(1)); }); thread::spawn(move || { tx2.send(String::from("world")).unwrap(); thread::sleep(Duration::from_secs(1)); }); let received = rx.recv().unwrap(); // 阻塞式 recv(), 非阻塞式 try_recv() println!("Got: {}", received); for received in rx { // 迭代器方式 println!("Got: {}", received); } }类似
Rc<RefCell<T>>达到同一个线程里多个owner对同一个数据的内部修改性,使用Arc<Mutex<T>>达到多个线程对同一个数据的互斥修改:use std::sync::{Arc, Mutex}; use std::thread; fn main() { // Arc: // atomically reference counted type let counter = Arc::new(Mutex::new(2i64)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 2; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("result={}", counter.lock().unwrap()); }Trait
std::marker::Send表示一个类型的 ownership 是否可以安全的转到另一个线程里。- 除了原始指针外,其它基本类型都是
Send。 - 成员全是
Send的复合类型也是Send。 Rc<T>和RefCell<T>不是Send,不能跨线程传递。
- 除了原始指针外,其它基本类型都是
Trait
std::marker::Sync表示一个类型是否可以安全的从多个线程里引用。- 如果
&T是Send,那么T是Sync。 - 成员全是
Sync的复合类型也是Sync。 - 基本类型是
Sync。 Rc<T>和RefCell<T>不是 Sync。
- 如果
17. Object Oriented Programming Features of Rust
- Rust 的 struct 成员默认是私有的,只能通过方法访问,因此 Rust 具备 OOP 里的封装语义;
- Rust 不支持 OOP 风格的继承来重用代码,只能通过 trait 里的默认方法来实现有限的继承语义;
- trait object 使用
dyn SomeTrait声明,在使用时需要是引用或者智能指针,比如Vec<Box<dyn Error>>。 - trait object 用来实现 OOP 的运行时多态,采用 dymaic dispatch,也即运行时才能知道调用哪个方法。
- trait object 需要 trait 满足 object safety 约束,最重要的两条:
- trait 里的方法不能返回
Self类型,比如dyn Clone就不能作为 trait object 使用 - trait 里不能有泛型参数
- trait 里的方法不能返回
18. Patterns and Matching
可以使用模式匹配的地方:
match VALUE { PATTERN => EXPRESSION, PATTERN => EXPRESSION, PATTERN => EXPRESSION, } if let PATTERN = EXPRESSION { } while let PATTERN = EXPRESSION { } for PATTERN in EXPRESSION { } let PATTERN = EXPRESSION; fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({}, {})", x, y); }PATTERN 的语法
P1 | P2 表示「或」 1..=5 表示闭区间,只适用于整数和char let Point {x: a, y: b} = Point {x: 1, y: 2}; let Point {x, y} = Point {x: 1, y: 2}; _ 忽略 _x 绑定,忽略未使用变量 .. 忽略剩余部分 // match guard match VALUE { PATTERN if CONDITION => EXPRESSION, } // 使用 @ 在匹配时赋值 enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7 } => { println!("Found an id in range: {}", id_variable) }, Message::Hello { id: 10..=12 } => { println!("Found an id in another range") }, Message::Hello { id } => { println!("Found some other id: {}", id) }, }
19. Advanced Features
unsafe可以获得额外的能力:- Dereference a raw pointer
- Call an unsafe function or method
- Access or modify a mutable static variable
- Implement an unsafe trait
- Access fields of
unions
trait 关联类型,默认泛型参数,操作符重载:
use std::ops::Add; // std::ops 中的操作符可以重载 /* trait Add<RHS=Self> { // 默认泛型参数 type Output; // 关联类型 fn add(self, rhs: RHS) -> Self::Output; } */ #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } }使用方法全名来区分 struct 以及实现的 trait 的同名函数
struct Dog; trait Animal { ... } let dog = Dog; dog.method() // 调用 Dog struct 上直接 impl 的方法 fn method(&self) Animal::method(&dog) // 调用 Dog struct 上 impl Animal 的方法 fn method(&self) Dog::func() // 调用 Dog struct 上直接 impl 的函数 fn func() <Dog as Animal>::func() // 调用 Dog struct 上 impl Animal 的函数 fn func()supertrait
trait B: A { ... } // 实现了 trait B 的 struct 必须也要实现 trait Aimpl Trait on Type只有当 Trait 和 Type 至少有一个是当前 crate 定义的时才被允许,如果 Trait 和 Type 都是外部 crate 的,要想扩展 Type 则需要使用 newtype 模式,也就在包装的 tuple struct 类型上实现 trait,Rust 编译器在编译时会自动消除这层包装。如果想要 wrapper 类型包含目标类型的所有方法,则可以实现Dereftrait。use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); }type alias:
type Result<T> = std::result::Result<T, std::io::Error>;never type:
!表示 empty type,比如在 match 的某个分支里使用continue或者panic!,这个分支的返回值类型就是!,比如在函数里有无限循环loop { ... },此函数的返回值类型就是!。trait
Sized表示编译时知道类型大小,特殊语法?Sized表示可能知道也可能不知道类型大小。fn foo<T>(t: T) {} // 相当于 fn foo<T: Sized>(t: T) {} fn bar<T: ?Sized>(t: &T) {} // 由于 T 的大小未知,所以用 &T 类型作为参数,也可以用 Box<T>function pointer:
fn(n: i32) -> i32,可以省略参数名字,function pointer 实现了 closure traitFn,FnMut,FnOnce,所以在以函数作为参数时,推荐使用 closure trait 以支持 function pointer 和 closure 两种。但在与 C 语言交互时,只能使用 function pointer,因为 C 语言不支持 closure。fn foo(f: impl Fn(i32) -> i32, n: i32) -> i32 { f(n) } // 返回 closure: // 第一种返回 trait object,允许函数里不同分支返回不同具体类型的closure 或者函数指针; // 第二种和第三种类似,只能返回一种具体类型 // 由于 Box 实现了 Deref trait,所以使用上都可以写 foo()(12) fn foo() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + x) } fn foo() -> impl Fn(i32) -> i32 { Box::new(|x| x + x) } fn foo() -> impl Fn(i32) -> i32 { |x| x + x }declarative macro, aka macro by example, aka pattern macro
procedural macro, aka syntax extension, aka compiler plugin
- custom
#[derive]macro - attribute-like macro
- function-like macro
- custom
Cheat sheets: