Slide 1

Slide 1 text

proc-macro の勉強して sqlx のバグを修正した話 2023-07-26 Rust、何もわからない... #9

Slide 2

Slide 2 text

© 2023 estie Inc. @southball02 SWE@estie • ソフトウェアエンジニア(SWE) • 京都大学工学部情報学科3年 • estieでは業務委託としてRustを使ったWebアプリの開 発に取り組んでいる 自己紹介 2

Slide 3

Slide 3 text

© 2023 estie Inc. 今日やること • マクロを触ってみる • ざっと説明するので、気になる人はこのあとゆっくり読んでください • 実際ライブラリの内部を見て、バグを修正してみる

Slide 4

Slide 4 text

© 2023 estie Inc. procedural macro? • マクロの一種 • 抽象構文木(AST)から抽象構文木への変換を定義する • 三種類ある: • #[proc_macro] → println!("...") • #[proc_macro_attribute] → #[test] • #[proc_macro_derive] → #[derive(Clone)] • 例えば sqlx は procedural macro でデータベースに接続して、クエリの型を確 認する

Slide 5

Slide 5 text

© 2023 estie Inc. © 2023 estie Inc. 初めてのproc_macro 1

Slide 6

Slide 6 text

© 2023 estie Inc. use proc_macro_test::print_twice; #[test] fn test() { print_twice!("Hello, {}!", "world"); } use proc_macro_test::print_twice; #[test] fn test() { { println!("Hello, {}!", "world"); println!("Hello, {}!", "world"); } } ?

Slide 7

Slide 7 text

© 2023 estie Inc. # Cargo.toml [package] name = "proc-macro-test" version = "0.1.0" edition = "2021" [lib] proc-macro = true [dependencies] // lib.rs use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}; #[proc_macro] pub fn print_twice(input: proc_macro::TokenStream) -> proc_macro::TokenStream { TokenTree::from(Group::new( Delimiter::Brace, vec![ vec![ TokenTree::from(Ident::new("println", Span::call_site())), TokenTree::from(Punct::new('!', Spacing::Alone)), TokenTree::from(Group::new(Delimiter::Parenthesis, input.clone())), ] .into_iter() .collect::(), TokenTree::from(Punct::new(';', Spacing::Alone)).into(), vec![ TokenTree::from(Ident::new("println", Span::call_site())), TokenTree::from(Punct::new('!', Spacing::Alone)), TokenTree::from(Group::new(Delimiter::Parenthesis, input.clone())), ] .into_iter() .collect::(), ] .into_iter() .collect::(), )) .into() } *あとでわかりやすい書き方説明するので あんまり気にしなくて大丈夫です

Slide 8

Slide 8 text

© 2023 estie Inc. $ cargo +nightly test -- --nocapture Compiling proc-macro-test v0.1.0 (/home/southball/proc-macro-test) Finished test [unoptimized + debuginfo] target(s) in 0.15s Running unittests src/lib.rs (target/debug/deps/proc_macro_test-03e1a98fcf1cb3da) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/test.rs (target/debug/deps/test-0863b971a4792ac0) running 1 test Hello, world! Hello, world! test test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests proc-macro-test running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Slide 9

Slide 9 text

