简介

Rust 编程语言里面有两种宏系统,一种是声明宏(Declarative Macros),另一种为过程宏(Procedural Macros)。声明宏和过程宏是两种基本上完全不一样的宏系统,编写的方式也完全不一致,使用方式除了函数式外也不一致。关于声明宏学习,Rust 宏小册 里面有比较详细的说明,这里不再啰嗦。而对于过程宏,网上是可以搜索到的资料则相对较少,系统介绍学习的资料就更加少了。

过程宏所做的事情则是从输入中获取到标记流处理这些标记流或者生成新的标记流,然后将处理后的标记流返回给编译器作下一步的处理。需要注意的是,过程宏操作的是Rust AST(抽象语法树),所以即使是在宏里面,也必须是合法Rust的语法结构。这也就意味着,解析过程宏的过程中,var表示的是一个合法的标识符,而6var则是非法的。

这篇文章是,对过程宏进行一些不完全的探讨和学习。

三种过程宏形式

过程宏必须是一个独立的库(很多开源项目喜欢用xxx_derive的名称命名),这个库只导出过程宏的函数,而这个宏是被编译器调用的。Cargo.toml里面必须有以下内容表明是一个过程宏:

  1. [lib]
  2. proc-macro = true

proc_macroRust 编译器提供的编写过程宏所需的类型和工具,过程宏有以下三种表示形式:

derive

  1. 函数带有#[proc_macro_derive(Name)] 属性或者 #[proc_macro_derive(Name, attributes(attr))]属性
  2. 函数签名为 pub fn xxxx (proc_macro::TokenStream) -> proc_macro::TokenStream

函数的名称叫什么并不重要,使用时是使用proc_macro_derive里面的名称,如下例子

  1. #[proc_macro_derive(Getters, attributes(getter))]
  2. pub fn getters(input: TokenStream) -> TokenStream {
  3. //...
  4. }

使用

  1. #[derive(Getters)]
  2. struct Test {
  3. #[getter(name=get_name)]
  4. name: String
  5. }

函数式

  1. 函数带有 #[proc_macro] 属性
  2. 函数签名为 pub fn xxx (proc_macro::TokenStream) -> proc_macro::TokenStream

函数的名称就是使用时名称,如下例子:

  1. #[proc_macro]
  2. pub fn lazy_static(input: TokenStream) -> TokenStream {
  3. //...
  4. }

使用方式和声明宏调用一摸一样

  1. lazy_static!{
  2. //...
  3. }

属性式

  1. 函数带有#[proc_macro_attribute]属性
  2. 函数签名为 pub fn xxx(proc_macro::TokenStream, proc_macro::TokenStream) -> proc_macro::TokenStream

函数的名称就是使用时名称,如下例子:

  1. #[proc_macro_attribute]
  2. pub fn retry(attr: TokenStream, input: TokenStream) -> TokenStream {
  3. //...
  4. }

使用方式和Python装饰器的使用方式类似

  1. #[retry(times=5, timeout=60s)]
  2. pub fn fetch_data(url: String) -> Result<MyData> {
  3. //...
  4. }

一般来说,derive 式是对原有功能的扩展,原有的声明是保留下的,更多是在原有基础上增加功能,比如增加impl函数,增加泛型约束等等。函数式则更多的是用于自定义语法的解析,如果声明宏描述语法困难一般可以考虑用函数式来替代。而属性式则是完全对原有功能的改写了,属于替代性的。特别需要注意的是,过程并不是卫生性的,这点是和声明宏不一样。也就是说,过程宏它是会污染当前模块。所以,在过程宏里面定义或使用类型时,必须要用全路径的形式,而自定生成的函数变量等等命名也要特殊考虑,以防止污染了当前模块。

quote, syn, proc_macro2以及trybuildcargo-expand

编写过程宏,编译器只提供proc_macro这个crate,不过它所提供的功能非常有限,单独使用这个库的话,编写比较啰嗦和麻烦。因此大神dtolnay提供了, 和``这三个库来简化过程宏的编写,有了这三个crate,编写过程宏就如同编写普通的代码一样,除了调试困难一点外,基本没什么差别。这三个库基本成功编写过程宏事实上的标准。

