自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Gopher的Rust第一課:Rust代碼組織

開發(fā) 前端
在實際開發(fā)中,使用Cargo來創(chuàng)建和管理Rust包是常見的做法。在本章的后半段,我們介紹了使用cargo管理的rust項目的代碼組織情況,包括單package項目和多package項目以及如何為項目引入外部和內部依賴。

在上一章的講解中,我們編寫了第一個Rust示例程序"hello, world",并給出了rustc版和cargo版本。在真實開發(fā)中,我們都會使用cargo來創(chuàng)建和管理Rust包。不過,Hello, world示例非常簡單,僅僅由一個Rust源碼文件組成,而且所有源碼文件都在同一個目錄中。但真實世界中的實用Rust程序,無論是公司商業(yè)項目,還是一些知名的開源項目,甚至是一些稍復雜一些的供教學使用的示例程序,它們通??刹粫@么簡單,都有著復雜的代碼結構。

Rust初學者在閱讀這些項目源碼時便仿佛進入了迷宮,不知道該走哪條(閱讀代碼的)路徑,不知道每個目錄代表的含義,也不知道自己想看的源碼究竟在哪個目錄下。但目前市面上的Rust入門教程大多沒有重視初學者的這一問題,要么沒有對Rust項目代碼組織結構進行針對性的講解,要么是將講解放到書籍的后面章節(jié)。

根據(jù)我個人的學習經驗來看,理解一個實用Rust項目的代碼組織結構越早,對后續(xù)的Rust學習越有益處。同時,掌握Rust項目的代碼組織結構也是Rust開發(fā)者走向編寫復雜Rust程序的必經的一步。并且,初學者在了解項目的代碼組織結構后,便可以自主閱讀一些復雜的Rust項目的源碼,可提高Rust學習的效率,提升學習效果。因此,我決定在介紹Rust基礎語法之前先在本章中系統(tǒng)地介紹Rust的代碼組織結構,以滿足很多Rust初學者的述求。

但在介紹Rust代碼組織結構之前,我們需要先來系統(tǒng)說明一下Rust代碼組織結構中的幾個重要概念,它們是了解Rust項目代碼組織結構的前提。

4.1 回顧Go代碼組織

Go項目代碼組織由module和package兩級組成。通常來說,每個Go repo就是一個module,由repo根目錄下的go.mod定義,go.mod文件所在目錄也被稱為module root。go.mod中典型內容如下:

// go.mod
module github.com/user/mymodule[/vN]

go 1.22.1

... ...

go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一來可以反映該module的具體網絡位置,同時也是該module下面的Go package導入(import)路徑的組成部分。module root下的子目錄中通常存放著該module下面的Go package,比如module root/foo目錄下存放的Go包的導入路徑為github.com/user/mymodule[/vN]/foo。

Go package是Go的編譯單元,也是功能單元,代碼內外部導入和引用的單位也都是包。而go module是后加入的,更多用于管理包的版本(一個module下的所有包都統(tǒng)一進行版本管理)以及構建時第三方依賴和版本的管理。

更多關于Go module和package管理以及Go項目布局的內容,可以詳見我的極客時間《Go語言第一課》[1]專欄。

個人認為Go的module和package的兩級管理還是很好理解和管理的,在這方面Rust的代碼組織形式又是怎樣的呢?接下來,我們就來正式看看Rust的代碼組織。

4.2 rustc-only的Rust項目

Rust是系統(tǒng)編程語言,這讓我想起了當初在Go成為我個人主力語言之前使用C/C++進行開發(fā)的歲月。C/C++是沒有像go或Rust的cargo那樣的統(tǒng)一的包依賴管理器和項目構建管理工具的。編譯器(如gcc等)是核心工具,而項目構建管理則經常由其他工具負責,如Makefile、CMake,或者是Google的Bazel[2]等。在Windows上開發(fā)應用的,則往往使用微軟或其他開發(fā)者工具公司提供的IDE,如當年炙手可熱的Visual Studio系列。

