Rust 包与模块系统 --- Packge Crate 与 Module 解惑

Rust 对于我是一门反复入门的语言。每当我以为自己入门了,过了一段时间又会发现之前理解的不准确。
其中有一个原因就是 Rust 中的一些概念,与其他编程语言对比的时候,经常似是而非。即其他语言也有类似的概念,但是只是相似,并不能看成一致。
很多人在学习 Rust 的时候,会下意识跟自己其他语言的经验类比。如 cargo 和 pip,use 和 import,trait 和 interface ,crate 和 package 等等。但是这些内容 rust 都有自己独立的特性。我猜 Rust 把这些相似的概念使用了新名词,大概也是让学习者不要轻易的当成一回事。
本文就 rust 的代码组织方式进行介绍,主要说明 crate 和 module 的关系。

何为Crate


Package CrateModuleRust 组织代码的方式。其中 package 和 module 比较好理解,大多数编程语言都有类似的概念。
package 译为,一个用于构建、测试并分享的 Cargo 功能,简而言之就是一个 cargo 项目就是一个 package。module 译为模块,用于组织代码结构和访问性的功能块,可以类别其他语言的命名空间(namespace)。
然而 crate 是 rust 特有的名词,通常译为单元包。介于 package 和 module 之间的。

A crate is the smallest amount of code that the Rust compiler considers at a time
---《The Rust Programming Language》

上面这句话直译:Crate 是 Rust 编译器(编译)考量的最小代码单元。
如何理解这句话呢?
我们回想一下C语言和编译(汇编)过程。通常一个从一个 .c 源代码文件到一个可执行的二进制文件a.out,需要经过步骤:预处理 --> 编译 --> 汇编 --> 链接 这几个步骤。

image.png
  • 预处理:将 .c代码文件的 include 语句处理,把多个相关的文件汇聚成一个文件.i
  • 编译:将 .i文件编译成.s汇编文件
  • 汇编:将.s汇编文件通过汇编器汇编成目标文件 .o o 表示object。
  • 链接:将多个.o文件汇聚成一个二进制可执行文件a.out

如何理解链接呢?
这其实是代码组织的一种方式,每个 c 文件编译成 o 文件的时候,都是想象自己独立使用内存,比如都从0地址开始分配使用内存,当汇编成一个可执行文件的时候,需要链接器对他们重新排列,不然内存就冲突了。

例如,学校考试后需要年级排序。班级内部也有排名。班级内部的排名类似编译汇编,从1开始。然后学校再按照年级排序,班级第一的同学,未必是年级第一。这个重排汇总的过程就类似链接。只不过是按照班级为单位重排。
理解了链接之后,我们再来考虑 crate 为最小代码单元。其本质是指 crate 是最小的编译单元。因此也有书翻译为单元包。
最小编译单元就类似上图的一组.h.c文件,最终编译出来的是.o文件,一个crate,就类似一个目标文件,只不过在 rust 里,一个 crate 可以有多个.rs文件组合。

Crate的种类


crate 有两种类型,bianry crate(二进制 crate ) 和 lib crate(库 crate )。前者会编译生成二进制可执行文件,后者编译成不可执行的二进制文件。一个 package 下的 crate 规则:

  • 一个 package 至少包含一个 crate(binary crate或 lib crate)
  • 一个 package 可以包含任意多个 binary crate
  • 一个 package 至多包含一个 lib crate

Golang 里也有 可执行包和库包的差别

Binary Crate

crate 中包含 main函数的就是 binary crate。即所有 rust 文件最后编译成一个可执行的二进制文件,入口是main 函数。
cargo 默认创建的项目,src/main.rs 是 binary crate,main.rs 是 crate 的根(root),该 crate 默认的名字是 cargo.toml 里 package 里定义的 name。
下面使用代码逐步说明 crate 和 bianry crate 的具体含义

新建package 和 crate

使用cargo new可以新建一个 package,按照一个 package 至少一个 crate 的规则,cargo 默认生成一个bianry crate。

➜  rust cargo new hello
     Created binary (application) `hello` package
➜  rust cd hello
➜  hello git:(master) ✗ tree
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files
➜  hello git:(master) ✗ cat Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
➜  hello git:(master) ✗ cat src/main.rs
fn main() {
    println!("Hello, world!");
}

上面创建了一个包,包名是hello,rust 默认在 src 生成了一个 main.rs 文件,该文件是一个 binary crate,crate 名也是 hello

运行

使用 cargo run 可以编译运行项目,编译的是默认的 bianry carte,crate 名为hello的 main.rs 文件,最终生成一个可执行的二进制文件。可以使用 --bin参数指定 binary crate,不指定就是默认的 crate。等价于cargo run --bin hello。其中 hello 是默认的 crate。

