c++hello world代码_在 Rust 代码中编写 Python 是种怎样的体验?
以下為譯文:
大約一年前,我發布了一個名為inline-python(https://crates.io/crates/inline-python)的Rust類庫,它允許大家使用python!{ .. }宏輕松地將一些Python混合到Rust代碼中。在本系列中,我將從頭展示開發此類庫的過程。
預覽
如果不熟悉inline-python類庫,你可以執行以下操作:
main()?{它允許你將Python代碼直接嵌入Rust代碼行之間,甚至直接在Python代碼中使用Rust變量。
我們將從一個比這個簡單得多的案例開始,然后逐步努力以達到這個結果(甚至更多!)。
運行Python代碼
首先,讓我們看一下如何在Rust中運行Python代碼。讓我們嘗試使第一個簡單的示例生效:
fn?main()?{我們可以使用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:5
note:?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?1
????print("...?World!")?print("Bye.")
????????????????????????^
SyntaxError:?invalid?syntax
很顯然,兩行Python代碼落在同一行,在Python中這是無效的語法。
現在我們遇到了必須克服的最大問題:stringify!把空白符搞亂了.
空白符和符號
讓我們仔細研究一下stringify!:
fn?main()?{$?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]上述單詞流不夠好。因為我們需要源代碼,而不僅僅是單詞。雖然目前還沒有成功,但是讓我們繼續吧,也許過程宏更大的靈活性能夠解決問題。
由于過程宏在編譯過程中運行Rust代碼,因此它們需要使用單獨的proc-macro類庫中,這個類庫在您編譯其他內容之前已經被編譯好。
new?--lib?python-macro查看python-macro/Cargo.toml:
[lib]查看Cargo.toml:
[dependencies]讓我們從一個只有panics (todo!())的實現開始,在輸出TokenStream之后:
//?python-macro/src/lib.rs//?src/main.rs$?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?|?|?????????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已經穩定下來了:你可以跳過最后幾段。^ ^)
'1i#![feature(proc_macro_hygiene)]'?src/main.rs在向我們展示了它的字符串輸入參數之后,我們的過程宏即如預期般地崩潰了:
print(正如預期的那樣,空白符再次被丟棄了。:(
是時候選擇放棄了。
不過或者..也許有一種方法可以解決這個問題。
重建空白符
盡管rustc編譯器只在解析和編譯時使用單詞,但是在某種程度上它仍然可以準確地知道何時報告錯誤。單詞中沒有換行符,但是它仍然知道我們的錯誤發生在第6到第9行。那它如何做到的?
事實證明,單詞中包含很多信息。它們包含一個Span,是單詞在源文件中的開始和結束的位置。Span可以告訴單詞在哪個文件、行和列編號處開始和結束。
如果我們能夠得到這些信息,我們就可以通過在單詞之間放置空格和換行符來重新構造空白符,以匹配它們的行和列信息。
提供這些信息的函數還不穩定,而且還沒有#![feature(proc_macro_span)]。讓我們啟用它,看看我們得到了什么:
#![feature(proc_macro_span)]$?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.0
LineColumn?{?line:?7,?column:?8?}:?print
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?}:?print
LineColumn?{?line:?8,?column:?13?}:?open?Parenthesis
LineColumn?{?line:?8,?column:?14?}:?"Bye."
LineColumn?{?line:?8,?column:?20?}:?close?Parenthesis
符合預期,太棒了!
現在要重建空白符,如果我們不在正確的行中,我們需要插入換行符,如果我們不在正確的列中,則需要插入空格。讓我們來看看效果:
#![feature(proc_macro_span)]$?cargo?r???Compiling?python-macro?v0.1.0
???Compiling?scratchpad?v0.1.0
????????print("...?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.0
print("...?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?現在使用我們的原始run_python函數對其進行測試:
#![feature(proc_macro_hygiene)]$?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/
本文為 CSDN 翻譯,轉載請注明來源出處。
更多精彩推薦
?AI 世界的硬核之戰,Tengine 憑什么成為最受開發者歡迎的主流框架?
?說了這么多 5G,最關鍵的技術在這里
?360金融新任首席科學家:別指望AI Lab做成中臺
?AI圖像智能修復老照片,效果驚艷到我了
?程序員內功修煉系列:10 張圖解談 Linux 物理內存和虛擬內存
?當 DeFi 遇上 Rollup,將擦出怎樣的火花?
你點的每個“在看”,我都認真當成了喜歡總結
以上是生活随笔為你收集整理的c++hello world代码_在 Rust 代码中编写 Python 是种怎样的体验?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 港币兑人民币去哪换
- 下一篇: s3c2440移植MQTT