下面表格展示了各語言的編譯器/鏈接器和構建管理工具的關系:

圖片圖片

像cargo、go這樣的“一站式”工具鏈都旨在為開發(fā)者提供體驗更為友好的交互接口的,在幕后,它們仍然依賴于底層的編譯器和鏈接器(如rustc和go tool compile/link)來執(zhí)行實際的代碼編譯。

不過,像cargo這樣的高級工具也給開發(fā)人員帶來了額外的抽象,或是叫“掩蓋”了一些真相,這有時候讓人看不清構建過程的本質,比如:很多Gopher用了很多年Go,但卻不知道go tool compile/link的存在。

本著只有in hard way,才能看到和抓住本質的思路,以及之前學習用系統(tǒng)編程語言C/C++時經驗,這里我們先來看一些rustc-only的Rust項目。Rustc-only的Rust項目是指不使用Cargo創(chuàng)建和管理的Rust項目,而是直接使用rustc編譯器來編譯和構建項目。這意味著開發(fā)者需要編寫自己的構建腳本,例如使用Makefile或其他構建工具來管理項目的構建過程。

不過,請注意:這類項目極少用于生產,即便是那些不需要復雜的依賴管理的小型項目。這里使用rustc-only的Rust項目僅僅是為了學習和了解Rustc編譯器的主要功能機制以及Rust語言在代碼組織上的一些抽象,比如module等。

下面我們就從最簡單的rustc-only項目開始,先來看看只有一個Rust源文件且無其他依賴項的“最簡項目”。

4.2.1 單文件項目

所謂單文件項目,即只有一個Rust源文件,例如前面章節(jié)中的hello_world.rs,這種項目可以直接使用rustc編譯器來編譯和運行:

// rust-guide-for-gopher/organizing-rust-code/rustc-only/single/hello-world/hello_world.rs
fn main() {
    println!("Hello, world!");
}

對于頂層帶有main函數(shù)的源文件,rustc會默認將其視為binary crate類型的源文件,并將其編譯為可執(zhí)行二進制文件hello_world。

我們當然也可以強制的讓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.

不過,如果強制將帶有頂層main函數(shù)的rust源文件視為lib crate型的,那么rustc將會報warning,提醒你函數(shù)main將是死代碼,永遠不會被用到:

$rustc --crate-type lib hello_world.rs
warning: function `main` is never used
 --> hello_world.rs:1:4
  |
1 | fn main() {
  |    ^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: 1 warning emitted

但即便如此,一個名為libhello_world.rlib的文件依然會被rustc生成出來?。壳?-crate-type lib等同于--create-type rlib)。

4.2.2 有外部依賴項的單文件項目

日常開發(fā)中,像上面的Hello, World級別的trivial應用是極其少見的,一個non-trivial的Rust應用或多或少都會有一些依賴。這里我們也來看一下如何基于rustc來構建帶有外部依賴的單文件項目。下面是一個帶有外部依賴的示例:

// organizing-rust-code/rustc-only/single/hello-world-with-deps/hello_world.rs
extern crate rand;
  
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let num: u32 = rng.gen();
    println!("Random number: {}", num);
}

這個示例程序依賴一個名為rand的crate,要編譯該程序,我們必須先手動下載rand的crate源碼,并在本地將rand源碼編譯為示例程序所需的rust library。下面步驟展示了如何下載和構建rand crate:

$curl -LO https://crates.io/api/v1/crates/rand/0.8.5/download
$tar -xvf download

解壓后,我們將看到rand-0.8.5這樣的一個crate目錄,進入該目錄,我們執(zhí)行cargo build來構建rand crate:

$cd rand-0.8.5
$cargo build
... ...
   Finished dev [unoptimized + debuginfo] target(s) in 0.19s

cargo構建出的librand.rlib就在rand-0.8.5/target/debug下。