同时为了使编写过程更加温柔点而不至于暴躁,dtolnay提供了trybuild这个库用于编写宏的单元测试,cargo-expand用于过程宏的展开。

proc_macro2

proc_macro所提供的TokenStreamquotesyn所处理的TokenStream是不兼容的,所以另外增加一个 proc_macro2,用于和proc_macroTokenStream互相转换。实际上这个库的使用,只是在最后返回值里面调用一下into而已。

quote

quote是将编写的代码转换为Rust token的方式,提供一种称之为quasi-quoting的方式,将代码视为数据,并可以进行插值。比较常用的是这两个宏:parse_quote!quote!,以及format_ident!

syn

这是编写过程宏最重要的一个,大部分时间都是和这个库进行打交道,它表示了一个完整的Rust 语法,如果看语言的Reference感觉到抽象,最好来这里看代码,它以非常具体的编码实现告诉你这个Reference是怎么表示的。

trybuild

平常的单元测试是编译好的代码,然后再运行测试用例。然而过程宏的的测试,处需要测试是否编译通过,也需要编译出错的结果是否正确。这是需要这个trybuild库了。

cargo-expand

cargo-expand用于宏代码的展开,需要注意的是,需要正常编译通过的代码才可以进行展开。

使用这quote, syn, proc_macro2三个库来编写过程宏后,框架代码基本一致,一般有如下三个步骤,如下方式:

  1. use proc_macro::TokenStream;
  2. use quote::quote;
  3. use syn::{parse_macro_input, DeriveInput};
  4. #[proc_macro_derive(MyMacro)]
  5. pub fn my_macro(input: TokenStream) -> TokenStream {
  6. // 将输入的标记解析成语法树
  7. let input = parse_macro_input!(input as DeriveInput);
  8. // 使用quote!进行插值处理
  9. let expanded = quote! {
  10. // ...
  11. };
  12. // 将proc_macro2的TokenStream转换为proc_macro的TokenStream
  13. TokenStream::from(expanded)
  14. }

以下,编写三种形式是示例,来看下着三种形式的过程过程宏怎么编写以及怎么使用。示例代码仓库:https://github.com/buf1024/my_macro_demo

示例:derive

假设我们需要为结构体生成一系列的getter函数,当然getter的名字是可以自定义的也可以根据默认的字段名称生成,也可以设置getter的可见性,同时根据是否注释生成对应的desc函数。不需要考虑这样的功能在实际工作中是否有意义,这里的重点是学校过程宏的编写过程。

首先编写宏就是为了使用它,所以第一步,要了解是怎么使用这个宏:

  1. // 首先可以是这样简单使用
  2. #[derive(Getters)]
  3. struct MyStruct {
  4. data: String,
  5. }
  6. // 又或者想变更一下它的名称
  7. #[derive(Getters)]
  8. struct MyStruct {
  9. #[getter(name=get_fuck_data)]
  10. data: String,
  11. }
  12. // 又或者是这样
  13. #[derive(Getters)]
  14. struct MyStruct {
  15. #[getter(vis=pub(crate))]
  16. #[getter(name=get_fuck_data)]
  17. data: String,
  18. }
  19. // 以及可能有注释
  20. #[derive(Getters)]
  21. struct MyStruct {
  22. /// 这是一个data的属性
  23. #[getter(vis=pub(crate))]
  24. #[getter(name=get_fuck_data)]
  25. data: String,
  26. }
  27. // 设置机构体可能复杂, 带上了生命周期参数和泛型
  28. #[derive(Getters)]
  29. struct MyStruct<'a, T: Sync+Send+Constraint> {
  30. #[getter(vis=pub(crate))]
  31. #[getter(name=get_fuck_data)]
  32. data: &'a str,
  33. constraint: T
  34. }

确定了过程宏的使用方式后,我就可以可以定义我们的导出函数了:

  1. #[proc_macro_derive(Getters, attributes(getter))]
  2. pub fn getters(input: TokenStream) -> TokenStream {
  3. let input = parse_macro_input!(input as DeriveInput);
  4. let token_stream = expand_getters(input);
  5. token_stream
  6. .unwrap_or_else(|e| e.into_compile_error())
  7. .into()
  8. }

