首页 > 其他分享 >过程宏(proc-macro)

过程宏(proc-macro)

时间:2024-04-15 20:00:30浏览次数:25  
标签:proc macro TokenStream let func 过程 fn

优点

  • 增加代码的复用。
  • 性能。因为是在编译时生成,所以会得到更好的性能。没测试过,有待商榷

过程宏的分类

  • proc-macro
  • proc-macro-derive
  • proc-macro-attribute

构建过程宏的必要设置

构建过程宏,要在cargo.toml里面设置一些参数,这是必须的。一般来说,过程宏必须是一个库,或者作为工程的子库,不能单独作为一个源文件存在,至少目前不行。

[lib]
proc-macro = true
path = "src/lib.rs"

而编写过程宏,在stable版本里,我们需要借助三个crate:

但在nightly版本里,以上的这些crate都不需要了,不依赖第三方crate,还有就是语法上是稍微有些不同,大部分是一样的。但这篇文章只讲stable rust里的过程宏,如果想了解nightly rust的过程宏,可以去看maud 和Rocket,前者是一个HTML模板引擎,大量使用了过程宏,模板都是编译时生成,所以性能非常高,而后者是一个web framework,rust各种黑魔法使用的集大成者。

 

proc-macro(function-like,类函数宏)

这种过程宏和标准宏很类似,只是构建过程不太一样,使用方式还是一样的。标准语法是这样的。

#[proc_macro]
pub fn my_proc_macro(input: TokenStream) -> TokenStream{
    // ...
}

可以看出函数式的过程宏只接受一个形参,而且必须是pub的。 简单写一个例子,参照官网文档的,只是稍微改了一点点。

#[proc_macro]
pub fn my_proc_macro(ident: TokenStream) -> TokenStream {
    let new_func_name = format!("test_{}", ident.to_string());
    let concated_ident = Ident::new(&new_func_name, Span::call_site()); // 创建新的ident,函数名

    let expanded = quote! {
        // 不能直接这样写trait bound,T: Debug
        // 会报错,找不到Debug trait,最好给出full path
        fn #concated_ident<T: std::fmt::Debug>(t: T) {
            println!("{:?}", t);
        }
    };
    expanded.into()
}

使用情形如下。

use your_crate_name::my_proc_macro;
// ...
my_proc_macro!(hello)!; // 函数test_hello就生成了,可见性在调用之后
// ...
test_hello("hello, proc-macro");
test_hello(10);

可以看出,写一个函数式的过程宏还是不那么复杂的。

proc_macro_derive(Derive mode macros, 继承宏)

继承宏的函数签名和前者有些类似:

#[proc_macro_derive(MyDerive)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
    // ...
}

不过不同的是,引入属性有些不同。

proc_macro_derive表明了这是继承宏,还定义了新的继承宏的名字MyDerive。 熟悉rust编程的,都应该知道有个继承宏,一直用得到,就是Debug。这是标准库里的,可以帮助调试和显示。所以呢,这里就来实现一个类似功能的继承宏,暂时命名这个过程宏名字为Show。 这个例子稍微有点复杂。当然我觉得还是先看了官方文档的例子之后再来看我的例子会比较好些。

#[proc_macro_derive(Show)]
pub fn derive_show(item: TokenStream) -> TokenStream {
    // 解析整个token tree
    let input = parse_macro_input!(item as DeriveInput);
    let struct_name = &input.ident; // 结构体名字

    // 提取结构体里的字段
    let expanded = match input.data {
        Data::Struct(DataStruct{ref fields,..}) => {
            if let Fields::Named(ref fields_name) = fields {
                // 结构体中可能是多个字段
                let get_selfs: Vec<_> = fields_name.named.iter().map(|field| {
                    let field_name = field.ident.as_ref().unwrap(); // 字段名字
                    quote! {
                        &self.#field_name
                    }
                }).collect();

            let implemented_show = quote! {
                // 下面就是Display trait的定义了
                // use std::fmt; // 不要这样import,因为std::fmt是全局的,无法做到卫生性(hygiene)
                // 编译器会报错重复import fmt当你多次使用Show之后
                impl std::fmt::Display for #struct_name {
                    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                        // #(#get_self),*,这是多重匹配,生成的样子大概是这样:&self.a, &self.b, &self.c, ...
                        // 用法和标准宏有点像,关于多个匹配,可以看这个文档
                        // https://docs.rs/quote/1.0.0/quote/macro.quote.html
                        write!(f, "{} {:?}", stringify!(#struct_name), (#(#get_selfs),*))
                    }
                }
            };
            implemented_show
            
            } else {
                panic!("sorry, may it's a complicated struct.");
            }
        }
        _ => panic!("sorry, Show is not implemented for union or enum type.")
    };
    expanded.into()
}

使用情形:

use your_crate_name::Show;
// ...
#[derive(Show)]
struct MySelf {
    name: String,
    age: u8,
}
// ...
let me = MySelf{name: "Jamie", age: 255};
println!("{}", me); // MySelf (Jamie, 255)

不过呢,继承宏还可以添加额外的属性,函数签名类似如下