注:rlib的命名方式:lib+{crate_name}.rlib

接下來,我們就來構建一下依賴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.rs
error[E0463]: can't find crate for `rand_core` which `rand` depends on
 --> hello_world.rs:1:1
  |
1 | extern crate rand;
  | ^^^^^^^^^^^^^^^^^^ can't find crate

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0463`.

我們看到rustc的編譯錯誤提示:無法找到rand crate依賴的rand_core crate!也就是說我們除了向rustc提供hello_world.rs依賴的rand crate之外,還要向rustc提供rand crate的各種依賴!

rand crate的各種依賴在哪里呢?我們在構建rand crate時,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會結合-L指示的位置以及--extern一起來定位,這里-L指示路徑為rand-0.8.5/target/debug/deps,--extern rand_core=librand_core-fc905f6ca5f8533b.rlib,那么rustc就會在rand-0.8.5/target/debug/deps下面搜索librand_core-fc905f6ca5f8533b.rlib是否存在。

我們運行rustc構建出的可執(zhí)行文件,輸出如下:

$./hello_world 
Random number: 431751199

4.2.3 有外部依賴的多文件項目

在Go中,如果某個目錄下有多個源文件,那么通常這幾個源文件均歸屬于同一個Go包(可能的例外的是*_test.go文件的包名)。但在Rust中,情況就會變得復雜了一些,我們來看一個例子:

// 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

在這個示例中,我們看到除了main.rs之外,還有一個sub2.rs以及一個目錄sub1,sub1下面還有三個rs文件。我們從main.rs開始,逐一看一下各個源文件的內容:

// 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三個module,我們分別來看一下。先來看一下最直觀的、定義在main.rs中的sub3 module。

第7行~第16行的代碼定義了一個名為sub3的module,它包含兩個函數(shù)func1和func2,這兩個函數(shù)前面的pub關鍵字表明他們是sub3 module的publish函數(shù),可以被module之外的代碼所訪問。任何未標記為pub的函數(shù)都是私有的,只能在模塊內部及其子模塊中使用。

在sub3 module的func2函數(shù)中,我們調用了self::func1()函數(shù),self指代是模塊自身,因此這個self::func1()函數(shù)就是sub3的func1函數(shù)。而接下來調用的super::func1()調用的語義你大概也能猜到。super指代的是sub3的父模塊,而super::func1()就是sub3的父模塊中的func1函數(shù)。

sub3的父模塊就是這個項目的頂層模塊,我們在main函數(shù)的入口處使用module_path!宏輸出了該頂層模塊的名稱。

和sub3在main.rs中定義不同,sub1和sub2也分別代表了另外兩種module的定義方式。

當Rust編譯器看到第4行mod sub1后,它會尋找當前目錄下是否有名為sub1.rs的源文件或是sub1/mod.rs源文件。在這個示例中,sub1定義在sub1目錄下的mod.rs中:

// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/mod.rs

pub mod bar;
pub mod foo;

pub fn func1() {
    println!("called {}::func1()", module_path!());
    foo::func1();
    bar::func1();
}

我們看到sub1/mod.rs中定義了一個公共函數(shù)func1,同時也在最開始處又嵌套定義了bar和foo兩個module,并在func1中調用了兩個嵌套子module的函數(shù):

bar和foo兩個module都是使用單文件module定義的,編譯器會在sub1目錄下搜尋foo.rs和bar.rs:

// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/foo.rs
pub fn func1() {
    println!("called {}::func1()", module_path!());
}

// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/bar.rs
pub fn func1() {
    println!("called {}::func1()", module_path!());
}

而main.rs中的sub2也是一個單文件的module,其源碼位于頂層目錄下的sub2.rs文件中:

// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub2.rs
pub fn func1() {
    println!("called {}::func1()", module_path!());
}

現(xiàn)在我們來編譯和執(zhí)行一下這個既有外部依賴,又是多文件且有多個module的rustc-only項目:

$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 

$./main
current module: main
Random number: 2691905579
called 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的定義方法:

  1. 直接將定義嵌入在某個rust源文件中:
mod module_name {

}
  1. 通過module_name.rs
  2. 通過module_name/mod.rs

在一個單crate的項目中,通過rust module可以滿足項目內部代碼組織的需要。

最后,我們再來看一個有多個crate的項目形式。

4.2.4 有多個crate的項目

下面是一個有著多個crate項目的示例:

// organizing-rust-code/rustc-only/workspace

$tree -L 2 -F
.
├── main.rs
├── my_local_crate1/
│   └── lib.rs
└── my_local_crate2/
    └── lib.rs

在這個示例中有三個crate,一個是頂層的binary類型的crate,入口為main.rs,另外兩個都是lib類型的crate,入口都在lib.rs中,我們貼一下他們的源碼:

// organizing-rust-code/rustc-only/workspace/main.rs
extern 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
}

要構建這個帶有三個crate的項目,我們需要首先編譯my_local_crate1和my_local_crate2這兩個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

這會在項目頂層目錄下生成兩個rlib文件:

$ls  |grep rlib 
libmy_local_crate1.rlib
libmy_local_crate2.rlib

之后,我們就可以用之前學到的方法編譯binary crate了:

$rustc --extern my_local_crate1=libmy_local_crate1.rlib --extern my_local_crate2=libmy_local_crate2.rlib main.rs

上述的幾個rustc-only的rust項目都是hard模式的,即一切都需要手工去做,包括下載crate、編譯crate時傳入各種路徑等。在真正的生產中,Rustacean們是不會這么做的,而是會直接使用cargo對rust項目進行管理。接下來,我們就來系統(tǒng)地看一下使用cargo進行rust項目管理以及對應的rust代碼組織形式。

4.3 使用cargo管理的Rust項目

在前面的章節(jié)中,我們見識過了:Rust的包管理器Cargo是一個強大的工具,可以幫助我們輕松地管理Rust項目,cargo才是生產類項目的項目構建管理工具標準,它可以讓Rustacean避免復雜的手工rustc操作。Cargo提供了許多功能,包括依賴項管理、構建和測試等。不過在這篇文章中,我不會介紹這些功能,而是看看使用cargo管理的Rust項目都有哪些代碼組織模式。

Rust項目的代碼組織結構可以分為兩類:單一package和多個package。

什么是package?在之前的rust-only項目中,我們可從未見到過package!package是cargo引入的一個管理單元概念,它指的是一個獨立的Rust項目,包含了源代碼、依賴項和配置信息。每個Package都有一個唯一的名稱和版本號,用于標識和管理項目。因此,在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)的項目來開始。

4.3.1 單一package的rust項目

單一package項目是指整個項目只有一個Cargo.toml文件。這種項目還可以進一步分為三類:

  1. 單一Binary Crate
  2. 單一Library Crate
  3. 多個Binary Crate和一個Library Crate

下面我們分別舉例來說明一下這三類項目。

4.3.1.1 單一Binary Crate

我們進入organizing-rust-code/cargo/single-package/single-binary-crate,然后執(zhí)行下面命令來創(chuàng)建一個單一Binary Crate的項目:

$cargo new hello_world --bin
     Created binary (application) `hello_world` package

這個例子我們在之前的章節(jié)中也是見過的,它的結構如下:

$tree hello_world 
hello_world
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

默認生成的Cargo.toml內容如下:

[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即可完成該項目的構建:

$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)這是一個binary crate,我們可以在Cargo.toml增加如下內容:

[[bin]]
name = "hello_world"
path = "src/main.rs"

這不會影響cargo的構建結果!

通過cargo run可以查看構建出的可執(zhí)行文件的運行結果:

$cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/hello_world`
Hello, world!