我们将输入token流解析为DeriveInput,是因为DeriveInput实现了Parse trait。定义如下:

  1. pub trait Parse: Sized {
  2. fn parse(input: ParseStream) -> Result<Self>;
  3. }
  4. pub struct DeriveInput {
  5. pub attrs: Vec<Attribute>,
  6. pub vis: Visibility,
  7. pub ident: Ident,
  8. pub generics: Generics,
  9. pub data: Data,
  10. }
  11. pub enum Data {
  12. Struct(DataStruct),
  13. Enum(DataEnum),
  14. Union(DataUnion),
  15. }

DeriveInput所实现的ParseDeriveInput数据结构可以看出,derive 式过程宏只支持StructEnumUnion三种数据结构。

写过程宏的一个重要的工作就是获取所修饰的数据结构的基本信息,而对于derive 式过程宏来说,这些数据放到attrs这个属性里面,用Attribute这个结构来表示,Meta则是存储这样数据的。

  1. pub struct Attribute {
  2. pub pound_token: Token![#],
  3. pub style: AttrStyle,
  4. pub bracket_token: token::Bracket,
  5. pub meta: Meta,
  6. }
  7. pub enum Meta {
  8. Path(Path),
  9. /// A structured list within an attribute, like `derive(Copy, Clone)`.
  10. List(MetaList),
  11. /// A name-value pair within an attribute, like `feature = "nightly"`.
  12. NameValue(MetaNameValue),
  13. }

Meta是什么鬼?按照syn的文档:

  1. /// text
  2. /// #[derive(Copy, Clone)]
  3. /// ~~~~~~Path
  4. /// ^^^^^^^^^^^^^^^^^^^Meta::List
  5. ///
  6. /// #[path = "sys/windows.rs"]
  7. /// ~~~~Path
  8. /// ^^^^^^^^^^^^^^^^^^^^^^^Meta::NameValue
  9. ///
  10. /// #[test]
  11. /// ^^^^Meta::Path
  12. ///

需要注意的是,注释文档是解析为#[doc = r" Single line doc comments"]的。所以,文档注释的获取:

  1. let doc_str: String = f
  2. .attrs
  3. .iter()
  4. .filter(|attr| attr.path().is_ident("doc"))
  5. .try_fold(String::new(), |acc, attr| {
  6. let mnv = match &attr.meta {
  7. syn::Meta::NameValue(mnv) => mnv,
  8. _ => return Err(syn::Error::new_spanned(attr, "expect name value!")),
  9. };
  10. let doc_str = match &mnv.value {
  11. syn::Expr::Lit(syn::ExprLit {
  12. lit: syn::Lit::Str(lit),
  13. ..
  14. }) => lit.value(),
  15. _ => return Err(syn::Error::new_spanned(attr, "expect string literal!")),
  16. };
  17. Ok(format!("{}\n{}", acc, doc_str))
  18. })?;

前面提到DeriveInput实现了Parsetrait进行解析,而我们要对Attribute里面的内容进行解析,则是需要实现该trait:

  1. impl Parse for GetterMeta {
  2. fn parse(input: syn::parse::ParseStream) -> Result<Self> {
  3. let lookahead = input.lookahead1();
  4. if lookahead.peek(kw::name) {
  5. let _: kw::name = input.parse()?;
  6. let _: Token![=] = input.parse()?;
  7. let name: Ident = if input.peek(LitStr) {
  8. let sl: LitStr = input.parse()?;
  9. let value = sl.value();
  10. format_ident!("{}", value.trim())
  11. } else {
  12. input.parse()?
  13. };
  14. Ok(Self {
  15. name: Some(name),
  16. vis: None,
  17. })
  18. } else if lookahead.peek(kw::vis) {
  19. let _: kw::vis = input.parse()?;
  20. let _: Token![=] = input.parse()?;
  21. let vis = input.parse()?;
  22. Ok(Self {
  23. name: None,
  24. vis: Some(vis),
  25. })
  26. } else {
  27. Err(lookahead.error())
  28. }
  29. }
  30. }

写法可以完全参考DeriveInput的写法,调用Atrribute的,parse_argsparse_args_with,则可以调用到Parsetrait。

获取到基本的数据后,自然就生成代码,这里两个重要的宏: quote!parse_quote!。最后生成的代码用#[automatically_derived]进行装饰,说明这是自动生成的代码。

  1. let (impl_generic, type_generic, where_clause) = input.generics.split_for_impl();
  2. Ok(quote! {
  3. #[automatically_derived]
  4. impl #impl_generic #st_name #type_generic #where_clause {
  5. pub fn hello(&self) {
  6. println!("hello!");
  7. }
  8. #getters
  9. }
  10. })