➜  hello git:(master) ✗ cargo run
   Compiling hello v0.1.0 (/Users/master/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 3.69s
     Running `target/debug/hello`
Hello, world!
➜  hello git:(master) ✗ tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── build
        ├── deps
        ├── examples
        ├── hello
        ├── hello.d
        └── incremental

7 directories, 6 files
➜  hello git:(master) ✗ ./target/debug/hello
Hello, world!

由此可见,cargo run其实有两个过程:

  • 编译:使用cargo build进行编译构建,生成 target 目录
  • 运行:执行编译的二进制可执行文件,执行 target/debug/hello

对于接下来的例子,为了清楚看到编译的结果,每次编译运行之前,都删除上一次编译生成的 target 文件夹

多个 bianry crate

前文提及,既然一个 package 可以包含任意多个 binary crate。表示一个 binary crate 是入口有 main 函数,因此我们可以再几个 binary crate。新建 bar.rs foo.rs 与 main.rs 同级。

➜  hello git:(master) ✗ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── bar.rs
    ├── foo.rs
    └── main.rs

1 directory, 5 files
➜  hello git:(master) ✗ cat src/foo.rs src/bar.rs
fn main() {
    println!("Hello, foo!");
}
fn main() {
    println!("Hello,bar!");
}

运行 cargo run 不指定就是默认的 binary crate。

➜  hello git:(master) ✗ cargo run
   Compiling hello v0.1.0 (/Users/master/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 2.70s
     Running `target/debug/hello`
Hello, world!
➜  hello git:(master) ✗ cargo run --bin hello
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/hello`
Hello, world!

由于我们新增了 bar 和 foo 两个新的 bianry crate,可以运行这两个 bianry crate

➜  hello git:(master) ✗ cargo run --bin bar
error: no bin target named `bar`.
Available bin targets:
    hello

➜  hello git:(master) ✗ cargo run --bin foo
error: no bin target named `foo`.
Available bin targets:
    hello

可是事与愿违,编译器反馈没有找到 bar 和 foo 两个目标 crate。同时提示了,只有 hello 这个 crate。这里rust有规定,binary crate 可以有多个,但是需要组织在 src/bin 目录下。编译器才能搜索。下面我们调整一下代码

➜  hello git:(master) ✗ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── bin
    │   ├── bar.rs
    │   └── foo.rs
    └── main.rs

2 directories, 5 files
➜  hello git:(master) ✗ cargo run
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: bar, foo, hello

再次执行 cargo run会报错,原因是 cargo 不知道需要执行哪一个 bianry crate,并且也提供了 bar foo hello 三个crate 可选。

按照之前的经验,不指定应该是默认的,此时rust又报不知道哪一个,挺奇怪的。

我们指定 crate,然后分别查看编译的目标文件

➜  hello git:(master) ✗ cargo run --bin hello
   Compiling hello v0.1.0 (/Users/master/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 2.86s
     Running `target/debug/hello`
Hello, world!
➜  hello git:(master) ✗ tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── bin
│   │   ├── bar.rs
│   │   └── foo.rs
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── build
        ├── deps
        ├── examples
        ├── hello
        ├── hello.d
        └── incremental

8 directories, 8 files

只有target/debug/hello可执行文件。

➜  hello git:(master) ✗ rm -rf target
➜  hello git:(master) ✗ cargo run --bin bar
   Compiling hello v0.1.0 (/Users/master/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 2.82s
     Running `target/debug/bar`
Hello,bar!
➜  hello git:(master) ✗ tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── bin
│   │   ├── bar.rs
│   │   └── foo.rs
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── bar
        ├── bar.d
        ├── build
        ├── deps
        ├── examples
        └── incremental

8 directories, 8 files
➜  hello git:(master) ✗ ./target/debug/bar
Hello,bar

从上面的结果来看,指定运行 bar ,bar 会被编译,并且运行。指定 foo 的结果也一样,foo crate 会被编译。
对于多个crate存在的情况,可以使用 cargo build一次性编译所有的crate,如果 crate 没有代码改动,不会重新编译。这就避免了多个 binary crate,需要多次编译的情况了。

➜  hello git:(master) ✗ cargo build
   Compiling hello v0.1.0 (/Users/master/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 3.27s
➜  hello git:(master) ✗ tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── bin
│   │   ├── bar.rs
│   │   └── foo.rs
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── bar
        ├── bar.d
        ├── build
        ├── deps
        ├── examples
        ├── foo
        ├── foo.d
        ├── hello
        ├── hello.d
        └── incremental

8 directories, 12 files

对于没有构建依赖工具的 C 和 C++ ,需要借助 Makefile 或 CMake 来处理依赖

Lib Crate

所谓 lib crate,就是没有 main 入口函数的 crate。一个包只有一个 lib crate,其实就是与 src 下有一个 lib.rs 文件,这个文件就是 lib crate。

➜  hello git:(master) ✗ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── bin
    │   ├── bar.rs
    │   └── foo.rs
    ├── lib.rs
    └── main.rs

2 directories, 6 files
➜  hello git:(master) ✗ cat src/lib.rs

fn hello_lib(){
    println!("hello lib");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        hello_lib()
    }
}

执行 cargo build 可以看到,生成的 target/debug 的文件中,多了一个libhello.d的中间文件,这就是 lib crate 的编译形成的目标文件,名字默认就是 lib + package-name

➜  hello git:(master) ✗ cargo build
   Compiling hello v0.1.0 (/Users/master/rust/hello)
warning: function `hello_lib` is never used
 --> src/lib.rs:2:4
  |
2 | fn hello_lib(){
  |    ^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: `hello` (lib) generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 4.07s
➜  hello git:(master) ✗ tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── bin
│   │   ├── bar.rs
│   │   └── foo.rs
│   ├── lib.rs
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── bar
        ├── bar.d
        ├── build
        ├── deps
        ├── examples
        ├── foo
        ├── foo.d
        ├── hello
        ├── hello.d
        ├── incremental
        ├── libhello.d
        └── libhello.rlib

8 directories, 15 files

lib.rs 文件中的代码,出现了 mod 的声明,这是 rust 模块的声明。就行一个package里可以有多个 crate,一个crate 里可以有多个 module

模块


crate 是编译最小化单元。真实的项目,代码会按照其功能性进行模块化。rust 的模块系统很强大,但是跟其他语言有一点点差别,模块系统和文件系统是相对独立。这一点跟其他编程语言完成不同。
简而言之,其他编程语言的文件系统和模块系统是高度一致。文件系统的目录树和模块的目录差不不大。rust 独树一帜,即可以跟文件系统一样组织,也可以独立成为一个文件。
为了简单起见,我们先在一个文件里介绍模块系统,然后再结合文件系统组织进行说明。

模块声明

以 《rust 权威指南》 里的例子说明,有这样一个crate,其模块组织如下:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
 

使用 cargo 新建一个 demo 项目。main.rs 文件如下:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("add_to_waitlist");
        }

        pub fn seat_at_table() {
            println!("seat_at_table");
        }
    }
    pub mod serving {
        pub fn take_order(){
            println!("take_order");
        }

        pub fn serve_order(){
            println!("serve_order");
        }

        pub fn take_payment(){
            println!("take_payment");
        }
    }
}


fn eat_at_restaurant() {
    println!("eat_at_restaurant");
}


fn main() {

    eat_at_restaurant();
    crate::front_of_house::hosting::add_to_waitlist();
}

为了先说明模块系统,所有模块和函数都定义 pub(可导出)。
从上上面的代码可以看到

  • rust 使用 mod 和花括号声明模块
  • 模块可以嵌套
  • 模块内可以包含其他条目的定义,比如结构体、枚举、常量、trait或函数
  • 模块的逻辑都可以组织在一个文件里,独立与文件系统

模块与文件系统

通过 mod可以组织模块。然而我们更熟悉的是使用文件系统。例如上面的模块目录树,我们更熟悉文件系统。理想状态如下:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
➜  src git:(master) ✗ tree
.
├── front_of_house
│   ├── hosting.rs
│   └── saving.rs
└── main.rs

0x00

下面我们逐步创建这样的模块组织。文件或文件夹的名字是模块名(module-name),然后通过 mod module-name语句注册和寻找模块。
新建一个 front_of_house.rs 文件用来表示 front_of_hourse 模块,把 main.rs 里的内容独立出来,在main里使用 pub mod front_of_house 声明 front_of_hourse 模块。

➜  src git:(master) ✗ tree
.
├── front_of_house.rs
└── main.rs

0 directories, 2 files
➜  src git:(master) ✗ cat front_of_house.rs main.rs
pub mod hosting {
    pub fn add_to_waitlist() {
        println!("add_to_waitlist");
    }

    pub fn seat_at_table() {
        println!("seat_at_table");
    }
}
pub mod serving {
    pub fn take_order(){
        println!("take_order");
    }

    pub fn serve_order(){
        println!("serve_order");
    }

    pub fn take_payment(){
        println!("take_payment");
    }
}

pub mod front_of_house; // 声明模块

fn main() {
    self::front_of_house::hosting::add_to_waitlist(); // 模块的完整路径
}

执行 cargo run,可以正常的编译运行。

0x01

下面把 front_of_house.rs 文件改成文件夹,并创建 hosting.rs 和 seving.rs 文件

pub fn add_to_waitlist() {
    println!("add_to_waitlist");
}

pub fn seat_at_table() {
    println!("seat_at_table");
}
pub fn take_order(){
    println!("take_order");
}

pub fn serve_order(){
    println!("serve_order");
}

pub fn take_payment(){
    println!("take_payment");
}
➜  src git:(master) ✗ tree
.
├── front_of_house
│   ├── hosting.rs
│   └── serving.rs
└── main.rs

1 directory, 3 files

执行 cargo run 会发现如下报错:

➜  front_of_house git:(master) ✗ cargo run
   Compiling demo v0.1.0 (/Users/master/rust/demo)
error[E0583]: file not found for module `front_of_house`
 --> src/main.rs:2:1
  |
2 | pub mod front_of_house;
  | ^^^^^^^^^^^^^^^^^^^^^^^
  |
  = help: to create the module `front_of_house`, create file "src/front_of_house.rs" or "src/front_of_house/mod.rs"

error[E0433]: failed to resolve: could not find `hosting` in `front_of_house`
 --> src/main.rs:5:27
  |
5 |     self::front_of_house::hosting::add_to_waitlist();
  |                           ^^^^^^^ could not find `hosting` in `front_of_house`

报错的地方是因为rust 无法找到 hosting 模块。正如前文所说,文件或文件夹名是模块名,文件系统的组织可以是模块的命名空间。但是 rust 搜索模块使用的是 mod module-name 的方式注册或搜索。
main.rs 通过 pub mod front_of_house; 语句找到了统计的文件夹front_of_house,但是 hosting 和 serving 模块并没注册到 front_of_house 上,从报错信息也可以看到。rust 建议我们在 front_of_house 文件夹内创建一个mod.rs的文件。事实上,rust 会搜索文件夹下的是 mod.rs 文件,这个文件声明了文件夹作为模块的子模块信息。
新建一个文件 mod.rs ,然后输入下面内容:

pub mod hosting;
pub mod serving;
➜  src git:(master) ✗ vim front_of_house
➜  src git:(master) ✗ tree
.
├── front_of_house
│   ├── hosting.rs
│   ├── mod.rs
│   └── serving.rs
└── main.rs

1 directory, 4 files

再次编译就正常。

0x02

至此,我们对rust的模块组织有了更深的认识。简而言之就是,模块的路径可以是文件系统的路径,不同于其他编程语言,rust 需要显示的注册模块。对于文件夹,其内部使用一个 mod.rs 的文件来注册该文件内的模块。我们再修改一下上面的模块组织,加深一下对模块的理解

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
➜  src git:(master) ✗ tree
.
├── front_of_house
│   ├── hosting
│   │   └── mod.rs
│   ├── mod.rs
│   └── serving.rs
└── main.rs

2 directories, 4 files

其中 front_of_house/hosting/mod.rs 的内容是原 hosting.rs 的内容。当模块命名成文件的时候,需要其内部的内容都必须写在 mod.rs 的文件里。

路径访问性

类似于在文件系统中使用路径进行导航的方式,Rust 也使用路径搜索模块和其内容。路径有两种形式:

  • 使用crate的名或字面量crate从根节点开始的绝对路径
  • 使用self、super或内部标识符从当前模块开始的相对路径

Rust中的所有条目(函数、方法、结构体、枚举、模块及常量)默认都是私有的。处于父级模块中的条目无法使用子模块中的私有条目,但子模块中的条目可以使用它所有祖先模块中的条目。
想要子模块的内容被外部应用,需要声明为 pub 属性。


上面例子的 front_of_house 模块是隶属于 bianry crate。也可以组织到 lib crate 里。新建 lib.rs,并声明 front_of_house 模块

pub mod front_of_house;

修改 main.rs

use demo::front_of_house::hosting::add_to_waitlist;

fn main() {
    // demo::front_of_house::hosting::add_to_waitlist();
    add_to_waitlist()
}

文件目录

➜  src git:(master) ✗ tree
.
├── front_of_house
│   ├── hosting
│   │   └── mod.rs
│   ├── mod.rs
│   └── serving.rs
├── lib.rs
└── main.rs

2 directories, 5 files

总结


Rust 使用 Package Crate 和 Module 来组织项目代码。一个 Cargo 项目就是一个 Package,一个Package 可以有多个 Crate,Crate 是 Rust 代码的最小编译单元,也翻译为单元包。Crate 分为 Binary Crate 和 Lib Crate。
binary crate 可以编译生成可执行二进制文件,lib crate 编译用来共享的代码模块。
每个Crate 可以有多个 Module,Module 是代码接口命名空间的树形组织。既可以组织成文件系统一样的目录树结构,也可以组织成独立的文件。
最后,Rust 自身有很多新的概念,这些内容既和其他编程语言类似,但又不能直接等效,Rust 创造了新名词来描述这些概念。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容