接下來,我們再來看看單一library crate的rust項目。

4.3.1.2 單一Library Crate

我們進入organizing-rust-code/cargo/single-package/single-library-crate,然后執(zhí)行下面命令來創(chuàng)建一個單一Library Crate的項目:

$cargo new my_library --lib
     Created library `my_library` package

創(chuàng)建后的my_library項目的結構如下:

$tree
.
├── Cargo.toml
└── src
    └── lib.rs

默認生成的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]],這是因為在一個carge package中最多只能存在一個library crate,但binary crate可以有多個。

接下來,我們就看看一個由多個binary crate和一個library crate混合構成的rust項目。

4.3.1.3 多個Binary Crate和一個Library Crate

我們在organizing-rust-code/cargo/single-package/hybrid-crates下面執(zhí)行如下命令創(chuàng)建這個多crates混合項目:

$cargo new my_project
     Created binary (application) `my_project` package

上述命令默認創(chuàng)建了一個binary crate的project,我們需要配置一下Cargo.toml,將其改造為多個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]

這里定義了三個crates。兩個binary crates: cmd1、cmd2以及一個library crate:my_library。

如果我們執(zhí)行cargo build,cargo會將三個crate都構建出來:

$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下找到構建出的crates:cmd1、cmd2和libmy_library.rlib:

$ls target/debug
build/   cmd1.d   cmd2.d   examples/  libmy_library.d
cmd1*   cmd2*   deps/   incremental/  libmy_library.rlib

我們也可以通過cargo分別運行兩個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

4.3.1.4 典型的cargo package

在The cargo book中,有一個典型的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

在這樣一個典型的項目中:

  • Cargo.toml和Cargo.lock文件存儲在包的根目錄(包根目錄)中。
  • 源代碼位于src目錄中。
  • 默認的庫文件是src/lib.rs。
  • 默認的可執(zhí)行文件是src/main.rs。
  • 其他可執(zhí)行文件可以放在src/bin/目錄中。
  • 基準測試位于benches目錄中。
  • 示例位于examples目錄中。
  • 集成測試位于tests目錄中。

4.3.2 多package的rust項目

一些中大型的Rust項目都是多package的,比如rust的異步編程事實標準tokio庫[5]、剛剛升級為Apache基金會頂級項目的SQL查詢引擎datafusion[6]等。以tokio為例,這些項目的頂層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"

上面這個Cargo.toml示例與我們在前面見到的Cargo.toml都不一樣,它并不包含package配置,其主要的配置為workspace。我們看到workspace的members字段中配置了該項目下的其他package。正是通過這個配置,cargo可以在一個項目里管理和構建多個package。

工作空間(Workspace)[7]是一組一個或多個包(Package)的集合,這些包稱為工作空間成員(Workspace Members),它們一起被管理。接下來,我們就來創(chuàng)建一個多package的cargo項目。

4.3.2.1 cargo管理的多package項目

由于cargo并沒有提供cargo new my-pakcage --workspace這樣的命令行參數(shù),項目的頂層Cargo.toml需要我們手動創(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)建和編輯一下項目頂層的Cargo.toml如下:

// organizing-rust-code/cargo/multi-packages/my-workspace/Cargo.toml
[workspace]
resolver = "2"
members = [
    "package1",
    "package2",
    "package3",
]

保存后,我們可以在項目頂層目錄下使用下面命令檢查整個工作空間(workspace)中的所有包(package),確保它們的代碼正確無誤,不包含任何編譯錯誤:

$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會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

構建后,該項目的目錄結構變成下面這個樣子:

$tree -L 2 -F
.
├── Cargo.lock
├── Cargo.toml
├── package1/
│   ├── Cargo.toml
│   └── src/
├── package2/
│   ├── Cargo.toml
│   └── src/
├── package3/
│   ├── Cargo.toml
│   └── src/
└── target/
    ├── CACHEDIR.TAG
    └── debug/