过程宏写好之后,我们就要写单元测试了,当然也可以先写单元测试,再写过程宏,这称为测试用例驱动的开发模式。不过,作为示例,我们不写很详细的测试用例,我们只写两个测试用例,一个是成功的,一个是失败的,展示怎么使用即可。失败的用例,我们需要提供一个和测试文件一致,以.stderr结尾的输出,如果编译器的输出一致则测试通过,这个输出可以先编译出来,让编译器生成,然后自己对比是不是自己想要的结果。

过程宏使用后,是否符合自己的需要,需要宏展开来观察,使用是cargo exand命令:

  1. #![allow(dead_code)]
  2. use my_derive::Getters;
  3. #[derive(Getters)]
  4. struct MyStructRef<'a> {
  5. /// 你好呀
  6. #[getter(vis=pub(crate))]
  7. #[getter(name = "get_fuck_data")]
  8. data: &'a str,
  9. }
  10. fn main() {}
  11. //展开代码
  12. #![feature(prelude_import)]
  13. #![allow(dead_code)]
  14. #[prelude_import]
  15. use std::prelude::rust_2021::*;
  16. #[macro_use]
  17. extern crate std;
  18. use my_derive::Getters;
  19. struct MyStructRef<'a> {
  20. /// 你好呀
  21. #[getter(vis = pub(crate))]
  22. #[getter(name = "get_fuck_data")]
  23. data: &'a str,
  24. }
  25. #[automatically_derived]
  26. impl<'a> MyStructRef<'a> {
  27. pub fn hello(&self) {
  28. {
  29. ::std::io::_print(format_args!("hello!\n"));
  30. };
  31. }
  32. pub(crate) fn get_fuck_data(&self) -> &'a str {
  33. &self.data
  34. }
  35. pub fn get_fuck_data_desc(&self) -> &'static str {
  36. "你好呀"
  37. }
  38. }
  39. fn main() {}

示例:属性式

假设我们有这样一个需求,可以对原有函数进行增加一些其他功能,比如retry,可以设置调用超时时间,超时后或者出错后,可以进行重新调用,类似于Python装饰器,可以考虑用属性式过程宏表示。同样,因为是测试例子,所以,没有超时和重试功能,只做怎么获取属性宏的数据和生成代码。

首先确定调用的形式:

  1. #[retry(times=5, timeout=60)]
  2. fn remote_request(a: i32, b: i32) -> i32 {
  3. println!("@remote_request!");
  4. a + b
  5. }

再确定其展开形式:

  1. // cargo exapnd 生成的
  2. fn remote_request(a: i32, b: i32) -> i32 {
  3. fn __new_remote_request(a: i32, b: i32) -> i32 {
  4. {
  5. ::std::io::_print(format_args!("@remote_request!\n"));
  6. };
  7. a + b
  8. }
  9. for _ in 0..5 {
  10. __new_remote_request(a, b);
  11. }
  12. for _ in 0..60 {
  13. __new_remote_request(a, b);
  14. }
  15. __new_remote_request(a, b)
  16. }

属性式的签名是这个样子的:

  1. #[proc_macro_attribute]
  2. pub fn retry(attr: TokenStream, input: TokenStream) -> TokenStream

属性和输入流已经作为参数传递给我们了,而我需所要做的是需要将属性解析出来和对原函数的重写。

  1. let item_fn = parse_macro_input!(input as ItemFn);
  2. let args = parse_macro_input!(attr as Args);

