作者 | Mara Bos,Rust资深工程师
译者 | Arvin 责编 | 屠敏
头图 | CSDN 下载自东方 IC
出品 | CSDN(ID:CSDNnews)
以下为译文:
大约一年前,我发布了一个名为inline-python(
https://crates.io/crates/inline-python)的Rust类库,它允许大家使用python!{ .. }宏轻松地将一些Python混合到Rust代码中。在本系列中,我将从头展示开发此类库的过程。
预览
如果不熟悉inline-python类库,你可以执行以下操作:
<code>fn main {let
who ="world"
;let
n =5
;
python! {for
iin
range('n):"Hello"
, 'who)"Goodbye"
)
}
}/<code>
它允许你将Python代码直接嵌入Rust代码行之间,甚至直接在Python代码中使用Rust变量。
我们将从一个比这个简单得多的案例开始,然后逐步努力以达到这个结果(甚至更多!)。
运行Python代码
首先,让我们看一下如何在Rust中运行Python代码。让我们尝试使第一个简单的示例生效:
<code>fn
main
{println
!("Hello ..."
);run_python
("print("
... World!\")"
);
}/<code>
我们可以使用std::process::命令来运行python可执行文件并传递python代码,从而实现run_python,但如果我们希望能够定义和读回Python变量,那么最好从使用PyO3库开始。
PyO3为我们提供了Python的Rust绑定。它很好地包装了Python C API,使我们可以直接在Rust中与各种Python对象交互。(甚至在Rust中编写Python库,但这是另一个主题。)
它的Python::run 功能完全符合我们的需求。它将Python代码作为&str,并允许我们使用两个可选的PyDicts 来定义范围内的任何变量。让我们试一试吧:
<code>fn
run_python
(code: &str
) {let
py = pyo3::Python::acquire_gil;
/<code>
<code>$ cargo run
Compiling scratchpad v0
.1.0
Finished dev [unoptimized + debuginfo] target(s)in
0
.29
s
Running`target/debug/scratchpad`
Hello ...
... World!
/<code>
看,这就成功了!
基于规则的宏
在字符串中编写Python不是最便捷的方法,所以我们尝试改进它。宏允许我们在Rust中自定义语法,所以让我们尝试一下:
<code>fn main {println
!("Hello ..."
);
python! {"... World!"
)
}
}/<code>
宏通常是使用macro_rules!进行定义,您可以基于标记和表达式之类的内容使用高级“查找和替换”规则来定义宏。(有关macro_rules!的介绍请参见Rust Book中有关宏的章节,有关Rust宏所有的细节都可以在《Rust宏的小书》中找到。)
由macro_rules!定义的宏在编译时无法执行任何代码,这些宏仅是应用了基于模式的替换规则。它非常适合vec!,甚至是lazy_static!{ .. },但对于解析和编译正则表达式(例如regex!("a.*b"))之类的功能而言,还不够强大。
在宏的匹配规则中,我们可以匹配表达式,标识符,类型和许多其他内容。由于“有效的Python代码”不是一个选项,所以我们只能让宏接受所有内容:大量的原始的符号:
<code>macro_rules! python {
($($code:tt
)*) => {
...
}
}/<code>
(有关macro_rules!工作原理的详细信息,请参见上面链接的资源。)
对宏的调用应该产生run_python(".."),这是一个包裹了所有Python代码的字符串文本。幸运的是:有一个内建宏为我们把内容放到一个字符串里,叫做stringify!,因此我不必从头开始。
<code>macro_rules! python {
($($code:tt
)*) => {
run_python(stringify!($($code)*));
}
}/<code>
结果如下:
<code>$ cargo r
Compiling scratchpad v0
.1.0
Finished dev [unoptimized + debuginfo] target(s)in
0
.32
s
Running`target/debug/scratchpad`
Hello ...
... World!/<code>
如愿以偿得到了期望结果!
但是,如果我们有不止一行的Python代码会怎样?
<code>fn main {println
!("Hello ..."
);
python! {"... World!"
)"Bye."
)
}
}
/<code>
<code>$ cargo r
Compiling scratchpad v0.1
.0
Finished dev [unoptimized + debuginfo] target(s)in
0.31
s
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
:5
n
ote: runwith
`RUST_BACKTRACE=1`
environment variable to display a backtrace
/<code>
很不幸,我们失败了。
为了进行调试,我们需要正确输出PyErr,并显示我们传递给Python::run的确切Python代码:
<code>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);
}
}
/<code>
<code>$ cargo r
Compiling scratchpad v0.1
.0
Finished dev [unoptimized + debuginfo] target(s) in0.27
s
Running`target/debug/scratchpad`
Hello ...
-----"... World!"
)"Bye."
)
-----
File""
, line1
"... World!"
)"Bye."
)
^
SyntaxError: invalid syntax
/<code>
很显然,两行Python代码落在同一行,在Python中这是无效的语法。
现在我们遇到了必须克服的最大问题:stringify!把空白符搞乱了.
空白符和符号
让我们仔细研究一下stringify!:
<code>
fn
main
{println
!("{}"
,stringify!
(
a123
b c
x ( y + z )
/<code>
<code>$ cargo r
Compiling scratchpad v0
.1.0
Finished dev [unoptimized + debuginfo] target(s)in
0
.21
s
Running`target/debug/scratchpad`
a123
b c x(y + z) .../<code>
它不仅删除了所有不必要的空格,还删除了注释。因为它的工作原理是处理单词(token),不再是源代码里面的:a,123,b等。
Rustc编译器做的第一件事就是将源代码分为单词,这使得解析后的工作更容易进行,不必处理诸如1,2,3,这样的个别字符,只需处理诸如“integer literal 123”这样的单词。另外,空白和注释在分词之后就消失了,因为它们对编译器来说没有意义。
stringify!是一种将一串单词转换回字符串的方法,但它是基于“最佳效果”的:它将单词转换回文本,并且仅在需要时才在单词周围插入空格(以避免将b、c转换为bc)。
所以这是一个死胡同。Rustc不小心把宝贵的空白符丢掉了,但这在Python中非常重要。
我们可以尝试猜测一下哪些代码的空格必须用换行符代替,但是缩进肯定会成为一个问题:
<code>fn
main
{let
a
=stringify
!(if
False:
x
y
);let
b =stringify!
(if
False:
x
y
);
dbg!(a);
dbg!(b);
dbg!(a == b);
}
/<code>
<code>$ cargo r
Compiling scratchpad v0
.1.0
Finished dev [unoptimized + debuginfo] target(s)in
0
.20
s
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
/<code>
这两个Python代码片段有不同的含义,但是stringify!给了我们相同的结果。
在放弃之前,让我们尝试一下其他类型的宏。
过程宏
Rust的过程宏是定义宏的另一种方法。尽管macro_rules!只能定义“函数样式的宏”(带有!标记的宏),过程宏也可以定义自定义派生宏(例如#[derive(Stuff)])和属性宏(例如#[stuff])。
过程宏是作为编译器插件实现的。您需要编写一个函数,该函数可以访问编译器看到的单词流,然后就可以执行所需的任何操作,最后需要返回一个新的单词流供编译器使用(或者用于自定义的用途):
<code>pub
fn
python
(input: TokenStream) -> TokenStream {
todo!
}/<code>
上述单词流不够好。因为我们需要源代码,而不仅仅是单词。虽然目前还没有成功,但是让我们继续吧,也许过程宏更大的灵活性能够解决问题。
由于过程宏在编译过程中运行Rust代码,因此它们需要使用单独的proc-macro类库中,这个类库在您编译其他内容之前已经被编译好。
<code>$ cargonew
--lib python-macro
Created library`python-macro`
package
/<code>
查看python-macro/Cargo.toml:
<code>[ ]
proc-macro =true
/<code>
查看Cargo.toml:
<code>[ ]
python-macro = { path ="./python-macro"
}/<code>
让我们从一个只有panics (todo!)的实现开始,在输出TokenStream之后:
<code>// python-macro/src/lib.rs
extern crate proc_macro;use
proc_macro::TokenStream;
/<code>
<code>// src/main.rs
use python_macro::python;
fn main {
println!("Hello ..."
);
python! {"... World!"
)"Bye."
)
}
}
/<code>
<code>$ cargo r
Compiling python-macro v0.1
.0
Compiling scratchpad v0.1
.0
error[E0658]: procedural macros cannot be expanded to statements
--> src/main.rs:5:5
|
5 | /
python! {6
| |"... World!"
)7
| |"Bye."
)8
| | }
| |_____^
|
= note: see issue
/<code>
天啊,这里发生了什么?
Rust错误为“ 过程宏不能扩展为语句 ”,以及有关启用“hygienic macros”的内容。Macro hygiene是Rust宏的出色功能,不会意外地将任何名称“泄漏”给外界(反之亦然)。如果宏扩展使用了名为的x的临时变量,则它将与宏外部的任何代码中出现的变量x分开。
但是,此功能对于过程宏还不稳定。因此,过程宏除了作为一个单独的项(例如在文件范围内,但不在函数内)之外,不允许出现在任何地方。
接下来,我们会发现存在一个非常可怕但令人着迷的解决方法—让我们启用实验功能#![feature(proc_macro_hygiene)]并继续我们的冒险。
(如果你将来读到这篇文章时,proc_macro_hygiene已经稳定下来了:你可以跳过最后几段。^ ^)
<code>$ 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 error
error: could
not
compile `scratchpad`./<code>
在向我们展示了它的字符串输入参数之后,我们的过程宏即如预期般地崩溃了:
<code>"... World!"
)"Bye."
)/<code>
正如预期的那样,空白符再次被丢弃了。:(
是时候选择放弃了。
不过或者..也许有一种方法可以解决这个问题。
重建空白符
尽管rustc编译器只在解析和编译时使用单词,但是在某种程度上它仍然可以准确地知道何时报告错误。单词中没有换行符,但是它仍然知道我们的错误发生在第6到第9行。那它如何做到的?
事实证明,单词中包含很多信息。它们包含一个Span,是单词在源文件中的开始和结束的位置。Span可以告诉单词在哪个文件、行和列编号处开始和结束。
如果我们能够得到这些信息,我们就可以通过在单词之间放置空格和换行符来重新构造空白符,以匹配它们的行和列信息。
提供这些信息的函数还不稳定,而且还没有#![feature(proc_macro_span)]。让我们启用它,看看我们得到了什么:
<code>extern
crate
proc_macro;use
proc_macro::TokenStream;pub
fn
python
(input: TokenStream) -> TokenStream {for
tin
input {
dbg!(t.span.start);
}
todo!
}
/<code>
<code>$
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
,
}
/<code>
真棒!我们得到了一些数据。
但是只有四个单词了。原来("... World!") 这里只出现一个单词,而不是三个((,"... World!",和))。如果看一下TokenStream的文档,我们会发现它并没有提供单词流,而是单词树。显然,Rust的词法分析器已经匹配了括号(以及大括号和方括号),并且它不仅给出了线性的单词列表,而且还给出了单词树。括号内的单词可以看成是某个单词组的后代。
让我们修改过程宏以递归地遍历组内的所有单词(并改进一下输出):
<code>pub
fn
python
(input: TokenStream) -> TokenStream {
print(input);
todo!
}fn
for
tin
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);
}
}
}
/<code>
<code>$
cargo
r
Compiling
python-macro
v0.1.0
Compiling
scratchpad
v0.1.0
LineColumn
{
line:
7
,
column:
8
}:
LineColumn
{
line:
7
,
column:
13
}:
open
Parenthesis
LineColumn
{
line:
7
,
column:
14
}:
"... World!"
LineColumn
{
line:
7
,
column:
26
}:
close
Parenthesis
LineColumn
{
line:
8
,
column:
8
}:
LineColumn
{
line:
8
,
column:
13
}:
open
Parenthesis
LineColumn
{
line:
8
,
column:
14
}:
"Bye."
LineColumn
{
line:
8
,
column:
20
}:
close
Parenthesis
/<code>
符合预期,太棒了!
现在要重建空白符,如果我们不在正确的行中,我们需要插入换行符,如果我们不在正确的列中,则需要插入空格。让我们来看看效果:
<code>extern
crate
proc_macro;use
proc_macro::{TokenTree, TokenStream, LineColumn};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
tin
input {if
let
TokenTree::Group(g) = t {let
s = g.to_string;self
.add_whitespace(g.span_open.start);self
.add_str(&s[..1
]);
/<code>
<code>$ cargo r
Compiling python-macro v0.1
.0
Compiling scratchpad v0.1
.0
"... World!"
)"Bye."
)error
: proc macro panicked/<code>
看来这是行得通的,但是这些额外的换行符和空格又是怎么回事?对比下源文件,这是对的,第一个标记从第7行第8列开始,因此它正确地将print放在第8列的第7行。我们要查找的位置正是.rs文件中的确切位置。
开始时多余的换行符不是问题(空行在Python中无效)。它甚至具有很好的副作用:当Python报告错误时,它报告的行号将与.rs文件中的行号匹配。
但是,这8个空格是个问题。尽管我们内部的Python代码python!{..}相对于Rust代码是适当缩进的,但我们提取的Python代码应以“零”缩进级别开始。否则,Python将发生无效缩进的错误。
让我们从所有列号中减去第一个标记的列号:
<code>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 < col {self
.source.push(' '
);self
.col +=1
;
}
//
/<code>
<code>$ cargo r
Compiling python-macro v0.1
.0
Compiling scratchpad v0.1
.0
"... World!"
)"Bye."
)error
: proc macro panicked/<code>
结果太棒了!
现在,我们只需要把这个字符串转换为字符串文字标记 并将其放在run_python;周围即可:
<code> 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)),
])
/<code>
太糟糕了,直接使用TokenTree太困难了,尤其是从头开始制作trees和streams。
如果只有一种方法可以编写我们要生成的Rust代码,那就只能是quote类库的quote!宏:
<code>let
source
= s.source;
quote!( run_python(/<code>
现在使用我们的原始run_python函数对其进行测试:
<code>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."
)
}
}
/<code>
<code>$ cargo r
Compiling scratchpad v0
.1.0
Finished dev [unoptimized + debuginfo] target(s)in
0
.31
s
Running`target/debug/scratchpad`
Hello ...
... World!
Bye./<code>
终于成功了!
封装成类库
现在我们把它变成一个可重用的库,:
删除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/
本文为 CSDN 翻译,转载请注明来源出处。
☞AI 世界的硬核之战,Tengine 凭什么成为最受开发者欢迎的主流框架?
☞说了这么多 5G,最关键的技术在这里
☞360金融新任首席科学家:别指望AI Lab做成中台
☞AI图像智能修复老照片,效果惊艳到我了
☞程序员内功修炼系列:10 张图解谈 Linux 物理内存和虚拟内存
☞当 DeFi 遇上 Rollup,将擦出怎样的火花?
關鍵字: scratchpad print Rust