在上一章的講解中,我們編寫了第一個(gè)Rust示例程序"hello, world",并給出了rustc版和cargo版本。在真實(shí)開發(fā)中,我們都會(huì)使用cargo來創(chuàng)建和管理Rust包。不過,Hello, world示例非常簡單,僅僅由一個(gè)Rust源碼文件組成,而且所有源碼文件都在同一個(gè)目錄中。但真實(shí)世界中的實(shí)用Rust程序,無論是公司商業(yè)項(xiàng)目,還是一些知名的開源項(xiàng)目,甚至是一些稍復(fù)雜一些的供教學(xué)使用的示例程序,它們通??刹粫?huì)這么簡單,都有著復(fù)雜的代碼結(jié)構(gòu)。
Rust初學(xué)者在閱讀這些項(xiàng)目源碼時(shí)便仿佛進(jìn)入了迷宮,不知道該走哪條(閱讀代碼的)路徑,不知道每個(gè)目錄代表的含義,也不知道自己想看的源碼究竟在哪個(gè)目錄下。但目前市面上的Rust入門教程大多沒有重視初學(xué)者的這一問題,要么沒有對Rust項(xiàng)目代碼組織結(jié)構(gòu)進(jìn)行針對性的講解,要么是將講解放到書籍的后面章節(jié)。
根據(jù)我個(gè)人的學(xué)習(xí)經(jīng)驗(yàn)來看,理解一個(gè)實(shí)用Rust項(xiàng)目的代碼組織結(jié)構(gòu)越早,對后續(xù)的Rust學(xué)習(xí)越有益處。同時(shí),掌握Rust項(xiàng)目的代碼組織結(jié)構(gòu)也是Rust開發(fā)者走向編寫復(fù)雜Rust程序的必經(jīng)的一步。并且,初學(xué)者在了解項(xiàng)目的代碼組織結(jié)構(gòu)后,便可以自主閱讀一些復(fù)雜的Rust項(xiàng)目的源碼,可提高Rust學(xué)習(xí)的效率,提升學(xué)習(xí)效果。因此,我決定在介紹Rust基礎(chǔ)語法之前先在本章中系統(tǒng)地介紹Rust的代碼組織結(jié)構(gòu),以滿足很多Rust初學(xué)者的述求。
但在介紹Rust代碼組織結(jié)構(gòu)之前,我們需要先來系統(tǒng)說明一下Rust代碼組織結(jié)構(gòu)中的幾個(gè)重要概念,它們是了解Rust項(xiàng)目代碼組織結(jié)構(gòu)的前提。
Go項(xiàng)目代碼組織由module和package兩級組成。通常來說,每個(gè)Go repo就是一個(gè)module,由repo根目錄下的go.mod定義,go.mod文件所在目錄也被稱為module root。go.mod中典型內(nèi)容如下:
// go.modmodule github.com/user/mymodule[/vN]go 1.22.1... ...
go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一來可以反映該module的具體網(wǎng)絡(luò)位置,同時(shí)也是該module下面的Go package導(dǎo)入(import)路徑的組成部分。module root下的子目錄中通常存放著該module下面的Go package,比如module root/foo目錄下存放的Go包的導(dǎo)入路徑為github.com/user/mymodule[/vN]/foo。
Go package是Go的編譯單元,也是功能單元,代碼內(nèi)外部導(dǎo)入和引用的單位也都是包。而go module是后加入的,更多用于管理包的版本(一個(gè)module下的所有包都統(tǒng)一進(jìn)行版本管理)以及構(gòu)建時(shí)第三方依賴和版本的管理。
更多關(guān)于Go module和package管理以及Go項(xiàng)目布局的內(nèi)容,可以詳見我的極客時(shí)間《Go語言第一課》[1]專欄。
個(gè)人認(rèn)為Go的module和package的兩級管理還是很好理解和管理的,在這方面Rust的代碼組織形式又是怎樣的呢?接下來,我們就來正式看看Rust的代碼組織。
Rust是系統(tǒng)編程語言,這讓我想起了當(dāng)初在Go成為我個(gè)人主力語言之前使用C/C++進(jìn)行開發(fā)的歲月。C/C++是沒有像go或Rust的cargo那樣的統(tǒng)一的包依賴管理器和項(xiàng)目構(gòu)建管理工具的。編譯器(如gcc等)是核心工具,而項(xiàng)目構(gòu)建管理則經(jīng)常由其他工具負(fù)責(zé),如Makefile、CMake,或者是Google的Bazel[2]等。在Windows上開發(fā)應(yīng)用的,則往往使用微軟或其他開發(fā)者工具公司提供的IDE,如當(dāng)年炙手可熱的Visual Studio系列。
下面表格展示了各語言的編譯器/鏈接器和構(gòu)建管理工具的關(guān)系:
圖片
像cargo、go這樣的“一站式”工具鏈都旨在為開發(fā)者提供體驗(yàn)更為友好的交互接口的,在幕后,它們?nèi)匀灰蕾囉诘讓拥木幾g器和鏈接器(如rustc和go tool compile/link)來執(zhí)行實(shí)際的代碼編譯。
不過,像cargo這樣的高級工具也給開發(fā)人員帶來了額外的抽象,或是叫“掩蓋”了一些真相,這有時(shí)候讓人看不清構(gòu)建過程的本質(zhì),比如:很多Gopher用了很多年Go,但卻不知道go tool compile/link的存在。
本著只有in hard way,才能看到和抓住本質(zhì)的思路,以及之前學(xué)習(xí)用系統(tǒng)編程語言C/C++時(shí)經(jīng)驗(yàn),這里我們先來看一些rustc-only的Rust項(xiàng)目。Rustc-only的Rust項(xiàng)目是指不使用Cargo創(chuàng)建和管理的Rust項(xiàng)目,而是直接使用rustc編譯器來編譯和構(gòu)建項(xiàng)目。這意味著開發(fā)者需要編寫自己的構(gòu)建腳本,例如使用Makefile或其他構(gòu)建工具來管理項(xiàng)目的構(gòu)建過程。
不過,請注意:這類項(xiàng)目極少用于生產(chǎn),即便是那些不需要復(fù)雜的依賴管理的小型項(xiàng)目。這里使用rustc-only的Rust項(xiàng)目僅僅是為了學(xué)習(xí)和了解Rustc編譯器的主要功能機(jī)制以及Rust語言在代碼組織上的一些抽象,比如module等。
下面我們就從最簡單的rustc-only項(xiàng)目開始,先來看看只有一個(gè)Rust源文件且無其他依賴項(xiàng)的“最簡項(xiàng)目”。
所謂單文件項(xiàng)目,即只有一個(gè)Rust源文件,例如前面章節(jié)中的hello_world.rs,這種項(xiàng)目可以直接使用rustc編譯器來編譯和運(yùn)行:
// rust-guide-for-gopher/organizing-rust-code/rustc-only/single/hello-world/hello_world.rsfn main() { println!("Hello, world!");}
對于頂層帶有main函數(shù)的源文件,rustc會(huì)默認(rèn)將其視為binary crate類型的源文件,并將其編譯為可執(zhí)行二進(jìn)制文件hello_world。
我們當(dāng)然也可以強(qiáng)制的讓rustc將該源文件視為library crate類型的源文件,并將其編譯為其他類型的crate輸出文件,rustc支持多種crate type:
--crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro] Comma separated list of types of crates for the compiler to emit
在rustc的文檔[3]中,各種crate類型的含義如下:
lib — Generates a library kind preferred by the compiler, currently defaults to rlib.rlib — A Rust static library.staticlib — A native static library.dylib — A Rust dynamic library.cdylib — A native dynamic library.bin — A runnable executable program.proc-macro — Generates a format suitable for a procedural macro library that may be loaded by the compiler.
不過,如果強(qiáng)制將帶有頂層main函數(shù)的rust源文件視為lib crate型的,那么rustc將會(huì)報(bào)warning,提醒你函數(shù)main將是死代碼,永遠(yuǎn)不會(huì)被用到:
$rustc --crate-type lib hello_world.rswarning: function `main` is never used --> hello_world.rs:1:4 |1 | fn main() { | ^^^^ | = note: `#[warn(dead_code)]` on by defaultwarning: 1 warning emitted
但即便如此,一個(gè)名為libhello_world.rlib的文件依然會(huì)被rustc生成出來!(目前--crate-type lib等同于--create-type rlib)。
日常開發(fā)中,像上面的Hello, World級別的trivial應(yīng)用是極其少見的,一個(gè)non-trivial的Rust應(yīng)用或多或少都會(huì)有一些依賴。這里我們也來看一下如何基于rustc來構(gòu)建帶有外部依賴的單文件項(xiàng)目。下面是一個(gè)帶有外部依賴的示例:
// organizing-rust-code/rustc-only/single/hello-world-with-deps/hello_world.rsextern crate rand; use rand::Rng;fn main() { let mut rng = rand::thread_rng(); let num: u32 = rng.gen(); println!("Random number: {}", num);}
這個(gè)示例程序依賴一個(gè)名為rand的crate,要編譯該程序,我們必須先手動(dòng)下載rand的crate源碼,并在本地將rand源碼編譯為示例程序所需的rust library。下面步驟展示了如何下載和構(gòu)建rand crate:
$curl -LO https://crates.io/api/v1/crates/rand/0.8.5/download$tar -xvf download
解壓后,我們將看到rand-0.8.5這樣的一個(gè)crate目錄,進(jìn)入該目錄,我們執(zhí)行cargo build來構(gòu)建rand crate:
$cd rand-0.8.5$cargo build... ... Finished dev [unoptimized + debuginfo] target(s) in 0.19s
cargo構(gòu)建出的librand.rlib就在rand-0.8.5/target/debug下。
注:rlib的命名方式:lib+{crate_name}.rlib
接下來,我們就來構(gòu)建一下依賴rand crate的hello_world.rs:
// 在organizing-rust-code/rustc-only/single/hello-world-with-deps下面執(zhí)行$rustc --verbose -L ./rand-0.8.5/target/debug --extern rand=librand.rlib hello_world.rserror[E0463]: can't find crate for `rand_core` which `rand` depends on --> hello_world.rs:1:1 |1 | extern crate rand; | ^^^^^^^^^^^^^^^^^^ can't find crateerror: aborting due to 1 previous errorFor more information about this error, try `rustc --explain E0463`.
我們看到rustc的編譯錯(cuò)誤提示:無法找到rand crate依賴的rand_core crate!也就是說我們除了向rustc提供hello_world.rs依賴的rand crate之外,還要向rustc提供rand crate的各種依賴!
rand crate的各種依賴在哪里呢?我們在構(gòu)建rand crate時(shí),cargo build將各種依賴都放在了rand-0.8.5/target/debug/deps目錄下了:
$ls -l|grep ".rlib"-rw-r--r-- 1 tonybai staff 6896 4 29 06:45 libcfg_if-cd6bebf18fb9c234.rlib-rw-r--r-- 1 tonybai staff 204072 4 29 06:45 libgetrandom-df6a8e95e188fc56.rlib-rw-r--r-- 1 tonybai staff 1651320 4 29 06:45 liblibc-f16531562d07b476.rlib-rw-r--r-- 1 tonybai staff 959408 4 29 06:45 libppv_lite86-f1d97d485bc43617.rlib-rw-r--r-- 1 tonybai staff 1784376 4 29 06:45 librand-9a91ea8db926e840.rlib-rw-r--r-- 1 tonybai staff 987936 4 29 06:45 librand_chacha-6fe22bd8b3bb228c.rlib-rw-r--r-- 1 tonybai staff 256768 4 29 06:45 librand_core-fc905f6ca5f8533b.rlib
我們看到其中還包含了librand自身:librand-9a91ea8db926e840.rlib。我們來試試基于deps目錄下的這些依賴rlib編譯一下:
$rustc --verbose --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib hello_world.rs
我們用rustc成功編譯了帶有外部依賴的Rust源碼。不過這里要注意的是rustc對直接依賴和間接依賴的crate的定位方式有所不同。
對于直接依賴的crate,比如這里的rand crate,我們需要給出具體路徑,它不依賴-L的位置指示,所以這里我們使用了--extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib。
對于間接依賴的crate,比如rand crate依賴的rand_core,rust會(huì)結(jié)合-L指示的位置以及--extern一起來定位,這里-L指示路徑為rand-0.8.5/target/debug/deps,--extern rand_core=librand_core-fc905f6ca5f8533b.rlib,那么rustc就會(huì)在rand-0.8.5/target/debug/deps下面搜索librand_core-fc905f6ca5f8533b.rlib是否存在。
我們運(yùn)行rustc構(gòu)建出的可執(zhí)行文件,輸出如下:
$./hello_world Random number: 431751199
在Go中,如果某個(gè)目錄下有多個(gè)源文件,那么通常這幾個(gè)源文件均歸屬于同一個(gè)Go包(可能的例外的是*_test.go文件的包名)。但在Rust中,情況就會(huì)變得復(fù)雜了一些,我們來看一個(gè)例子:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps$tree -F -L 2.├── main.rs├── sub1/│ ├── bar.rs│ ├── foo.rs│ └── mod.rs└── sub2.rs
在這個(gè)示例中,我們看到除了main.rs之外,還有一個(gè)sub2.rs以及一個(gè)目錄sub1,sub1下面還有三個(gè)rs文件。我們從main.rs開始,逐一看一下各個(gè)源文件的內(nèi)容:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/main.rs 1 extern crate rand; 2 use rand::Rng; 3 4 mod sub1; 5 mod sub2; 6 7 mod sub3 { 8 pub fn func1() { 9 println!("called {}::func1()", module_path!());10 }11 pub fn func2() {12 self::func1();13 println!("called {}::func2()", module_path!());14 super::func1();15 }16 }17 18 fn func1() {19 println!("called {}::func1()", module_path!());20 }21 22 fn main() {23 println!("current module: {}", module_path!());24 let mut rng = rand::thread_rng();25 let num: u32 = rng.gen();26 println!("Random number: {}", num);27 28 sub1::func1();29 sub2::func1();30 sub3::func2();31 }
在main.rs中,我們除了看到了第1~2行的對外部rand crate的依賴外,我們還看到了一種新的語法元素:rust module。這里涉及sub1~sub3三個(gè)module,我們分別來看一下。先來看一下最直觀的、定義在main.rs中的sub3 module。
第7行~第16行的代碼定義了一個(gè)名為sub3的module,它包含兩個(gè)函數(shù)func1和func2,這兩個(gè)函數(shù)前面的pub關(guān)鍵字表明他們是sub3 module的publish函數(shù),可以被module之外的代碼所訪問。任何未標(biāo)記為pub的函數(shù)都是私有的,只能在模塊內(nèi)部及其子模塊中使用。
在sub3 module的func2函數(shù)中,我們調(diào)用了self::func1()函數(shù),self指代是模塊自身,因此這個(gè)self::func1()函數(shù)就是sub3的func1函數(shù)。而接下來調(diào)用的super::func1()調(diào)用的語義你大概也能猜到。super指代的是sub3的父模塊,而super::func1()就是sub3的父模塊中的func1函數(shù)。
sub3的父模塊就是這個(gè)項(xiàng)目的頂層模塊,我們在main函數(shù)的入口處使用module_path!宏輸出了該頂層模塊的名稱。
和sub3在main.rs中定義不同,sub1和sub2也分別代表了另外兩種module的定義方式。
當(dāng)Rust編譯器看到第4行mod sub1后,它會(huì)尋找當(dāng)前目錄下是否有名為sub1.rs的源文件或是sub1/mod.rs源文件。在這個(gè)示例中,sub1定義在sub1目錄下的mod.rs中:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/mod.rspub mod bar;pub mod foo;pub fn func1() { println!("called {}::func1()", module_path!()); foo::func1(); bar::func1();}
我們看到sub1/mod.rs中定義了一個(gè)公共函數(shù)func1,同時(shí)也在最開始處又嵌套定義了bar和foo兩個(gè)module,并在func1中調(diào)用了兩個(gè)嵌套子module的函數(shù):
bar和foo兩個(gè)module都是使用單文件module定義的,編譯器會(huì)在sub1目錄下搜尋foo.rs和bar.rs:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/foo.rspub fn func1() { println!("called {}::func1()", module_path!());}// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/bar.rspub fn func1() { println!("called {}::func1()", module_path!());}
而main.rs中的sub2也是一個(gè)單文件的module,其源碼位于頂層目錄下的sub2.rs文件中:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub2.rspub fn func1() { println!("called {}::func1()", module_path!());}
現(xiàn)在我們來編譯和執(zhí)行一下這個(gè)既有外部依賴,又是多文件且有多個(gè)module的rustc-only項(xiàng)目:
$rustc --verbose --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib main.rs $./maincurrent module: mainRandom number: 2691905579called main::sub1::func1()called main::sub1::foo::func1()called main::sub1::bar::func1()called main::sub2::func1()called main::sub3::func1()called main::sub3::func2()called main::func1()
上面示例演示了三種rust module的定義方法:
mod module_name {}
在一個(gè)單crate的項(xiàng)目中,通過rust module可以滿足項(xiàng)目內(nèi)部代碼組織的需要。
最后,我們再來看一個(gè)有多個(gè)crate的項(xiàng)目形式。
下面是一個(gè)有著多個(gè)crate項(xiàng)目的示例:
// organizing-rust-code/rustc-only/workspace$tree -L 2 -F.├── main.rs├── my_local_crate1/│ └── lib.rs└── my_local_crate2/ └── lib.rs
在這個(gè)示例中有三個(gè)crate,一個(gè)是頂層的binary類型的crate,入口為main.rs,另外兩個(gè)都是lib類型的crate,入口都在lib.rs中,我們貼一下他們的源碼:
// organizing-rust-code/rustc-only/workspace/main.rsextern crate my_local_crate1;extern crate my_local_crate2;fn main() { let x = 5; let y = my_local_crate1::add_one(x); let z = my_local_crate2::multiply_two(y); println!("Result: {}", z);}// organizing-rust-code/rustc-only/workspace/my_local_crate1/lib.rs pub fn add_one(x: i32) -> i32 { x + 1}// organizing-rust-code/rustc-only/workspace/my_local_crate2/lib.rs pub fn multiply_two(x: i32) -> i32 { x * 2}
要構(gòu)建這個(gè)帶有三個(gè)crate的項(xiàng)目,我們需要首先編譯my_local_crate1和my_local_crate2這兩個(gè)lib crates:
$rustc --crate-type lib --crate-name my_local_crate1 my_local_crate1/lib.rs$rustc --crate-type lib --crate-name my_local_crate2 my_local_crate2/lib.rs
這會(huì)在項(xiàng)目頂層目錄下生成兩個(gè)rlib文件:
$ls |grep rlib libmy_local_crate1.rliblibmy_local_crate2.rlib
之后,我們就可以用之前學(xué)到的方法編譯binary crate了:
$rustc --extern my_local_crate1=libmy_local_crate1.rlib --extern my_local_crate2=libmy_local_crate2.rlib main.rs
上述的幾個(gè)rustc-only的rust項(xiàng)目都是hard模式的,即一切都需要手工去做,包括下載crate、編譯crate時(shí)傳入各種路徑等。在真正的生產(chǎn)中,Rustacean們是不會(huì)這么做的,而是會(huì)直接使用cargo對rust項(xiàng)目進(jìn)行管理。接下來,我們就來系統(tǒng)地看一下使用cargo進(jìn)行rust項(xiàng)目管理以及對應(yīng)的rust代碼組織形式。
在前面的章節(jié)中,我們見識(shí)過了:Rust的包管理器Cargo是一個(gè)強(qiáng)大的工具,可以幫助我們輕松地管理Rust項(xiàng)目,cargo才是生產(chǎn)類項(xiàng)目的項(xiàng)目構(gòu)建管理工具標(biāo)準(zhǔn),它可以讓Rustacean避免復(fù)雜的手工rustc操作。Cargo提供了許多功能,包括依賴項(xiàng)管理、構(gòu)建和測試等。不過在這篇文章中,我不會(huì)介紹這些功能,而是看看使用cargo管理的Rust項(xiàng)目都有哪些代碼組織模式。
Rust項(xiàng)目的代碼組織結(jié)構(gòu)可以分為兩類:單一package和多個(gè)package。
什么是package?在之前的rust-only項(xiàng)目中,我們可從未見到過package!package是cargo引入的一個(gè)管理單元概念,它指的是一個(gè)獨(dú)立的Rust項(xiàng)目,包含了源代碼、依賴項(xiàng)和配置信息。每個(gè)Package都有一個(gè)唯一的名稱和版本號,用于標(biāo)識(shí)和管理項(xiàng)目。因此,在the cargo book[4]中,cargo也被稱為“Rust package manager”,crates.io也被稱為“the Rust community’s package registry”。
最能直觀體現(xiàn)package存在的就是下面Cargo.toml中的配置了:
[package]name = "hello_world"version = "0.1.0"edition = "2021"[dependencies]
下面我們就來看看不同類型的rust package的代碼組織形式。我們先從單一package形態(tài)的項(xiàng)目來開始。
單一package項(xiàng)目是指整個(gè)項(xiàng)目只有一個(gè)Cargo.toml文件。這種項(xiàng)目還可以進(jìn)一步分為三類:
下面我們分別舉例來說明一下這三類項(xiàng)目。
我們進(jìn)入organizing-rust-code/cargo/single-package/single-binary-crate,然后執(zhí)行下面命令來創(chuàng)建一個(gè)單一Binary Crate的項(xiàng)目:
$cargo new hello_world --bin Created binary (application) `hello_world` package
這個(gè)例子我們在之前的章節(jié)中也是見過的,它的結(jié)構(gòu)如下:
$tree hello_world hello_world├── Cargo.toml└── src └── main.rs1 directory, 2 files
默認(rèn)生成的Cargo.toml內(nèi)容如下:
[package]name = "hello_world"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]
使用cargo build即可完成該項(xiàng)目的構(gòu)建:
$cargo build Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/single-binary-crate/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 1.16s
為了更顯式地體現(xiàn)這是一個(gè)binary crate,我們可以在Cargo.toml增加如下內(nèi)容:
[[bin]]name = "hello_world"path = "src/main.rs"
這不會(huì)影響cargo的構(gòu)建結(jié)果!
通過cargo run可以查看構(gòu)建出的可執(zhí)行文件的運(yùn)行結(jié)果:
$cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.06s Running `target/debug/hello_world`Hello, world!
接下來,我們再來看看單一library crate的rust項(xiàng)目。
我們進(jìn)入organizing-rust-code/cargo/single-package/single-library-crate,然后執(zhí)行下面命令來創(chuàng)建一個(gè)單一Library Crate的項(xiàng)目:
$cargo new my_library --lib Created library `my_library` package
創(chuàng)建后的my_library項(xiàng)目的結(jié)構(gòu)如下:
$tree.├── Cargo.toml└── src └── lib.rs
默認(rèn)生成的Cargo.toml如下:
[package]name = "my_library"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]
和binary crate的一樣,我們也可以顯式指定target:
[lib]name = "my_library"path = "src/lib.rs"
注意,這里是[lib]而不是[[lib]],這是因?yàn)樵谝粋€(gè)carge package中最多只能存在一個(gè)library crate,但binary crate可以有多個(gè)。
接下來,我們就看看一個(gè)由多個(gè)binary crate和一個(gè)library crate混合構(gòu)成的rust項(xiàng)目。
我們在organizing-rust-code/cargo/single-package/hybrid-crates下面執(zhí)行如下命令創(chuàng)建這個(gè)多crates混合項(xiàng)目:
$cargo new my_project Created binary (application) `my_project` package
上述命令默認(rèn)創(chuàng)建了一個(gè)binary crate的project,我們需要配置一下Cargo.toml,將其改造為多個(gè)crates并存的project:
[package]name = "my_project"version = "0.1.0"edition = "2021"[[bin]]name = "cmd1"path = "src/main1.rs"[[bin]]name = "cmd2"path = "src/main2.rs"[lib]name = "my_library"path = "src/lib.rs"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]
這里定義了三個(gè)crates。兩個(gè)binary crates: cmd1、cmd2以及一個(gè)library crate:my_library。
如果我們執(zhí)行cargo build,cargo會(huì)將三個(gè)crate都構(gòu)建出來:
$cargo build Compiling my_project v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/hybrid-crates/my_project) Finished dev [unoptimized + debuginfo] target(s) in 0.80s
我們可以在target/debug下找到構(gòu)建出的crates:cmd1、cmd2和libmy_library.rlib:
$ls target/debugbuild/ cmd1.d cmd2.d examples/ libmy_library.dcmd1* cmd2* deps/ incremental/ libmy_library.rlib
我們也可以通過cargo分別運(yùn)行兩個(gè)binary crate:
$cargo run --bin cmd1 Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/cmd1`cmd1$cargo run --bin cmd2 Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/cmd2`cmd2
在The cargo book中,有一個(gè)典型的cargo package的示例:
.├── Cargo.lock├── Cargo.toml├── src/│ ├── lib.rs│ ├── main.rs│ └── bin/│ ├── named-executable.rs│ ├── another-executable.rs│ └── multi-file-executable/│ ├── main.rs│ └── some_module.rs├── benches/│ ├── large-input.rs│ └── multi-file-bench/│ ├── main.rs│ └── bench_module.rs├── examples/│ ├── simple.rs│ └── multi-file-example/│ ├── main.rs│ └── ex_module.rs└── tests/ ├── some-integration-tests.rs └── multi-file-test/ ├── main.rs └── test_module.rs
在這樣一個(gè)典型的項(xiàng)目中:
一些中大型的Rust項(xiàng)目都是多package的,比如rust的異步編程事實(shí)標(biāo)準(zhǔn)tokio庫[5]、剛剛升級為Apache基金會(huì)頂級項(xiàng)目的SQL查詢引擎datafusion[6]等。以tokio為例,這些項(xiàng)目的頂層Cargo.toml都是這樣的:
// https://github.com/tokio-rs/tokio/blob/master/Cargo.toml[workspace]resolver = "2"members = [ "tokio", "tokio-macros", "tokio-test", "tokio-stream", "tokio-util", # Internal "benches", "examples", "stress-test", "tests-build", "tests-integration",][workspace.metadata.spellcheck]config = "spellcheck.toml"
上面這個(gè)Cargo.toml示例與我們在前面見到的Cargo.toml都不一樣,它并不包含package配置,其主要的配置為workspace。我們看到workspace的members字段中配置了該項(xiàng)目下的其他package。正是通過這個(gè)配置,cargo可以在一個(gè)項(xiàng)目里管理和構(gòu)建多個(gè)package。
工作空間(Workspace)[7]是一組一個(gè)或多個(gè)包(Package)的集合,這些包稱為工作空間成員(Workspace Members),它們一起被管理。接下來,我們就來創(chuàng)建一個(gè)多package的cargo項(xiàng)目。
由于cargo并沒有提供cargo new my-pakcage --workspace這樣的命令行參數(shù),項(xiàng)目的頂層Cargo.toml需要我們手動(dòng)創(chuàng)建和編輯。
$cd organizing-rust-code/cargo/multi-packages$mkdir my-workspace$cd my-workspace$cargo new package1 --bin Created binary (application) `package1` package$cargo new package2 --lib Created library `package2` package$cargo new package3 --lib Created library `package3` package
接下來,我們手工創(chuàng)建和編輯一下項(xiàng)目頂層的Cargo.toml如下:
// organizing-rust-code/cargo/multi-packages/my-workspace/Cargo.toml[workspace]resolver = "2"members = [ "package1", "package2", "package3",]
保存后,我們可以在項(xiàng)目頂層目錄下使用下面命令檢查整個(gè)工作空間(workspace)中的所有包(package),確保它們的代碼正確無誤,不包含任何編譯錯(cuò)誤:
$cargo check --workspace Checking package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1) Checking package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2) Checking package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3) Finished dev [unoptimized + debuginfo] target(s) in 0.18s
在頂層目錄執(zhí)行cargo build,cargo會(huì)build工作空間中的所有package:
$cargo build Compiling package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3) Compiling package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2) Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1) Finished dev [unoptimized + debuginfo] target(s) in 0.64s
構(gòu)建后,該項(xiàng)目的目錄結(jié)構(gòu)變成下面這個(gè)樣子:
$tree -L 2 -F.├── Cargo.lock├── Cargo.toml├── package1/│ ├── Cargo.toml│ └── src/├── package2/│ ├── Cargo.toml│ └── src/├── package3/│ ├── Cargo.toml│ └── src/└── target/ ├── CACHEDIR.TAG └── debug/
我們看到該項(xiàng)目下的所有package共享一個(gè)共同的 Cargo.lock 文件,該文件位于工作空間的根目錄下。并且,所有包共享一個(gè)共同的輸出目錄,默認(rèn)情況下是工作空間根目錄下的一個(gè)名為target的目錄,該target目錄下的布局如下:
$tree -F -L 2 ./target./target├── CACHEDIR.TAG└── debug/ ├── build/ ├── deps/ ├── examples/ ├── incremental/ ├── libpackage2.d ├── libpackage2.rlib ├── libpackage3.d ├── libpackage3.rlib ├── package1* └── package1.d
我們在這下面可以找到所有package的編譯輸出結(jié)果,比如package1、libpackage2.rlib以及l(fā)ibpackage3.rlib。
當(dāng)然,你也可以指定一個(gè)package來構(gòu)建或運(yùn)行:
$cargo build -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.00s$cargo build -p package2 Finished dev [unoptimized + debuginfo] target(s) in 0.00s$cargo run -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/package1`Hello, world!
我們復(fù)制一份my-workspace,改名為my-workspace-with-deps,修改一下package1/src/main.rs,為其增加外部依賴rand crate:
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rsextern crate rand;use rand::Rng;fn main() { let mut rng = rand::thread_rng(); let num: u32 = rng.gen(); println!("Random number: {}", num);}
接下來,我們需要修改一下package1/Cargo.toml,手工加上對rand crate的依賴配置:
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml[package]name = "package1"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]rand = "0.8.5"
保存后,我們執(zhí)行package1的構(gòu)建:
$cargo build -p package1 Downloaded getrandom v0.2.14 (registry `rsproxy`) Downloaded libc v0.2.154 (registry `rsproxy`) Downloaded 2 crates (780.6 KB) in 1m 07s Compiling libc v0.2.154 Compiling cfg-if v1.0.0 Compiling ppv-lite86 v0.2.17 Compiling getrandom v0.2.14 Compiling rand_core v0.6.4 Compiling rand_chacha v0.3.1 Compiling rand v0.8.5 Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1) Finished dev [unoptimized + debuginfo] target(s) in 1m 46s
我們看到:cargo會(huì)自動(dòng)下載package1的直接外部依賴以及相關(guān)間接依賴。構(gòu)建成功后,可以執(zhí)行一下package1的編譯結(jié)果:
$cargo run -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.09s Running `target/debug/package1`Random number: 3840180495
接下來,我們再為package1添加內(nèi)部依賴,比如依賴package2的編譯結(jié)果:
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rsextern crate package2;extern crate rand;use rand::Rng;fn main() { let mut rng = rand::thread_rng(); let num: u32 = rng.gen(); println!("Random number: {}", num); let result = package2::add(2, 2); println!("result: {}", result);}// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml[package]name = "package1"version = "0.1.0"edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]rand = "0.8.5"package2 = { path = "../package2" }
我們看到:package1的main.rs依賴package2這個(gè)crate中的add函數(shù),我們在package1的Cargo.toml中為package1添加了新依賴package2,由于package2僅僅存放在本地,所以這里我們使用了path方式指定package2的位置。
我們執(zhí)行一下添加內(nèi)部依賴后的package1:
$cargo run -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/package1`Random number: 2485645524result: 4
本文循序漸進(jìn)地討論了在Rust項(xiàng)目中如何組織代碼的問題,這對于Rust初學(xué)者來說尤為有用。
我們首先回顧了Go語言中的代碼組織方式,介紹了Go項(xiàng)目代碼組織的兩個(gè)層級:module和package。然后,我們將Rust項(xiàng)目可以分為兩種類型:使用rustc編譯器的項(xiàng)目和使用Cargo的項(xiàng)目。
對于rustc-only的項(xiàng)目,開發(fā)者需要編寫自己的構(gòu)建腳本來管理項(xiàng)目的構(gòu)建過程。
文章從最簡單的單文件rustc-only項(xiàng)目開始介紹,展示了如何使用rustc編譯器來編譯和運(yùn)行這種項(xiàng)目,并逐步介紹了帶有外部依賴的rustc-only項(xiàng)目以及多文件項(xiàng)目的情況,引出了rust module概念。
rustc-only項(xiàng)目很少用于生產(chǎn)環(huán)境,這種方式主要用于學(xué)習(xí)和了解Rustc編譯器的功能機(jī)制以及Rust語言的代碼組織抽象。
在實(shí)際開發(fā)中,使用Cargo來創(chuàng)建和管理Rust包是常見的做法。在本章的后半段,我們介紹了使用cargo管理的rust項(xiàng)目的代碼組織情況,包括單package項(xiàng)目和多package項(xiàng)目以及如何為項(xiàng)目引入外部和內(nèi)部依賴。
總體而言,本文旨在幫助初學(xué)者理解和掌握Rust項(xiàng)目的代碼組織結(jié)構(gòu),以提高學(xué)習(xí)效率和學(xué)習(xí)效果。通過介紹rustc-only項(xiàng)目和cargo管理的項(xiàng)目,讀者可以逐步了解Rust代碼組織的基本概念和實(shí)踐方法。
本文涉及的源碼可以在這里[8]下載。
本文鏈接:http://www.www897cc.com/showinfo-26-96981-0.htmlGopher的Rust第一課:Rust代碼組織
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com