derive式的一样,通过实现Parsetrait来解析:

  1. impl Parse for RetryAttr {
  2. fn parse(input: syn::parse::ParseStream) -> Result<Self> {
  3. let lookahead = input.lookahead1();
  4. if lookahead.peek(kw::times) {
  5. let _: kw::times = input.parse()?;
  6. let _: Token![=] = input.parse()?;
  7. let times: LitInt = if input.peek(LitInt) {
  8. input.parse()?
  9. } else {
  10. return Err(lookahead.error());
  11. };
  12. Ok(Self {
  13. times: Some(times),
  14. timeout: None,
  15. })
  16. } else if lookahead.peek(kw::timeout) {
  17. let _: kw::timeout = input.parse()?;
  18. let _: Token![=] = input.parse()?;
  19. let timeout: LitInt = if input.peek(LitInt) {
  20. input.parse()?
  21. } else {
  22. return Err(lookahead.error());
  23. };
  24. Ok(Self {
  25. times: None,
  26. timeout: Some(timeout),
  27. })
  28. } else {
  29. Err(lookahead.error())
  30. }
  31. }
  32. }

示例:函数式

假设我们要计算二元二次方程组的值,我们计划是这样使用的:

  1. let (x, y) = formula!(1 * x + 1 * y = 2, 2 * x + 1 * y = 9);

这个宏直接就在编译期间就计算出x,y的值(当然是存在有解的情况下),无解就panic。为了使问题简单,我们假设x,y前面都是有系数的。过程宏的操作过程就是解析出里面的表达式:

  1. pub(crate) struct FormulaArgs {
  2. formula: Vec<Formula>,
  3. }
  4. impl Parse for FormulaArgs {
  5. fn parse(input: syn::parse::ParseStream) -> Result<Self> {
  6. let attrs = Punctuated::<Formula, Token![,]>::parse_terminated(input)?;
  7. let formula: Vec<_> = attrs.into_iter().collect();
  8. if formula.len() != 2 {
  9. panic!("require two formula")
  10. }
  11. Ok(FormulaArgs { formula })
  12. }
  13. }
  14. #[derive(Default)]
  15. pub(crate) struct Formula {
  16. x: i32,
  17. y: i32,
  18. rs: i32,
  19. }
  20. impl Parse for Formula {
  21. fn parse(input: syn::parse::ParseStream) -> Result<Self> {
  22. let lookahead = input.lookahead1();
  23. let x = if lookahead.peek(LitInt) {
  24. let x_lit: LitInt = input.parse()?;
  25. let x: i32 = x_lit.to_string().parse().unwrap();
  26. let _: Token![*] = input.parse()?;
  27. let _: kw::x = input.parse()?;
  28. x
  29. } else {
  30. return Err(lookahead.error());
  31. };
  32. let r1: Result<Token![+]> = input.parse();
  33. let r2: Result<Token![-]> = input.parse();
  34. let factor = match (r1, r2) {
  35. (Ok(_), Err(_)) => 1,
  36. (Err(_), Ok(_)) => -1,
  37. (_, _) => return Err(lookahead.error()),
  38. };
  39. // let factor = if lookahead.peek(Token![+]) {
  40. // let _: Token![+] = input.parse()?;
  41. // 1
  42. // } else if lookahead.peek(Token![-]) {
  43. // let _: Token![-] = input.parse()?;
  44. // -1
  45. // } else {
  46. // return Err(lookahead.error());
  47. // };
  48. let y = if lookahead.peek(LitInt) {
  49. let y_lit: LitInt = input.parse()?;
  50. let y: i32 = y_lit.to_string().parse().unwrap();
  51. let _: Token![*] = input.parse()?;
  52. let _: kw::y = input.parse()?;
  53. y * factor
  54. } else {
  55. return Err(lookahead.error());
  56. };
  57. let _: Token![=] = input.parse()?;
  58. let rs_lit: LitInt = input.parse()?;
  59. let rs: i32 = rs_lit.to_string().parse().unwrap();
  60. if x == 0 && y == 0 && rs != 0 {
  61. return Err(syn::Error::new_spanned(rs_lit, "invalid equal"));
  62. }
  63. Ok(Self { x, y, rs })
  64. }
  65. }

