Skip to content

Commit

Permalink
update doc of architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
vita-dounai committed Sep 13, 2020
1 parent 4121f2e commit 3c09ae9
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 12 deletions.
83 changes: 82 additions & 1 deletion docs/in_depth/architecture.md
Original file line number Diff line number Diff line change
@@ -1 +1,82 @@
# 架构设计与实现原理
# 实现原理与架构设计

## 实现原理

为理解Liquid的实现原理,我们需要简单了解元编程与宏的概念。在[维基百科](https://zh.wikipedia.org/wiki/%E5%85%83%E7%BC%96%E7%A8%8B)中,元编程被描述成一种计算机程序可以将代码看待成数据的能力,使用元编程技术编写的程序能够像普通程序在运行时更新、替换变量那样操作更新、替换代码。宏在Rust语言中是一种功能,能够在编译实际代码之前按照自定义的规则展开原始代码,从而能够达到在修改原始代码的目的。从元编程的角度理解,宏就是“生成代码的代码”,因而Rust语言中的元编程能力主要来自于宏系统。通过Rust语言的宏系统,不仅能够实现C/C++语言的宏系统所提供的模式替换功能,甚至还能够控制编译器的行为、设计自己的语法从而实现eDSL,而Liquid正是基于Rust语言的宏系统实现的eDSL。我们接下来从“编译器如何处理Rust源代码文件”的问题出发,进一步研究Liquid的实现。

```eval_rst
.. admonition:: 提示
若您有编译原理相关的知识背景,将有助于理解Liquid的工作机制。
```

Rust源代码文件编译需要经过下列阶段(我们在图中省略了优化等步骤,因为它们并不涉及我们所讨论的主题):

![rust_compile](../../images/in_depth/architecture/rust_compile.png)

1. 编译器在获得源代码文件后,会先进行词法分析,即把源代码字符序列转换为标记(Token)序列,标记是单独的语法单元,在Rust语言中,关键字、标识符都能够构成标记。词法分析还会将标记与标记的关系也记录下来,从而的生成标记树(Token tree),以一条简单的程序语句为例:

```rust
a + b + (c + d[0]) + e
```

其标记树如下图所示:

![token_tree](../../images/in_depth/architecture/token_tree.png)

```eval_rst
.. admonition:: 注意
与C/C++中宏处理(展开#include的头文件、替换#define的符号等)是发生在预编译期不同,Rust语言并没有预编译阶段,其宏展开是发生在的完成语法分析后。也正是因为如此,Rust宏能够获得更详细、更复杂的编译器信息,从而能够提供极为强大的功能。
```

2. 随即,编译器启动语法分析流程,将词法分析生成的标记树翻译为AST。AST的全称是**A**bstract **S**yntax **T**ree,即抽象语法树。在计算机科学中,AST是源代码语法结构的一种抽象表示,能够方便地被编译器处理。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。上述第1步中生成的样例标记树会被翻译为如下图所示的AST:

![ast](../../images/in_depth/architecture/ast.png)

3. 然后,编译器开始分析AST并执行宏展开过程。此阶段是是最为重要的阶段,因为Liquid主要工作在这个阶段。以[HelloWorld合约](../quick_start/introduction.md)为例,编译器构造出HelloWorld合约的AST后,当扫描至AST中表示"```#[liquid::contract(version = "0.1.0")]```"的语法树节点时,编译器能够知道,此处正在调用[属性宏](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros)(Rust中一种特殊的宏),因此会开始寻找`contract`属性宏的定义并尝试进行宏展开。在Liquid中,`contract`属性宏的定义如下:

```rust
#[proc_macro_attribute]
pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream {
contract::generate(attr.into(), item.into()).into()
}
```

在上述代码中可以看到,属性宏是以函数的形式定义的,其输入是若干个标记序列(TokenStream),其输出同样是一个标记序列。事实上,在Rust语言中,宏的确可以理解为将某一个AST变换到另外一个AST的函数!只是Rust编译器并不会向属性宏直接传递AST,而且会将其调用位置所在的语法树节点转换为标记序列传递给属性宏,由属性宏的编写者自行决定如何处理这段标记序列。无论如何处理,属性宏都需要返回一段标记序列,Rust编译器接收到这段标记序列后,会将其重新编译为AST插入到原调用位置,从而完成代码编译期更新。

具体到Liquid的`contract`属性宏,当编译器进行展开时,`contract`属性宏会获取到自身及其后跟随的`mod`(即我们用来定义合约状态及合约方法的模块)的标记序列,并将其解析为一颗AST。随后,`contract`属性宏会自顶向下扫描这颗AST,当遇到使用```#[liquid(storage)] struct ...```语法定义的合约状态时,会进行语法检查及代码变换,将对结构体成员的读写操作变为对链上状态读写接口的调用。同理,在合约代码中使用```#[liquid(event)] struct ...```定义事件、使用```#[liquid(methods)] impl ...```定义合约方法等也会经历同样的代码变换过程,只是变换及桥接到区块链底层平台的方式不尽相同。

4. 编译器将进行宏展开之后的AST翻译为可执行文件:若是需要在本机运行单元测试,则会将AST翻译为本机所使用的操作系统及CPU所能识别的可执行文件格式及二进制代码;若是需要可以在区块链上部署运行,则会将AST翻译为Wasm格式字节码。至此,合约的基本构建流程结束。

从Liquid的实现原理中可以看出,Liquid可以理解为是一种以Rust语言目标语言的编程语言。编译器的广义定义是一种会将某种编程语言写成的源代码转换成另一种编程语言计算机程序,因此实际上Liquid的宏展开过程一定程度上扮演了编译器的角色,从而让您在编写合约时更加便利、自然。如果您对[HelloWorld合约](../quick_start/introduction.md)完全展开后的代码形态感兴趣,可以参考[这里](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a2ac3d836b0fdce414e656019b454c82),您可能会发现这些代码极其晦涩,但幸运的是,您完全不需要和这些代码打交道,Liquid会帮您打理好一切:)。