© 2023 estie Inc. TokenStream とは? TokenStream [ Literal { kind: Str, symbol: "Hello, {}!", suffix: None, span: #0 bytes(71..83), }, Punct { ch: ',', spacing: Alone, span: #0 bytes(83..84), }, Literal { kind: Str, symbol: "world", suffix: None, span: #0 bytes(85..92), }, ] use proc_macro_test::print_twice; #[test] fn test() { print_twice!("Hello, {}!", "world"); }

Slide 10

Slide 10 text

© 2023 estie Inc. proc_macro2 + quote #[proc_macro] pub fn print_twice(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = proc_macro2::TokenStream::from(input); quote::quote! { { println!( #input ); println!( #input ); } } .into() } proc_macro2: proc macro を定義する crate 以外で proc_macro ライブラリの使うための crate quote: Rust を抽象構文木に変換する crate

Slide 11

Slide 11 text

© 2023 estie Inc. syn crate AST 上に構文解析をして、Rust の ASTに変換するライブラリ

Slide 12

Slide 12 text

© 2023 estie Inc. syn crate:例 // test.rs use proc_macro_test::print_rust_ast; #[test] fn test() { print_rust_ast!(1 + 2 * 3); } Expr::Binary { attrs: [], left: Expr::Lit { attrs: [], lit: Lit::Int { token: 1, }, }, op: BinOp::Add( Plus, ), right: Expr::Binary { attrs: [], left: Expr::Lit { attrs: [], lit: Lit::Int { token: 2, }, }, op: BinOp::Mul( Star, ), right: Expr::Lit { attrs: [], lit: Lit::Int { token: 3, }, }, }, } # cargo.toml [package] name = "proc-macro-test" version = "0.1.0" edition = "2021" [lib] proc-macro = true [dependencies] quote = "1.0" proc-macro2 = "1.0“ syn = { version = "2.0", features = ["extra-traits"] } // lib.rs use proc_macro2::{Literal, TokenTree}; #[proc_macro] pub fn print_rust_ast(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = TokenTree::from( Literal::string(&format!( "{:#?}", syn::parse_macro_input!(input as syn::Expr) ) )); quote::quote! { println!( "{}", #input ); } .into() }

Slide 13

Slide 13 text

© 2023 estie Inc. cargo expand $ cargo expand --test test Checking proc-macro-test v0.1.0 (/home/southball/proc-macro-test) Finished dev [unoptimized + debuginfo] target(s) in 0.03s ... fn test() { { { ::std::io::_print(format_args!("Hello, {0}!¥n", "world")); }; { ::std::io::_print(format_args!("Hello, {0}!¥n", "world")); }; }; } #[rustc_main] pub fn main() -> () { extern crate test; test::test_main_static(&[&test]) }

Slide 14

Slide 14 text

© 2023 estie Inc. © 2023 estie Inc. sqlx のバグ修正の話 2

Slide 15

Slide 15 text

© 2023 estie Inc. use anyhow::Ok; use sqlx::MySqlConnection; // ... struct Struct { a: i32 } async fn handler(conn: &mut MySqlConnection) { let data = sqlx::query_as!(Struct, "...") .fetch_all(conn) .await; }

Slide 16

Slide 16 text

© 2023 estie Inc. error[E0308]: mismatched types --> mod.rs:8:16 | 8 | let data = sqlx::query_as!(Struct, "...") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, Error>`, found `Result`

Slide 17

Slide 17 text

© 2023 estie Inc. ???

Slide 18

Slide 18 text

© 2023 estie Inc. use anyhow::Ok; use sqlx::MySqlConnection; // ... struct Struct { a: i32 } async fn handler() { let data = { { #[allow(clippy::all)] { use ::sqlx::Arguments as _; let query_args = ::Arguments::default(); ::sqlx::query_with::("...", query_args) .try_map(|row: sqlx::mysql::MySqlRow| { use ::sqlx::Row as _; let sqlx_query_as_a = row .try_get_unchecked::(0usize)?; Ok(Struct { a: sqlx_query_as_a }) }) } } } .fetch_all(conn) .await; } cargo-expand

Slide 19

Slide 19 text

© 2023 estie Inc. use anyhow::Ok; use sqlx::MySqlConnection; // ... struct Struct { a: i32 } async fn handler() { let data = { { #[allow(clippy::all)] { use ::sqlx::Arguments as _; let query_args = ::Arguments::default(); ::sqlx::query_with::("...", query_args) .try_map(|row: sqlx::mysql::MySqlRow| { use ::sqlx::Row as _; let sqlx_query_as_a = row .try_get_unchecked::(0usize)?; Ok (Struct { a: sqlx_query_as_a }) }) } } } .fetch_all(conn) .await; } cargo-expand

Slide 20

Slide 20 text

© 2023 estie Inc. THIS IS (NOT) OK

Slide 21

Slide 21 text

© 2023 estie Inc.

Slide 22

Slide 22 text

© 2023 estie Inc.

Slide 23

Slide 23 text

© 2023 estie Inc. proc-macro の勉強して sqlx のバグを修正した話 まとめ • proc macro の基本の作り方の紹介をした • proc_macro, proc_macro2 で抽象構文木上に操作する • syn で Rust の構文解析する • quote で Rust のコードを抽象構文木に変換する • マクロの実装を見る • cargo-expand でマクロを展開する • 作ってみよう!