#[proc_macro_derive(MyDerive, attributes(my_attr)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
    // ...
}

这里增加了一个关键字attributes,并指定了属性的名字。详细情况可以看官方文档。示例代码里也有个例子,因为文章篇幅,我就不赘述了。

proc_macro_attribute(Attribute macros, 属性宏)

属性宏的函数签名类似如下:

#[proc_macro_attribute]
pub fn my_attribute_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
    // ...
}

可以看到这里的形参是两个,使用的关键字是proc_macro_attribute。 关于例子,熟悉python的人应该知道修饰器吧,其实本质就是函数(闭包)可以作为一个对象来返回。 比如我需要一个修饰器来测量一个调用函数的运行时间。python的实现很简单,如下:

def my_decorator(func):
    import time
    def timming_measrement(*args):
        start = time.time()
        func(*args)
        end = time.time()
        print(f"time cost: {end - start}")
    return timming_measrement
    
@my_decorator
def my_target_func(sec):
    import time
    time.sleep(sec)
    
my_target_func(2) # should print 2.00xx
my_target_func(4) # should print 4.00xx

 

如果要用rust来实现类似功能的代码,就要复杂一些了。 属性宏接受的参数也不太一样,这也会导致属性宏的实现也会不太一样:

// 可能属性参数多种多样
// #[my_macro_attribute]
// #[my_macro_attribute=something]
#[my_macro_attribute(post)] // 这是例子的使用情况
fn my_func() {
    // ...
}

实现过程

#[proc_macro_attribute]
pub fn rust_decorator(attr: TokenStream, func: TokenStream) -> TokenStream {
    let func = parse_macro_input!(func as ItemFn); // 我们传入的是一个函数,所以要用到ItemFn
    let func_vis = &func.vis; // pub
    let func_block = &func.block; // 函数主体实现部分{}

    let func_decl = &func.sig; // 函数申明
    let func_name = &func_decl.ident; // 函数名
    let func_generics = &func_decl.generics; // 函数泛型
    let func_inputs = &func_decl.inputs; // 函数输入参数
    let func_output = &func_decl.output; // 函数返回

    // 提取参数,参数可能是多个
    let params: Vec<_> = func_inputs.iter().map(|i| {
        match i {
            // 提取形参的pattern
            // https://docs.rs/syn/1.0.1/syn/struct.PatType.html
            FnArg::Typed(ref val) => &val.pat, // pat没有办法移出val,只能借用,或者val.pat.clone()
            _ => unreachable!("it's not gonna happen."),
        }
    }).collect();
    
    // 解析attr
    let attr = parse_macro_input!(attr as AttributeArgs);
    // 提取attr的ident,此处例子只有一个attribute
    let attr_ident = match attr.get(0).as_ref().unwrap() {
        NestedMeta::Meta(Meta::Path(ref attr_ident)) => attr_ident.clone(),
        _ => unreachable!("it not gonna happen."),
    };
    
    // 创建新的ident, 例子里这个ident的名字是time_measure
    // let attr = Ident::new(&attr.to_string(), Span::call_site());
    let expanded = quote! { // 重新构建函数执行
        #func_vis fn #func_name #func_generics(#func_inputs) #func_output {
            // 这是没有重新构建的函数,最开始声明的,需要将其重建出来作为参数传入,
            // fn time_measure<F>(func: F) -> impl Fn(u64) where F: Fn(u64)
            // fn deco(t: u64) {
            //     let secs = Duration::from_secs(t);
            //     thread::sleep(secs);
            // }
            fn rebuild_func #func_generics(#func_inputs) #func_output #func_block
            // 注意这个#attr的函数签名:fn time_measure<F>(func: F) -> impl Fn(u64) where F: Fn(u64)
            // 形参是一个函数,就是rebuild_func
            let f = #attr_ident(rebuild_func);

            // 要修饰函数的参数,有可能是多个参数,所以这样匹配 #(#params,) *
            f(#(#params,) *)
        }
    };
    expanded.into()
}

还有一段代码,这个函数相当于过程宏的属性(参数attr)。

// use std::time;
// 该函数接受一个函数作为参数,并返回一个闭包,代码很简单,就不解释了。
// thanks for impl trait
fn runtime_measurement<F>(func: F) -> impl Fn(u64) where F: Fn(u64) {
    move |s| {
        let start = time::Instant::now();
        func(s);
        println!("time cost {:?}", start.elapsed());
    }
}

假定这是我们要修饰的目标函数。

 
#[rust_decorator(runtime_measurement)]
fn deco(t: u64) {
    let secs = Duration::from_secs(t);
    thread::sleep(secs);
}

// ...
deco(4);
deco(2);

上面这个例子有点复杂了,其实可以不用把测试函数作为参数传入,所以不需要定义一个attr,也不需要解析这个attr。直接可以这样写,简单明了,可以获取任意函数的运行时,上面的那个还要考虑参数类型。

