作者 | Mara Bos,Rust资深工程师译者 | Arvin,编辑 | 屠敏来源 | CSDN(ID:CSDNnews)大约一年前,我发布了一个名为inline-python(https://crates.io/crates/inline-python)的Rust类库,它允许大家使用python!{ .. }宏轻松地将一些Python混合到Rust代码中。在本系列中,我将从头展示开发此类库的过程。预览如果不熟悉inline-python类库,你可以执行以下操作:

fn main() {let who = "world";let n = 5;    python! {for i in range('n):            print(i, "Hello", 'who)print("Goodbye")    }}

它允许你将Python代码直接嵌入Rust代码行之间,甚至直接在Python代码中使用Rust变量。我们将从一个比这个简单得多的案例开始,然后逐步努力以达到这个结果(甚至更多!)。运行Python代码首先,让我们看一下如何在Rust中运行Python代码。让我们尝试使第一个简单的示例生效:

fn main() {    println!("Hello ...");    run_python("print(\"... World!\")");}

我们可以使用std::process::命令来运行python可执行文件并传递python代码,从而实现run_python,但如果我们希望能够定义和读回Python变量,那么最好从使用PyO3库开始。PyO3为我们提供了Python的Rust绑定。它很好地包装了Python C API,使我们可以直接在Rust中与各种Python对象交互。(甚至在Rust中编写Python库,但这是另一个主题。)它的Python::run 功能完全符合我们的需求。它将Python代码作为&str,并允许我们使用两个可选的PyDicts 来定义范围内的任何变量。让我们试一试吧:

fn run_python(code: &str) {    let py = pyo3::Python::acquire_gil(); // Acquire the 'global interpreter lock', as Python is not thread-safe.    py.python().run(code, None, None).unwrap(); // No locals, no globals.}
$ cargo run   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.29s     Running `target/debug/scratchpad`Hello ...... World!

看,这就成功了!基于规则的宏在字符串中编写Python不是最便捷的方法,所以我们尝试改进它。宏允许我们在Rust中自定义语法,所以让我们尝试一下:

fn main() {println!("Hello ...");    python! {print("... World!")    }}

宏通常是使用macro_rules!进行定义,您可以基于标记和表达式之类的内容使用高级“查找和替换”规则来定义宏。(有关macro_rules!的介绍请参见Rust Book中有关宏的章节,有关Rust宏所有的细节都可以在《Rust宏的小书》中找到。)由macro_rules!定义的宏在编译时无法执行任何代码,这些宏仅是应用了基于模式的替换规则。它非常适合vec![],甚至是lazy_static!{ .. },但对于解析和编译正则表达式(例如regex!("a.*b"))之类的功能而言,还不够强大。在宏的匹配规则中,我们可以匹配表达式,标识符,类型和许多其他内容。由于“有效的Python代码”不是一个选项,所以我们只能让宏接受所有内容:大量的原始的符号:

macro_rules! python {    ($($code:tt)*) => {        ...    }}

(有关macro_rules!工作原理的详细信息,请参见上面链接的资源。)对宏的调用应该产生run_python(".."),这是一个包裹了所有Python代码的字符串文本。幸运的是:有一个内建宏为我们把内容放到一个字符串里,叫做stringify!,因此我不必从头开始。

macro_rules! python {    ($($code:tt)*) => {        run_python(stringify!($($code)*));    }}

结果如下:

$ cargo r   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.32s     Running `target/debug/scratchpad`Hello ...... World!

如愿以偿得到了期望结果!但是,如果我们有不止一行的Python代码会怎样?

fn main() {println!("Hello ...");    python! {print("... World!")print("Bye.")    }}
$ cargo r   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.31s     Running `target/debug/scratchpad`Hello ...thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: PyErr { type: Py(0x7f1c0a5649a0, PhantomData) }', src/main.rs:9:5note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

很不幸,我们失败了。为了进行调试,我们需要正确输出PyErr,并显示我们传递给Python::run的确切Python代码:

fn run_python(code: &str) {println!("-----");println!("{}", code);println!("-----");    let py = pyo3::Python::acquire_gil();if let Err(e) = py.python().run(code, None, None) {        e.print(py.python());    }}
$ cargo r   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.27s     Running `target/debug/scratchpad`Hello ...-----print("... World!") print("Bye.")-----  File "", line 1print("... World!") print("Bye.")                        ^SyntaxError: invalid syntax

很显然,两行Python代码落在同一行,在Python中这是无效的语法。现在我们遇到了必须克服的最大问题:stringify!把空白符搞乱了.

空白符和符号让我们仔细研究一下stringify!:

fn main() {    println!("{}", stringify!(        a 123    b   cx ( y + z )// comment        ...    ));}
$ cargo r   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.21s     Running `target/debug/scratchpad`a 123 b c x(y + z) ...

它不仅删除了所有不必要的空格,还删除了注释。因为它的工作原理是处理单词(token),不再是源代码里面的:a,123,b等。Rustc编译器做的第一件事就是将源代码分为单词,这使得解析后的工作更容易进行,不必处理诸如1,2,3,这样的个别字符,只需处理诸如“integer literal 123”这样的单词。另外,空白和注释在分词之后就消失了,因为它们对编译器来说没有意义。stringify!()是一种将一串单词转换回字符串的方法,但它是基于“最佳效果”的:它将单词转换回文本,并且仅在需要时才在单词周围插入空格(以避免将b、c转换为bc)。所以这是一个死胡同。Rustc不小心把宝贵的空白符丢掉了,但这在Python中非常重要。我们可以尝试猜测一下哪些代码的空格必须用换行符代替,但是缩进肯定会成为一个问题:

fn main() {let a = stringify!(if False:            x()        y()    );let b = stringify!(if False:            x()            y()    );    dbg!(a);    dbg!(b);    dbg!(a == b);}
$ cargo r   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.20s     Running `target/debug/scratchpad`[src/main.rs:12] a = "if False : x() y()"[src/main.rs:13] b = "if False : x() y()"[src/main.rs:14] a == b = true

这两个Python代码片段有不同的含义,但是stringify!给了我们相同的结果。在放弃之前,让我们尝试一下其他类型的宏。过程宏Rust的过程宏是定义宏的另一种方法。尽管macro_rules!只能定义“函数样式的宏”(带有!标记的宏),过程宏也可以定义自定义派生宏(例如#[derive(Stuff)])和属性宏(例如#[stuff])。过程宏是作为编译器插件实现的。您需要编写一个函数,该函数可以访问编译器看到的单词流,然后就可以执行所需的任何操作,最后需要返回一个新的单词流供编译器使用(或者用于自定义的用途):

#[proc_macro]pub fn python(input: TokenStream) -> TokenStream {    todo!()}

上述单词流不够好。因为我们需要源代码,而不仅仅是单词。虽然目前还没有成功,但是让我们继续吧,也许过程宏更大的灵活性能够解决问题。由于过程宏在编译过程中运行Rust代码,因此它们需要使用单独的proc-macro类库中,这个类库在您编译其他内容之前已经被编译好。

$ cargo new --lib python-macro     Created library `python-macro` package

查看python-macro/Cargo.toml:

[lib]proc-macro = true

查看Cargo.toml:

[dependencies]python-macro = { path = "./python-macro" }

让我们从一个只有panics (todo!())的实现开始,在输出TokenStream之后:

// python-macro/src/lib.rsextern crate proc_macro;use proc_macro::TokenStream;#[proc_macro]pub fn python(input: TokenStream) -> TokenStream {    dbg!(input.to_string());    todo!()}
// src/main.rsuse python_macro::python;fn main() {    println!("Hello ...");    python! {print("... World!")print("Bye.")    }}
$ cargo r   Compiling python-macro v0.1.0   Compiling scratchpad v0.1.0error[E0658]: procedural macros cannot be expanded to statements --> src/main.rs:5:5  |5 | /     python! {6 | |         print("... World!")7 | |         print("Bye.")8 | |     }  | |_____^  |  = note: see issue #54727  for more information  = help: add `#![feature(proc_macro_hygiene)]` to the crate attributes to enable

天啊,这里发生了什么?Rust错误为“ 过程宏不能扩展为语句 ”,以及有关启用“hygienic macros”的内容。Macro hygiene是Rust宏的出色功能,不会意外地将任何名称“泄漏”给外界(反之亦然)。如果宏扩展使用了名为的x的临时变量,则它将与宏外部的任何代码中出现的变量x分开。但是,此功能对于过程宏还不稳定。因此,过程宏除了作为一个单独的项(例如在文件范围内,但不在函数内)之外,不允许出现在任何地方。接下来,我们会发现存在一个非常可怕但令人着迷的解决方法—让我们启用实验功能#![feature(proc_macro_hygiene)]并继续我们的冒险。(如果你将来读到这篇文章时,proc_macro_hygiene已经稳定下来了:你可以跳过最后几段。^ ^)

$ sed -i '1i#![feature(proc_macro_hygiene)]' src/main.rs$ cargo r   Compiling scratchpad v0.1.0[python-macro/src/lib.rs:6] input.to_string() = "print(\"... World!\") print(\"Bye.\")"error: proc macro panicked--> src/main.rs:6:5  |6 | /     python! {7 | |         print("... World!")8 | |         print("Bye.")9 | |     }  | |_____^  |  = help: message: not yet implementederror: aborting due to previous errorerror: could not compile `scratchpad`.

在向我们展示了它的字符串输入参数之后,我们的过程宏即如预期般地崩溃了:

print("... World!") print("Bye.")

正如预期的那样,空白符再次被丢弃了。:(是时候选择放弃了。不过或者..也许有一种方法可以解决这个问题。

重建空白符尽管rustc编译器只在解析和编译时使用单词,但是在某种程度上它仍然可以准确地知道何时报告错误。单词中没有换行符,但是它仍然知道我们的错误发生在第6到第9行。那它如何做到的?事实证明,单词中包含很多信息。它们包含一个Span,是单词在源文件中的开始和结束的位置。Span可以告诉单词在哪个文件、行和列编号处开始和结束。如果我们能够得到这些信息,我们就可以通过在单词之间放置空格和换行符来重新构造空白符,以匹配它们的行和列信息。提供这些信息的函数还不稳定,而且还没有#![feature(proc_macro_span)]。让我们启用它,看看我们得到了什么:

#![feature(proc_macro_span)]extern crate proc_macro;use proc_macro::TokenStream;#[proc_macro]pub fn python(input: TokenStream) -> TokenStream {for t in input {        dbg!(t.span().start());    }    todo!()}
$ cargo r   Compiling python-macro v0.1.0   Compiling scratchpad v0.1.0[python-macro/src/lib.rs:9] t.span().start() = LineColumn {    line: 7,    column: 8,}[python-macro/src/lib.rs:9] t.span().start() = LineColumn {    line: 7,    column: 13,}[python-macro/src/lib.rs:9] t.span().start() = LineColumn {    line: 8,    column: 8,}[python-macro/src/lib.rs:9] t.span().start() = LineColumn {    line: 8,    column: 13,}

真棒!我们得到了一些数据。但是只有四个单词了。原来("... World!") 这里只出现一个单词,而不是三个((,"... World!",和))。如果看一下TokenStream的文档,我们会发现它并没有提供单词流,而是单词树。显然,Rust的词法分析器已经匹配了括号(以及大括号和方括号),并且它不仅给出了线性的单词列表,而且还给出了单词树。括号内的单词可以看成是某个单词组的后代。让我们修改过程宏以递归地遍历组内的所有单词(并改进一下输出):

#[proc_macro]pub fn python(input: TokenStream) -> TokenStream {print(input);    todo!()}fn print(input: TokenStream) {for t in input {if let TokenTree::Group(g) = t {            println!("{:?}: open {:?}", g.span_open().start(), g.delimiter());print(g.stream());            println!("{:?}: close {:?}", g.span_close().start(), g.delimiter());        } else {            println!("{:?}: {}", t.span().start(), t.to_string());        }    }}
$ cargo r   Compiling python-macro v0.1.0   Compiling scratchpad v0.1.0LineColumn { line: 7, column: 8 }: printLineColumn { line: 7, column: 13 }: open ParenthesisLineColumn { line: 7, column: 14 }: "... World!"LineColumn { line: 7, column: 26 }: close ParenthesisLineColumn { line: 8, column: 8 }: printLineColumn { line: 8, column: 13 }: open ParenthesisLineColumn { line: 8, column: 14 }: "Bye."LineColumn { line: 8, column: 20 }: close Parenthesis

符合预期,太棒了!现在要重建空白符,如果我们不在正确的行中,我们需要插入换行符,如果我们不在正确的列中,则需要插入空格。让我们来看看效果:

#![feature(proc_macro_span)]extern crate proc_macro;use proc_macro::{TokenTree, TokenStream, LineColumn};#[proc_macro]pub fn python(input: TokenStream) -> TokenStream {    let mut s = Source {        source: String::new(),        line: 1,        col: 0,    };    s.reconstruct_from(input);    println!("{}", s.source);    todo!()}struct Source {    source: String,    line: usize,    col: usize,}impl Source {    fn reconstruct_from(&mut self, input: TokenStream) {for t in input {if let TokenTree::Group(g) = t {                let s = g.to_string();self.add_whitespace(g.span_open().start());self.add_str(&s[..1]); // the '[', '{' or '('.self.reconstruct_from(g.stream());self.add_whitespace(g.span_close().start());self.add_str(&s[s.len() - 1..]); // the ']', '}' or ')'.            } else {self.add_whitespace(t.span().start());self.add_str(&t.to_string());            }        }    }    fn add_str(&mut self, s: &str) {// Let's assume for now s contains no newlines.self.source += s;self.col += s.len();    }    fn add_whitespace(&mut self, loc: LineColumn) {while self.line             self.source.push('\n');self.line += 1;self.col = 0;        }while self.col             self.source.push(' ');self.col += 1;        }    }}
$ cargo r   Compiling python-macro v0.1.0   Compiling scratchpad v0.1.0print("... World!")print("Bye.")error: proc macro panicked

看来这是行得通的,但是这些额外的换行符和空格又是怎么回事?对比下源文件,这是对的,第一个标记从第7行第8列开始,因此它正确地将print放在第8列的第7行。我们要查找的位置正是.rs文件中的确切位置。开始时多余的换行符不是问题(空行在Python中无效)。它甚至具有很好的副作用:当Python报告错误时,它报告的行号将与.rs文件中的行号匹配。但是,这8个空格是个问题。尽管我们内部的Python代码python!{..}相对于Rust代码是适当缩进的,但我们提取的Python代码应以“零”缩进级别开始。否则,Python将发生无效缩进的错误。让我们从所有列号中减去第一个标记的列号:

start_col: None,//     start_col: Option,//     let start_col = *self.start_col.get_or_insert(loc.column);    let col = loc.column.checked_sub(start_col).expect("Invalid indentation.");while self.col         self.source.push(' ');self.col += 1;    }// 
$ cargo r   Compiling python-macro v0.1.0   Compiling scratchpad v0.1.0print("... World!")print("Bye.")error: proc macro panicked

结果太棒了!现在,我们只需要把这个字符串转换为字符串文字标记 并将其放在run_python();周围即可:

 TokenStream::from_iter(vec![        TokenTree::from(Ident::new("run_python", Span::call_site())),        TokenTree::Group(Group::new(            Delimiter::Parenthesis,            TokenStream::from(TokenTree::from(Literal::string(&s.source))),        )),        TokenTree::from(Punct::new(';', Spacing::Alone)),    ])

太糟糕了,直接使用TokenTree太困难了,尤其是从头开始制作trees和streams。如果只有一种方法可以编写我们要生成的Rust代码,那就只能是quote类库的quote!宏:

    let source = s.source;    quote!( run_python(#source); ).into()

现在使用我们的原始run_python函数对其进行测试:

#![feature(proc_macro_hygiene)]use python_macro::python;fn run_python(code: &str) {    let py = pyo3::Python::acquire_gil();if let Err(e) = py.python().run(code, None, None) {        e.print(py.python());    }}fn main() {    println!("Hello ...");    python! {print("... World!")print("Bye.")    }}
$ cargo r   Compiling scratchpad v0.1.0    Finished dev [unoptimized + debuginfo] target(s) in 0.31s     Running `target/debug/scratchpad`Hello ...... World!Bye.

终于成功了!封装成类库现在我们把它变成一个可重用的库:

  • 删除fn main,

  • 重命名main.rs为lib.rs,

  • 给类库起个好名字,例如inline-python,

  • 公开run_python,

  • 更改quote!()中的run_python调用改为::inline_python::run_python,同时添加pub python_macro::python;从python!这个类库中重新导出宏。

下一步计划可能还有很多内容需要改进,还有很多错误需要发现,但是至少我们现在可以在Rust代码行之间运行Python片段了。目前最大的问题是,这还不是很有用,因为没有数据可以(轻松)越过Rust-Python的边界。在第2部分中,我们将研究如何使Rust变量用于Python代码。更新:在等待第2部分的同时,还有第1A部分,只是它没有改进我们的python!{}宏,但涉及了人们向我询问的一些细节。具体来说,它涉及:

  • 为什么要像这样在Rust内部使用Python,

  • 语法问题,例如使用Python的单引号字符串

  • 使用Span::source_text的选项,当我第一次编写这段代码时,它其实还不存在。

原文链接:https://blog.m-ou.se/writing-python-inside-rust-1/

推荐阅读

  • 360金融首席科学家张家兴:别指望AI Lab做成中台
  • 我们想研发一个机器学习框架,6 个月后失败了

  • 八年,腾讯优图攒了多厚的技术“家底”?
  • 无需训练 RNN 或生成模型,如何编写一个快速且通用的 AI “讲故事”项目?
  • 区块链重大技术分析:IBM、微软、苹果、Google 都做了什么?

你点的每个“在看”,我都认真当成了AI

c++hello world代码_在Rust代码中编写Python是种怎样的体验?相关推荐

  1. 我们编写的python代码在运行过程中_在Rust代码中编写Python是种怎样的体验?

    原标题:在Rust代码中编写Python是种怎样的体验? 作者 | Mara Bos,Rust资深工程师 译者 | Arvin,编辑 | 屠敏 来源 | CSDN(ID:CSDNnews) 大约一年前 ...

  2. c++hello world代码_在 Rust 代码中编写 Python 是种怎样的体验?

    作者 | Mara Bos,Rust资深工程师译者 | Arvin 责编 | 屠敏头图 | CSDN 下载自东方 IC出品 | CSDN(ID:CSDNnews) 以下为译文: 大约一年前,我发布了一 ...

  3. 在Rust代码中编写Python是种怎样的体验?

    作者 | Mara Bos,Rust资深工程师 译者 | Arvin,编辑 | 屠敏 来源 | CSDN(ID:CSDNnews) 大约一年前,我发布了一个名为inline-python(https: ...

  4. python调用rust_在 Rust 代码中编写 Python 是种怎样的体验?

    本文为转载摘要,完整版请移步: https://mp.weixin.qq.com/s/YT_HNFDCQ_IyocvBkRNJnA 以下为译文: 大约一年前,我发布了一个名为inline-python ...

  5. 在 Rust 代码中编写 Python 是种怎样的体验?

    作者 | Mara Bos,Rust资深工程师 译者 | Arvin 责编 | 屠敏 头图 | CSDN 下载自东方 IC 出品 | CSDN(ID:CSDNnews) 以下为译文: 大约一年前,我发 ...

  6. pycharm怎么编写python代码_如何设置PyCharm中的Python代码模版(推荐)

    在MacOs运行的PyCharm中,执行python文件,如果不指定python文件字符编码会报错: SyntaxError: Non-ASCII character '\xe6' in file / ...

  7. 在java中柱状图代码_我在java中编写了个柱状图,可运行了,我想让柱状图在JSP页面中显示,请问有什么方法么?谢谢。...

    h1,h2 代表了柱形图的高度 你可以这样试一试 function createImgItem(count){ var div=document.createElement("") ...

  8. python第一行代码_“少年py”001:下载Python软件,写第一行代码

    Python,网络上称之为人工智能时代的第一编程语言. 功能超级强大,能做科学计算.大数据处理.网络爬虫.游戏开发等等. 但是说实话,彬哥玩Python还没到这么厉害的程度,究竟怎么实现,我们不着急, ...

  9. android studio调用python,Android studio中编写Python代码-2

    Chaquopy 教程 Chaquopy Chaquopy的作用:使用Chaquopy在Android Studio添加Python环境,java和Python互调 目前调试后APP可以正常运行(20 ...

最新文章

  1. C# winform 多线程中创建等待窗体
  2. svm算法 java实现_SVM算法实现(一)
  3. 魅族17系列正式发布:17年梦想之作,3699元起
  4. Android 中东阿拉伯语适配,看这一篇够了
  5. 【托业】【新托业TOEIC新题型真题】学习笔记13-题库四-P7
  6. 163个人邮箱注册申请流程,公司邮箱怎么注册?
  7. 2015年4月21日---开始写自己的专业博客啦
  8. 海贼王剧场版:Z 剧情详解(附TS无字幕版地址)
  9. 【专利】如何画专利流程图(逻辑图)
  10. HTTP协议5之代理--转
  11. 《必然》二、奔跑吧,所有人都是菜鸡
  12. 搞事情 | 大数据文摘和ta的朋友们:环游世界的80天
  13. CorelDRAW图片导出变色,如何解决?
  14. python滤波与图像去噪
  15. 解决导出Excel报COM类工厂错误的办法--修改版
  16. 信签纸有虚线怎么写_中间有虚线的稿纸,字要写在哪里
  17. MV3D:Multi-View 3D Object Detection Network for Autonomous Driving(翻译)
  18. LINUX——shell脚本编程
  19. 罗马时钟代码 jquery
  20. python之lxml(xpath)

热门文章

  1. 【Java】44个Java代码性能优化总结
  2. 83-spark2.2的DataFrame使用以及以前版本SqlContext的隐式引用
  3. 60-150-040-使用-Sink-Flink自定义UpsertStreamTableSink
  4. 95-130-342-源码-source-kafka相关-AbstractPartitionDiscoverer
  5. 2021年3月程序员工资统计,平均15189元,又涨了
  6. 给文件夹中的文件批量更改名称
  7. Docker多主机安装Zookeeper集群
  8. MySQL高级-索引的使用及优化
  9. golang实现四种排序(快速,冒泡,插入,选择)
  10. Vue.js 源码分析(九) 基础篇 生命周期详解