Upgrade to Pro — share decks privately, control downloads, hide ads and more …

proc-macro の勉強して sqlx のバグを修正した話

proc-macro の勉強して sqlx のバグを修正した話

Southball

July 26, 2023
Tweet

Other Decks in Programming

Transcript

  1. © 2023 estie Inc. @southball02 SWE@estie • ソフトウェアエンジニア(SWE) • 京都大学工学部情報学科3年

    • estieでは業務委託としてRustを使ったWebアプリの開 発に取り組んでいる 自己紹介 2
  2. © 2023 estie Inc. procedural macro? • マクロの一種 • 抽象構文木(AST)から抽象構文木への変換を定義する

    • 三種類ある: • #[proc_macro] → println!("...") • #[proc_macro_attribute] → #[test] • #[proc_macro_derive] → #[derive(Clone)] • 例えば sqlx は procedural macro でデータベースに接続して、クエリの型を確 認する
  3. © 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"); } } ?
  4. © 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::<TokenStream>(), 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::<TokenStream>(), ] .into_iter() .collect::<TokenStream>(), )) .into() } *あとでわかりやすい書き方説明するので あんまり気にしなくて大丈夫です
  5. © 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
  6. © 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"); }
  7. © 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
  8. © 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() }
  9. © 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]) }
  10. © 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; }
  11. © 2023 estie Inc. error[E0308]: mismatched types --> mod.rs:8:16 |

    8 | let data = sqlx::query_as!(Struct, "...") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, Error>`, found `Result<Struct, ...>`
  12. © 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 = <sqlx::mysql::MySql as ::sqlx::database::HasArguments>::Arguments::default(); ::sqlx::query_with::<sqlx::mysql::MySql, _>("...", query_args) .try_map(|row: sqlx::mysql::MySqlRow| { use ::sqlx::Row as _; let sqlx_query_as_a = row .try_get_unchecked::<i64, _>(0usize)?; Ok(Struct { a: sqlx_query_as_a }) }) } } } .fetch_all(conn) .await; } cargo-expand
  13. © 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 = <sqlx::mysql::MySql as ::sqlx::database::HasArguments>::Arguments::default(); ::sqlx::query_with::<sqlx::mysql::MySql, _>("...", query_args) .try_map(|row: sqlx::mysql::MySqlRow| { use ::sqlx::Row as _; let sqlx_query_as_a = row .try_get_unchecked::<i64, _>(0usize)?; Ok (Struct { a: sqlx_query_as_a }) }) } } } .fetch_all(conn) .await; } cargo-expand
  14. © 2023 estie Inc. proc-macro の勉強して sqlx のバグを修正した話 まとめ •

    proc macro の基本の作り方の紹介をした • proc_macro, proc_macro2 で抽象構文木上に操作する • syn で Rust の構文解析する • quote で Rust のコードを抽象構文木に変換する • マクロの実装を見る • cargo-expand でマクロを展開する • 作ってみよう!