最近几天在弄 ddnspod 的时候,写了个宏: custom_meta_struct
解决什么问题
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct ActionA { url: String, // https://example.com version: String, // v1.2.3 a: u64, // ... } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[some custome attribute] // 这个 action 独有 attribute struct ActionB { url: String, // https://example.com version: String, // v1.2.3 b: bool, // ... } // 后面很多的 Action // ...
上面代码中有很多个 struct Action
每一个 Action 都有一些像 #[derive(Debug)] 这样的共同的 Attributes
每个 struct 内同样也都有像 url version 这样相同的 fields
并且大部分的值都相同, 此时我该如何利用 macro 来减少重复代码的编写?
custom_meta_struct! {}
我的 custom_meta_struct 就是专门来干这个活儿的
简单用法
custom_meta_struct! { ( #[derive(Debug)] #[derive(Clone)] ), struct A; #[derive(Copy)] struct B; }
这段代码展开后会变成这样:
#[derive(Debug)] #[derive(Clone)] struct A; #[derive(Debug)] #[derive(Clone)] #[derive(Copy)] struct B;
复杂点的用法
对于 url version 也避免重复的用法:
首先定一个 trait
trait CommonParams { fn url(&self) -> String { "https://hangj.cnblogs.com" } fn version(&self) -> String { "v1.2.3".into() } }
然后让所有的 Action 都
impl CommonParams for ActionX { // 如果这个 Action 的 url 或 version 比较特殊, 就重载一下 }
具体解法:
custom_meta_struct! { ( define_structs, // callback macro #[derive(Debug)] ), #[derive(Clone)] struct A; @[version = "v2.3.4".into()] #[derive(serde::Serialize)] struct B; @[url = "https://crates.io/crates/ddnspod".into()] struct C; }
其中的 define_structs 也是一个宏, 用来作为回调, custom_meta_struct 会对将要展开的代码做一个格式化, 代码格式化之后传递给 define_structs
@[..] 是我们的自定义属性, 用来辅助实现 trait CommonParams 内函数重载的
接下来看具体实现:
macro_rules! define_structs { ( $( $(#[$meta: meta])* $(@[$($my_meta: tt)*])* $vis: vis struct $name: ident $body: tt )* ) => { $( $(#[$meta])* $vis struct $name $body impl CommonParams for $name { $( overriding_method!( $($my_meta)* ); )* } )* }; }
overriding_method 也是一个宏:
macro_rules! overriding_method { (url = $expr: expr) => { fn url(&self) -> String { $expr } }; (version = $expr: expr) => { fn version(&self) -> String { $expr } }; ($($tt: tt)*) => { compile_error!("This macro only accepts `url` and `version`"); }; }
经过这一系列操作, 就完美解决了最前面的问题
完整示例代码
trait CommonParams { fn url(&self) -> String { "https://hangj.cnblogs.com" } fn version(&self) -> String { "v1.2.3".into() } } macro_rules! overriding_method { (url = $expr: expr) => { fn url(&self) -> String { $expr } }; (version = $expr: expr) => { fn version(&self) -> String { $expr } }; ($($tt: tt)*) => { compile_error!("This macro only accepts `url` and `version`"); }; } macro_rules! define_structs { ( $( $(#[$meta: meta])* $(@[$($my_meta: tt)*])* $vis: vis struct $name: ident $body: tt )* ) => { $( $(#[$meta])* $vis struct $name $body impl CommonParams for $name { $( overriding_method!{ $($my_meta)* } )* } )* }; } custom_meta_struct! { ( define_structs, // callback macro #[derive(Debug)] ), #[derive(Clone)] struct A; @[version = "v2.3.4".into()] #[derive(serde::Serialize)] struct B; @[url = "https://crates.io/crates/ddnspod".into()] struct C; }
被展开后:
#[derive(Debug)] #[derive(Clone)] struct A; impl CommonParams for A {} #[derive(Debug)] #[derive(serde::Serialize)] struct B; impl CommonParams for B { fn version(&self) -> String { "v2.3.4".into() } } #[derive(Debug)] struct C; impl CommonParams for C { fn url(&self) -> String { "https://crates.io/crates/ddnspod".into() } }
最后
custom_meta_struct 的代码有 300 行左右, 花了我好多精力
要想编写出符合预期且行为复杂的 declarative macro 还是挺有挑战性的, 但是写完之后很有成就感 ✌️✌️
如果你想了解更多细节,不妨直接看代码 https://github.com/hangj/dnspod-lib/tree/main/src/macros
Have fun!