## 架构设计

Liquid及周边开发工具的整体架构如下图所示:

![liquid_arch](../../images/in_depth/architecture/liquid_arch.png)

其中各模块功能如下:

- **cargo-liquid**:方便开发者进行合约开发及构建的命令行工具。开发者在本机安装后`cargo-liquid`能够以`cargo`自定义扩展命令的形式为开发提供服务:

- 当开发者执行`cargo liquid new ...`命令时,`cargo-liquid``new`模块会以[HelloWorld合约](../quick_start/introduction.md)合约为模板创建合约项目,并自动配置编译选项及外部依赖、实现ABI生成器,开发者可以基于该模板进一步开发属于自己的智能合约。

- 当开发者执行`cargo liquid build ...`命令时,`cargo-liquid``build`模块会收集编译元信息(如目标类型等),并调用`rustc`进行跨平台编译,将Liquid合约编译为Wasm格式字节码,随后使用`wasm-opt`等工具对生成的字节码进行效率及体积上的优化,最后调用ABI生成器为开发者的合约生成JSON格式的ABI。

- **Liquid**:嵌入式领域特定语言Liquid的实现,其内部是一组[](https://doc.rust-lang.org/reference/macros.html)(Macro)及区块链底层API的集合,以包的形式提供给开发者使用,其内部主要有下列三个子模块:

- *Lang*:开发者在合约开发过程中所使用到的`contract`(用于以`mod`语法声明智能合约)、`InOut`(用于以`struct`语法定义自定义方法参数类型)等宏均由`macro`模块定义并导出。当构建开发者的合约时,`rustc`会对这些的宏进行匹配及展开。在宏的展开过程中,`IR`模块会解析开发者的代码并重新生成抽象语法树(AST),以对部分Rust语法进行重新诠释。随后,`code-gen`模块会依据`IR`模块生成的AST生成调用`Core`模块的Rust代码,这些代码对开发者并不可见。

- *Core*:包含了开发者能够使用的智能合约功能的实现。以自底向下的视角来看:
- `engine`:智能合约的执行引擎。对于上层,`engine`模块提供了一系列基础API,包括用于读取链上存储的`get_storage`接口、用于写入链上存储的`set_storage`接口、用于获取当前区块时间戳的`now`接口等。对于这些接口,`engine`有两种不同的实现:
- `off-chain`中的实现用于在本机执行智能合约的单元测试时使用,其内部模拟了区块链特性(K-V存储、事件记录器等)并提供了测试专用的接口,用于开发者在正式部署合约前测试合约逻辑是否正确;
- `on-chain`中的实现用于智能合约真正在区块链中执行时使用,其实现相对较为简单,因为具体实现是由区块链底层平台完成,`on-chain`中只需要声明这些接口并进行适配即可。接口的规范(名称、参数类型、返回值类型等)由区块链底层平台给出,对于FISCO BCOS,这个规范称为BCOS环境接口规范(BCOS Environment Interface,BEI)。关于BEI的具体内容可参考附录。

- `types`:提供了区块链场景下基本数据类型的定义,如地址(Address)、字符串(String)等。`types`模块与`engine`模块一同构成了智能合约的执行环境,即`env`模块。

- `storage`:基于`env`模块提供接口,对链上状态的访问方式进行了进一步的抽象。智能合约需要通过`storage`模块提供的容器类型读写链上状态。若要访问单个链上状态,则可以使用常规容器`Value`;若要以下标的形式序列式的访问链上状态,则可以使用向量容器`Vec`;若要以键值对的形式访问链上状态,则可以使用映射容器`Mapping`;若需要在`Mapping`的基础上按键对链上状态进行迭代访问,则可以使用可迭代映射容器`Mapping`

- *Utils*:涵盖了其他基础功能。主要包括:用于实现合约方法参数及返回值编解码的`abi-codec`、用于生成ABI生成器代码的`abi-gen`及用于内存分配的`alloc`,其中,`alloc`模块用于为合约注册为全局内存分配器,合约内所有的内存分配操作(动态数组、字符串等)都会通过`alloc`模块进行。

14 changes: 7 additions & 7 deletions docs/quick_start/basic_dev_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ hello_world/

1. `.gitignore`:隐藏文件,用于在告诉Git哪些文件或目录不需要被添加到版本管理中。Liquid会默认将某些项目排除在版本管理之外,如果您不需要使用到Git,可以忽略该文件。
2. `.liquid\`:隐藏目录,用于实现Liquid的某些内部功能,其中`abi_gen`目录下包含了ABI生成器的实现,该目录下的编译配置及代码逻辑是固定的,您不应当修改该目录下的任何文件,否则可能会造成ABI生成出错。
3. `Cargo.toml`:项目元信息,主要包括项目信息、外部包依赖、编译配置等,一般而言您无需修改该文件,除非您有特殊得需求等(引用新的第三方库、调整优化等级)。
3. `Cargo.toml`:项目元信息,主要包括项目信息、外部包依赖、编译配置等,一般而言您无需修改该文件,除非您有特殊得需求等(引用新的第三方包、调整优化等级)。
4. `lib.rs`:Liquid合约源代码,您可以在此文件中进行Liquid合约的开发。

当执行`cargo liquid new`命令时,Liquid会以我们之前介绍过的`HelloWorld`合约为模板创建新的项目,因此创建出的`lib.rs`文件中包含了`HelloWorld`合约的代码实现,以及大量的解释每个程序语句的目的及功能的注释,这是一个能直接编译运行的“最简单”的合约。`HelloWorld`合约我们学习Liquid的起点,随着学习的深入,我们可以基于它开发出功能更为强大的合约!
当执行`cargo liquid new`命令时,Liquid会以我们在第一节中介绍过的HelloWorld合约为模板创建新的项目,因此创建出的`lib.rs`文件中包含了HelloWorld合约的代码实现,以及大量的解释每个程序语句的目的及功能的注释,这是一个能直接编译运行的“最简单”的合约。HelloWorld合约我们学习Liquid的起点,随着学习的深入,我们可以基于它开发出功能更为强大的合约!

本节更加关注Liquid合约的开发步骤,因此接下来都我们将围绕这个自动生成的`HelloWorld`合约进行讲解:)
本节更加关注Liquid合约的开发步骤,因此接下来都我们将围绕这个自动生成的HelloWorld合约进行讲解:)

## 测试合约

`HelloWorld`合约代码的底部,我们为`HelloWorld`合约撰写了了两个分别检验`get`方法与`set`方法功能正确性的基本单元测试。Liquid内置了对区块链链上环境的模拟,因此即使不将合约部署上链,我们也能够在本地方便地执行合约并观察合约的运行状态,仿佛合约真的已经部署在链上一般。在`hello_world`目录下执行以下命令便可快速执行单元测试:
在HelloWorld合约代码的底部,我们为HelloWorld合约撰写了了两个分别检验`get`方法与`set`方法功能正确性的基本单元测试。Liquid内置了对区块链链上环境的模拟,因此即使不将合约部署上链,我们也能够在本地方便地执行合约并观察合约的运行状态,仿佛合约真的已经部署在链上一般。在`hello_world`目录下执行以下命令便可快速执行单元测试:

```bash
cargo +nightly test
Expand Down Expand Up @@ -70,7 +70,7 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

从结果上看,我们通过了所有的单元测试,因此我们可以很有信心地认为我们的合约逻辑是正确无误的!我们接下来可以开始着手构建`HelloWorld`合约,并把它部署到真正的区块链上。
从结果上看,我们通过了所有的单元测试,因此我们可以很有信心地认为我们的合约逻辑是正确无误的!我们接下来可以开始着手构建HelloWorld合约,并把它部署到真正的区块链上。

## 构建合约

Expand Down Expand Up @@ -150,7 +150,7 @@ Positionals:
parameters The parameters(splitted by space) of constructor [array] [default: []]
```
执行该命令时需要依次传入字节码文件的路径、ABI文件的路径及构造函数的参数。当配置好CLI工具后(见[配置手册](https://github.com/FISCO-BCOS/nodejs-sdk#22-%E9%85%8D%E7%BD%AE),我们可以使用以下命令部署`HelloWorld`合约(其构造函数参数为空,因此我们在命令中没有提供任何构造函数参数):
执行该命令时需要依次传入字节码文件的路径、ABI文件的路径及构造函数的参数。当配置好CLI工具后(见[配置手册](https://github.com/FISCO-BCOS/nodejs-sdk#22-%E9%85%8D%E7%BD%AE),我们可以使用以下命令部署HelloWorld合约(其构造函数参数为空,因此我们在命令中没有提供任何构造函数参数):
```bash
node .\cli.js deployWasm <your working root directory>\hello_world\target\hello_world.wasm <your working root directory>\hello_world\target\hello_world.abi
Expand Down Expand Up @@ -184,7 +184,7 @@ Positionals:
parameters The parameters(splitted by space) of a function [array] [default: []]
```
执行该命令时需要依次传入合约名(对于Liquid合约,是合约项目名)、合约地址、要调用的合约方法名及传递给合约方法的参数。以调用`HelloWorld`合约的`get`方法为例,我们可以使用以下命令调用该方法(其方法参数为空,因此我们在命令中没有提供任何方法参数):
执行该命令时需要依次传入合约名(对于Liquid合约,是合约项目名)、合约地址、要调用的合约方法名及传递给合约方法的参数。以调用HelloWorld合约的`get`方法为例,我们可以使用以下命令调用该方法(其方法参数为空,因此我们在命令中没有提供任何方法参数):
```bash
node .\cli.js call hello_world 0x039ced1cd5bea5ace04de8e74c66e312ba4a48af get
Expand Down
8 changes: 4 additions & 4 deletions docs/quick_start/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,19 @@ Liquid是一种[嵌入式领域特定语言](http://wiki.haskell.org/Embedded_do

### 合约代码剖析

第1行用于向编译器表明,当以非std模式(即编译为Wasm字节码)编译时,启用`no_std`属性。出于安全性的考虑,Wasm对Rust标准库的支持较为有限,因此需要启用该属性以保证您的合约能够运行在Wasm虚拟机上。
第1行用于向编译器表明,当以非std模式(即编译为Wasm字节码)编译时,启用`no_std`属性。出于安全性的考虑,Wasm对Rust标准包的支持较为有限,因此需要启用该属性以保证您的合约能够运行在Wasm虚拟机上。

第3行用于导入`liquid_lang`库,Liquid诸多特性的实现均位于该库中
第3行用于导入`liquid_lang`包,Liquid诸多特性的实现均位于该包中

第5~6行用于声明`alloc`,由于我们在拼接字符串时使用了`format!`宏,而`format!`宏需要从`alloc`库中导入
第5~6行用于声明`alloc`,由于我们在拼接字符串时使用了`format!`宏,而`format!`宏需要从Rust语言的基础包`alloc`中导入

第8~51行是合约主体。本质上,合约就是一系列函数(合约方法)与数据(状态变量)的集合,在Liquid中,合约方法及状态变量封装于一个由`contract`属性标注的`mod`中,其中`contract`属性的定义位于`liquid_lang`中:

- 第8行声明合约了的元数据,元数据通过`contract`属性的参数传递给Liquid,主要包括要使用的Liquid版本等;

- 第12~15行定义了合约的状态变量,合约的所有状态变量需要被封装于一个由`liquid(storage)`属性标注的`struct`中。在`Hello World`合约中,声明了一个名为`name`的状态变量,其类型为`Value<String>`。此处`String`是状态变量的实际类型,即字符串;而`Value`则是一种状态变量容器。读写状态变量时需要对链上持久化的状态进行访问,而`Value`容器对这些操作进行了封装,因此可以使得您像使用普通变量那样使用状态变量。在**深入学习**一章中,我们还将学习到更多容器的使用;

- 第17~30行定义了合约方法,合约的所有方法需要被封装于一个由`liquid(methods)`属性标注的`impl`块中,且`impl`块的标识符需要与封装合约状态变量的`struct`的标识符相同(此合约中标识符都为"HelloWorld")。`HelloWorld`合约中,我们为合约定义了三个合约方法:
- 第17~30行定义了合约方法,合约的所有方法需要被封装于一个由`liquid(methods)`属性标注的`impl`块中,且`impl`块的标识符需要与封装合约状态变量的`struct`的标识符相同(此合约中标识符都为"HelloWorld")。在HelloWorld合约中,我们为合约定义了三个合约方法:
- `new`:构造函数。在Liquid中,每个合约有且仅能有一个构造函数,且构造函数的名称必须为`new`。构造函数会在合约部署时自动执行,但不能被用户或外部合约调用。此处的构造函数用于将合约状态变量`name`初始化为"Alice";

- `get`:法会将状态变量`name`的值拼接至字符串`"Hello, "`尾部,并将拼接后的新字符串返回给调用者;
Expand Down
Binary file added images/in_depth/architecture/ast.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/in_depth/architecture/liquid_arch.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/in_depth/architecture/rust_compile.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/in_depth/architecture/token_tree.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3c09ae9

Please sign in to comment.