#[proc_macro_attribute]
pub fn run_time(_: TokenStream, func: TokenStream) -> TokenStream {
    let func = parse_macro_input!(func as ItemFn);
    let func_vis = &func.vis; // like pub
    let func_block = &func.block; // { some statement or expression here }

    let func_decl = func.sig;
    let func_name = &func.ident; // function name
    let func_generics = &func_decl.generics;
    let func_inputs = &func_decl.inputs;
    let func_output = &func_decl.output;
    
    let caller = quote!{
        // rebuild the function, add a func named is_expired to check user login session expire or not.
        #func_vis fn #func_name #func_generics(#func_inputs) #func_output {
            use std::time;
            
            let start = time::Instant::now();
            #func_block
            println!("time cost {:?}", start.elapsed());
        }
    };
    
    caller.into() 
}

使用和前面的类似。

 
#[run_time]
fn deco(t: u64) {
    let secs = Duration::from_secs(t);
    thread::sleep(secs);
}

// ...
deco(4);
deco(2);

 

from:https://dengjianping.github.io/2019/02/28/%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E8%BF%87%E7%A8%8B%E5%AE%8F(proc-macro).html

标签:proc,macro,TokenStream,let,func,过程,fn
From: https://www.cnblogs.com/banking/p/18136816

相关文章

  • Java使用javacv处理视频文件过程记录
    最近接到一个需求是将.mp4/.m4v文件体积进行压缩,我使用javacv中的FFmpegFrameGrabber、FFmpegFrameFilter、FFmpegFrameRecorder简单的实现视频帧的抓取、过滤、录制与输出。性能暂未验证。文章对这次的过程进行记录。1.jdk的选择mcr.microsoft.com/java/jdk:8u222-zulu-cento......
  • process scheduling (进程调度)& practice 1 process operation
    进程切换并发进程的切换并发进程中,一个进程在执行过程中可能被另一个进程替换占有CPU,这个过程称为“进程切换”是什么触发了进程切换?进程切换时要做什么?操作系统到底做了什么操作2中断技术中断是指程序执行过程中当发生某一个事件时,中止cpu上现行的程序的运行in......
  • 定义存储过程和函数
    定义存储过程和函数本文出处:https://www.modb.pro/db/222642学习地址https://www.modb.pro/course/133学习目标学习openGauss定义存储过程和函数课后作业1.创建带有入参和出参的函数1,调用函数时使用按参数值传递和命名标记法传参omm=#createfunctionfunc_multiply(......
  • 9个MogDB存储过程示例
    9个MogDB存储过程示例本文出处:https://www.modb.pro/db/400634存储过程是一组结构化的查询和语句,例如控制语句和声明。这里介绍9个在不同情况下很有用的存储过程示例。创建测试表:createtablepublic.test1(idint,namevarchar(10));使用存储过程插入数据CREATEORR......
  • 十大管理每个过程定义以及作用
    一、整体管理1、制定项目章程制定一份正式批准并授权项目经理在项目活动中使用活动资源的文件。明确了项目于组织战略目标之间的直接联系;确立了项目的正式地位;展示了组织对项目的承诺。2、制定项目管理计划组织、准备和协调项目计划的其他组成部分的文件,并把它们整合成一份综合的......
  • 【已解决】squashfs4.3编译过程中的一些问题
    Q:执行./build.sh时,mksquashfs.c和unsquashfs.c报错找不到major()和minor()函数的定义,找不到‘makedev’cc-O2-I.-D_FILE_OFFSET_BITS=64-D_LARGEFILE_SOURCE-D_GNU_SOURCE-DCOMP_DEFAULT=\"gzip\"-Wall-DGZIP_SUPPORT-DXATTR_SUPPORT-DXATTR_DEFAULT-c-omksqua......
  • 【Spring】AOP进阶-JoinPoint和ProceedingJoinPoint详解
    2024-04-141.前言在Spring AOP中,JoinPoint和ProceedingJoinPoint都是关键的接口,用于在切面中获取方法的相关信息以及控制方法的执行。它们的主要区别在于它们在AOP通知中的使用方式和功能。2.JoinPoint简介Joinpoint是面向切面编程(AOP)中的一个重要概念,指的是在应用程序执行......
  • SpringBoot项目中对定义的多个BeanPostProcessor排序
    前言BeanPostProcessor是Spring提供的一种扩展机制,可以让我们在bean初始化前后做一些额外的操作,Spring中的@Async,@Scheduled,@RabbitHandler等注解的底层实现都是BeanPostProcessor在起作用,如RabbitListenerAnnotationBeanPostProcessor。代码示例@Configurationpub......
  • MySQL-09-mysql 存储过程入门介绍
    拓展阅读MySQL00ViewMySQL01Rulermysql日常开发规范MySQL02truncatetable与delete清空表的区别和坑MySQL03Expression1ofORDERBYclauseisnotinSELECTlist,referencescolumnMySQL04EMOJI表情与UTF8MB4的故事MySQL05MySQL入门教程(MySQLtutor......
  • process concept
    进程的定义程序和进程Aprogramisapassiveentity(是被动的主体),suchasafilecontainingalistofinstructionsstoredondisk(oftencalledanexecutablefile(就是可执行文件))Aprogrambecomesaprocesswhenanexecutablefileisloadedintomemory.(可执......