Summer-boot一期宏部分构思及实现

summer-boot中的宏

使用的第三方库

  • proc_macro2: 该库是对 proc_macro 的封装,是由 Rust 官方提供的。
  • syn: 基于 proc_macro2 中暴露的 TokenStream API 来生成 AST 。该库提供来方便的 AST 操作接口。
  • quote: 该库配合 syn,将 AST 转回 TokenSteam,回归到普通文本代码生成的 TokenSteam 中。
  • toml: 解析toml配置文件

#[summer_boot::main]

提供简单的tokio运行时功能,和语法检查工作。

async语法检查部分:

if sig.asyncness.is_none() {
return syn::Error::new_spanned(sig.fn_token, "仅支持 async fn")
.to_compile_error()
.into();
}
sig.asyncness = None;

#[summer_boot::auto_scan]

提供了对象整个项目下所有的rs文件中的method宏扫描工作。

判断当前项目目录结构

此时则需要上述的第三方库toml了,因为rust项目提供了两种目录结构:projectworkspace。则会导致发现及调用的指定宏函数方式不同。

// 根据包类型分别处理
if let Ok(conf_work_space) = toml::from_str::<ConfWorkSpace>(&content) {
if let Some(workspace) = conf_work_space.workspace {
if let Some(members) = workspace.members {
...
}
} else if project.len() == 0 {
if let Some(_) = conf_work_space.package {
...
}
}
}

递归寻找目录结构下所有的rs文件

根据目录的结构,递归查找所有的rs文件,此时就需要查找指定的method宏了。

// 开始扫描
for path in project {
scan_method(&path, &mut input, (master_index, &master_name));
}

将rs文件分解成指定token,并判定是否为method宏
// 如果是文件,则处理内部细节
let mut content = String::new();
file.read_to_string(&mut content).unwrap();

// 解析文件,转为ast数据结构
let ast = parse_file(&content).unwrap();
let items = ast.items;
for item in items {
...
}

因为method宏的标记都是宏,因此我们只需要找到ast中的宏即可。

找到宏
// 遍历所有宏信息
if let Meta::List(meta) = attr.parse_meta().unwrap() {
// 判断宏是否为指定的宏

proc_macro2ayn库提供了很棒的ast处理部分,完善的数据结构只需要在文档找找到相应的结构体,即可实现查找指定的语句结构,甚至提供了自定义数据结构查找。

*注意,让我停一下:
整理思路,在此我们找到了想要找到的宏标记,相应的可以找到这个函数路径,那么为什么这么做呢?
目前rust开源服务的框架,大都是在实现接口函数后,手动添加至server内,即手动装配。
那么Summer解决的问题就是实现自动装配,将实现接口的函数自动装配到指定的server内。
那么怎么实现装配呢?答案是:宏。

设想方案:
主运行函数提供了简单的summer_boot::run()调用机制,并返回server即可,将尽量简化了开发者开发的复杂度。
那么针对实现功能,宏就需要提供如下的ast补充。
github
上图可以看出来,绿色部分是开发者调用段落,而红色部分则是宏来完成。
其中包括:装配server变量、调用server服务、资源文件读取、装配接口函数三大部分*

找到主函数-summer::run()

在main函数中找到我们的主功能函数,即summer_boot::run();,并且给他配置一个变量,完成后续工作。

// 查找主函数的位置和是否存在变量名
// 未找到则直接退出宏处理
// 变量名不存在则默认添加app
// 如果存在则会返回出来,供后续使用
if let Some((master_index, master_name)) = scan_master_fn(&mut input) {
...
}

此时我们就会得到:

  • 主函数在main中的调用位置索引
  • 主函数返回的变量名称
装配server变量

此处的逻辑:如果在主函数调用索引下找到了便令,则只需要找到该变量信息返回即可;如果开发者没有配置变量,则宏来完成变量装配。

if let Stmt::Local(local) = &input.block.stmts[master_index as usize] {
let pat = &local.pat;
if let Pat::Ident(patIdent) = pat {
let name = patIdent.ident.to_string();
// 找到开发者配置的变量
}
} else {
// 宏装配变量
input.block.stmts.insert...

}

装配method宏接口函数

根据找到的主函数位置,后续接着在此处插入我们找到的method宏接口函数即可。

input.block.stmts.insert(
master_index as usize,
parse_quote! {
#master_name.at(#url).#method(#fn_path_token_stream);
},
);

资源文件读取(解析yml文件)

这里则是将读取yml的代码,获取到内部数据返回即可。

调用server服务

此时只需要调listen函数即可,所需要的只有server变量和资源数据即可。

// 配置listen
input.block.stmts.push(parse_quote!{
#master_name.listen(#listener_addr).await.unwrap();
});

#[summer_boot::${method}]

由于此处的get、post等宏,都具有相似处,因此直接使用声明宏来实现即可。

macro_rules! method_macro {
(
$($method:ident,)+
) => {
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {

let mut input = parse_macro_input!(input as ItemFn);
let attrs = &input.attrs;
let vis = &input.vis;
let sig = &mut input.sig;
let body = &input.block;
let _name = &sig.ident;
if sig.asyncness.is_none() {
return syn::Error::new_spanned(sig.fn_token, "仅支持 async fn")
.to_compile_error()
.into();
}

(quote! {
#(#attrs)*
#vis #sig
#body
}).into()
}
})+
};
}