当解析出所需要的数据后,直接就可以进行插值生成自己所需要的数据。

总结

过程宏的编写过程有两个步骤。

首先是解析出过程宏所需要的信息,这个步骤一般是通过实现syn所提供的Parsetrait实现的。由于syn表示的也是合法的语法结构,所以并不是所以的写法都是支持的。有时候,解析的时候出出现一些莫名奇妙解析不了的问题,比如解析二元方程时:

  1. // let factor = if lookahead.peek(Token![+]) {
  2. // let _: Token![+] = input.parse()?;
  3. // 1
  4. // } else if lookahead.peek(Token![-]) {
  5. // let _: Token![-] = input.parse()?;
  6. // -1
  7. // } else {
  8. // return Err(lookahead.error());
  9. // };

这段代码总是无法解析成功,原因未解,或者使用姿势不对,又或者是syn可能潜在有bug。不过都有变通的方法实现。

其次解析出自己所要的数据后,就可以根据具体的情况进行插值处理,只有是使用到quote这个库,而大多数情况之下只使用到两个宏: quote!parse_quote!。当然也不是说没有坑。比如说解析出函数的block时,再重组时,要加上大扩号。再比如,解析出行数调用列表时,解析成一个元组表达式,而插值时,可以放个函数名称在前面,就变成了函数调用。而这些都是变成过程宏编写的惯例吧,习惯就好。

错误处理是给宏的使用者看的,友好的错误提示很容易就让调用者知道哪里错了。而使用错误处理是比较简单的,直接掉用syn::Error生成一个Span即可,Span也是可以combine的里面有个token的参数。不过如果不知道token的情况之下怎么处理呢?目前自己的做法是panic,这并非是一种明智的方式。或者有更加灵活的处理方式。

使用cargo expand是可以查看到宏展开的内容,不过cargo expand的问题是过程宏没有问题时,才可以正常的展开,出现编译问题不展开,这也就造成了调试过程宏的困难,目前也没有什么好的办法去解决。

总体来说,过程宏的编写并不是非常困难,syn表示了一个完整的Rust语法,查看里面语法的表示,比看语言Reference强太多了,而这对于更深入了解声明宏的片段分类符更有帮助!proc-macro-workshop 是大神dtolnay设计的过程宏系统,全部做出来,估计写过程宏没有什么问题了。

再贴一下示例代码仓库:https://github.com/buf1024/my_macro_demo

Post AT:

https://mp.weixin.qq.com/s/17jPRjzyU4lkSD-mS1wxvg

rust 过程宏的更多相关文章

  1. Rust中的宏:声明宏和过程宏

    Rust中的声明宏和过程宏 宏是Rust语言中的一个重要特性,它允许开发人员编写可重用的代码,以便在编译时扩展和生成新的代码.宏可以帮助开发人员减少重复代码,并提高代码的可读性和可维护性.Rust中有 ...

  2. 【译】Rust宏:教程与示例(一)

    原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...

  3. 【译】Rust宏:教程与示例(二)

    原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...

  4. 《手把手教你》系列技巧篇(七)-java+ selenium自动化测试-宏哥带你全方位吊打Chrome启动过程(详细教程)

    1.简介 经过前边几篇文章和宏哥一起的学习,想必你已经知道了如何去查看Selenium相关接口或者方法.一般来说我们绝大多数看到的是已经封装好的接口,在查看接口源码的时候,你可以看到这个接口上边的注释 ...

  5. [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]

    [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro] 实用知识 宏Macro 我们今天来讲讲Rust中强大的宏Macro. Rust的宏macro是实现元编程的强大工具. ...

  6. Rust入坑指南:万物初始

    有没有同学记得我们一起挖了多少个坑?嗯-其实我自己也不记得了,今天我们再来挖一个特殊的坑,这个坑可以说是挖到根源了--元编程. 元编程是编程领域的一个重要概念,它允许程序将代码作为数据,在运行时对代码 ...

  7. 最强肉坦:RUST多线程

    Rust最近非常火,作为coder要早学早享受.本篇作为该博客第一篇学习Rust语言的文章,将通过一个在其他语言都比较常见的例子作为线索,引出Rust的一些重要理念或者说特性.这些特性都是令人心驰神往 ...

  8. Rust 中的继承与代码复用

    在学习Rust过程中突然想到怎么实现继承,特别是用于代码复用的继承,于是在网上查了查,发现不是那么简单的. C++的继承 首先看看c++中是如何做的. 例如要做一个场景结点的Node类和一个Sprit ...

  9. Win32汇编过程与宏调用

    汇编语言(assembly language)是一种用于电子计算机.微处理器.微控制器或其他可编程器件的低级语言,亦称为符号语言.在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地 ...

  10. 性能提升 40 倍!我们用 Rust 重写了自己的项目

    前言 Rust 已经悄然成为了最受欢迎的编程语言之一.作为一门新兴底层系统语言,Rust 拥有着内存安全性机制.接近于 C/C++ 语言的性能优势.出色的开发者社区和体验出色的文档.工具链和IDE 等 ...