我們看到該項目下的所有package共享一個共同的 Cargo.lock 文件,該文件位于工作空間的根目錄下。并且,所有包共享一個共同的輸出目錄,默認情況下是工作空間根目錄下的一個名為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的編譯輸出結果,比如package1、libpackage2.rlib以及l(fā)ibpackage3.rlib。

當然,你也可以指定一個package來構建或運行:

$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!

4.3.2.2 帶有外部依賴和內部依賴的多package項目

我們復制一份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.rs
extern 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的構建:

$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會自動下載package1的直接外部依賴以及相關間接依賴。構建成功后,可以執(zhí)行一下package1的編譯結果:

$cargo run -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/package1`
Random number: 3840180495

接下來,我們再為package1添加內部依賴,比如依賴package2的編譯結果:

// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs

extern 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這個crate中的add函數(shù),我們在package1的Cargo.toml中為package1添加了新依賴package2,由于package2僅僅存放在本地,所以這里我們使用了path方式指定package2的位置。

我們執(zhí)行一下添加內部依賴后的package1:

$cargo run -p package1
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/package1`
Random number: 2485645524
result: 4

4.4 小結

本文循序漸進地討論了在Rust項目中如何組織代碼的問題,這對于Rust初學者來說尤為有用。

我們首先回顧了Go語言中的代碼組織方式,介紹了Go項目代碼組織的兩個層級:module和package。然后,我們將Rust項目可以分為兩種類型:使用rustc編譯器的項目和使用Cargo的項目。

對于rustc-only的項目,開發(fā)者需要編寫自己的構建腳本來管理項目的構建過程。

文章從最簡單的單文件rustc-only項目開始介紹,展示了如何使用rustc編譯器來編譯和運行這種項目,并逐步介紹了帶有外部依賴的rustc-only項目以及多文件項目的情況,引出了rust module概念。

rustc-only項目很少用于生產環(huán)境,這種方式主要用于學習和了解Rustc編譯器的功能機制以及Rust語言的代碼組織抽象。

在實際開發(fā)中,使用Cargo來創(chuàng)建和管理Rust包是常見的做法。在本章的后半段,我們介紹了使用cargo管理的rust項目的代碼組織情況,包括單package項目和多package項目以及如何為項目引入外部和內部依賴。

總體而言,本文旨在幫助初學者理解和掌握Rust項目的代碼組織結構,以提高學習效率和學習效果。通過介紹rustc-only項目和cargo管理的項目,讀者可以逐步了解Rust代碼組織的基本概念和實踐方法。

本文涉及的源碼可以在這里[8]下載。

4.5 參考資料

  • The book[9] - https://doc.rust-lang.org/book
  • The cargo book[10] - https://doc.rust-lang.org/cargo/index.html
  • The rustc book[11] - https://doc.rust-lang.org/rustc/index.html
責任編輯:武曉燕 來源: TonyBai
相關推薦

2024-04-22 08:06:34

Rust語言

2024-06-17 09:00:08

2024-06-07 08:59:35

2020-03-13 14:20:02

代碼開發(fā) Rust

2023-06-19 14:14:24

Rust程序Web

2023-05-29 16:25:59

Rust函數(shù)

2024-11-08 09:19:28

2022-12-30 11:05:40

Rust代碼

2021-02-16 11:04:26

RustGo華為

2021-05-31 09:42:10

FuchsiaRust代碼

2023-05-14 18:56:50

Rust數(shù)據(jù)類型

2025-01-03 09:12:11

2023-05-23 18:11:12

Rust數(shù)組元組

2024-06-12 08:00:07

2021-03-19 08:58:19

Rust共享愿景文檔開發(fā)者

2023-06-15 17:00:11

Rust循環(huán)

2021-02-24 07:42:34

PythonRust語言

2024-01-18 15:24:06

Rust開發(fā)鴻蒙OH4.0

2023-04-28 14:10:33

2025-02-27 00:00:15

點贊
收藏

51CTO技術棧公眾號