随机推荐

  1. PostgreSQL-分区表介绍

    一.分区简介 表分区是解决一些因单表过大引用的性能问题的方式,比如某张表过大就会造成查询变慢,可能分区是一种解决方案.一般建议当单表大小超过内存就可以考虑表分区了. 表的分区就是将一个逻辑上的大表(主 ...

  2. Hyper-V安装Ubuntu16.04,连接网络的设置

    使用Windows10自带的Hyper-V安装Ubuntu. 进入系统界面后,无法连接网络. 1.首先确定安装虚拟机的网络设置 2.在Wlan属性分享页面中选取对应的网络设置,然后回到属性页面勾选Hy ...

  3. Vitess全局唯一ID生成的实现方案

    为了标识一段数据,通常我们会为其指定一个唯一id,比如利用MySQL数据库中的自增主键. 但是当数据量非常大时,仅靠数据库的自增主键是远远不够的,并且对于分布式数据库只依赖MySQL的自增id无法满足 ...

  4. 利用SPI实现全自动化——LCD屏与RGB灯

    如果你开启了广告屏蔽,请将博客园加入白名单,帮助博客园渡过难关,谢谢! 前言 在21年做物理实验和23年客串电赛之后,我带着STM32重回电子DIY界.这次的项目是一个电池供电的补光灯,由于用途更偏向 ...

  5. 02-Shell变量

    1.Shell变量 1.1 Shell变量的介绍 变量用于存储管理临时的数据, 这些数据都是在运行内存中的. 1.2 变量类型 系统环境变量 自定义变量 特殊符号变量 2.系统环境变量 2.1 介绍 ...

  6. Python基础知识——函数的基本使用、函数的参数、名称空间与作用域、函数对象与闭包、 装饰器、迭代器、生成器与yield、函数递归、面向过程与函数式(map、reduce、filter)

    文章目录 1 函数的基本使用 一 引入 二 定义函数 三 调用函数与函数返回值 2 函数的参数 一 形参与实参介绍 二 形参与实参的具体使用 2.1 位置参数 2.2 关键字参数 2.3 默认参数 2 ...

  7. Gossip in Hyperledger Fabric

    1. Gossip协议基础 1.1 什么是分布式系统 分布式系统(Distributed System)是由多台计算机或计算节点组成的计算机系统,这些计算节点通过网络连接在一起,并协同工作以完成共同的 ...

  8. 栈溢出-GOT表劫持测试

    1.目标程序源代码 char name[64]; int main(){ int unsigned long long addr; setvbuf(stdin,0,2,0); setvbuf(stdo ...

  9. 浅谈Python异步编程

    1. 异步编程概述 异步编程是一种编程范式,用于处理那些需要等待I/O操作完成或者耗时任务的情况.在传统的同步编程中,代码会按照顺序逐行执行,直到遇到一个耗时操作,它会阻塞程序的执行直到操作完成.这种 ...

  10. JVM 学习

    目录 1. 类加载器及类加载过程 1.1 基本流程 1.2 类加载器子系统作用 1.3 类加载器角色 1.4 加载过程 (1) 加载 loading (2) 链接 linking 验证 verify ...