通过 Libra 学习 Protobuf

Protobuf 是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于网络通信和数据存储,本文看看它如果应用在 Libra 中。

Libra 是 Facebook 牵头发布的基于稳定币的区块链项目,大家可以通过社区翻译的 Libra 中文文档入门 Libra。

编译安装相关依赖

通过执行./scripts/dev_setup.sh 是可以自动安装相关依赖以及编译整个 libra 系统的,参考 Libra 环境搭建
如果想自己手工安装 protobuf 相关依赖可以安装如下步骤:

1
2
cargo install protobuf
cargo install protobuf-codegen
  • 注意:我当前使用的是 v2.6.2

找一个文件试试

这是我从 libra 中抠出来的非源文件,位于 transaction.proto 。

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";

package types;
// Account state as a whole.
// After execution, updates to accounts are passed in this form to storage for
// persistence.
message AccountState {
    // Account address
    bytes address = 1;
    // Account state blob
    bytes blob = 2;
}

运行下面的命令:

1
protoc --rust_out . accountstate.proto

可以看到目录下会多出来一个 accountstate.rs
简单看一下生成的 AccountState 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#[derive(PartialEq,Clone,Default)]
pub struct AccountState {
    // message fields
    pub address: ::std::vec::Vec<u8>,
    pub blob: ::std::vec::Vec<u8>,
    // special fields
    pub unknown_fields: ::protobuf::UnknownFields,
    pub cached_size: ::protobuf::CachedSize,
}

impl<'a> ::std::default::Default for &'a AccountState {
    fn default() -> &'a AccountState {
        <AccountState as ::protobuf::Message>::default_instance()
    }
}

impl AccountState {
    pub fn new() -> AccountState {
        ::std::default::Default::default()
    }

    // bytes address = 1;


    pub fn get_address(&self) -> &[u8] {
        &self.address
    }
    pub fn clear_address(&mut self) {
        self.address.clear();
    }

    // Param is passed by value, moved
    pub fn set_address(&mut self, v: ::std::vec::Vec<u8>) {
        self.address = v;
    }

    // Mutable pointer to the field.
    // If field is not initialized, it is initialized with default value first.
    pub fn mut_address(&mut self) -> &mut ::std::vec::Vec<u8> {
        &mut self.address
    }

    // Take field
    pub fn take_address(&mut self) -> ::std::vec::Vec<u8> {
        ::std::mem::replace(&mut self.address, ::std::vec::Vec::new())
    }

    // bytes blob = 2;


    pub fn get_blob(&self) -> &[u8] {
        &self.blob
    }
    pub fn clear_blob(&mut self) {
        self.blob.clear();
    }

    // Param is passed by value, moved
    pub fn set_blob(&mut self, v: ::std::vec::Vec<u8>) {
        self.blob = v;
    }

    // Mutable pointer to the field.
    // If field is not initialized, it is initialized with default value first.
    pub fn mut_blob(&mut self) -> &mut ::std::vec::Vec<u8> {
        &mut self.blob
    }

    // Take field
    pub fn take_blob(&mut self) -> ::std::vec::Vec<u8> {
        ::std::mem::replace(&mut self.blob, ::std::vec::Vec::new())
    }
}

除了这些,还为 AccountState 自动生成了 protobuf::Message,protobuf::Clear 和 std::fmt::Debug 接口。

  • 注意如果是 Service 的话,一样会自动生成一个_grpc.rs 文件,用于服务的实现。

利用 build.rs 自动将 proto 编译成 rs

rust 在工程化方面做的非常友好,我们可以编译的过程都可以介入。
也就是如果我们的项目目录下有 build.rs,那么在运行 cargo build 之前会自动编译然后运行此程序。 相当于在项目目录下运行 cargo run build.rs 然后再去 build。
这看起来有点类似于 go 中的 //go:generate command argument..., 但是要更为强大,更为灵活。

build.rs

在 Libra 中包含了 proto 的子项目都会在项目根目录下包含一个 build.rs. 其内容非常简单。

1
2
3
4
5
6
7
8
9
10
fn main() {
    let proto_root = "src/proto";
    let dependent_root = "../../types/src/proto";

    build_helpers::build_helpers::compile_proto(
        proto_root,
        vec![dependent_root],
        false, /* generate_client_code */
    );
}

这是 storage_proto/build.rs, 主要有两个参数是 proto_root 和 dependent_root:

  1. proto_root : 表示要自动转换的 proto 所在目录
  2. dependent_root : 表示编译这些 proto 文件 import 所引用的目录,也就是 protoc -I 参数指定的目录。当然编译成的 rs 文件如果要正常工作,那么也必须编译 dependent_root 中的所有 proto 文件才行

至于第三个参数 generate_client_code, 则表示是否生成 client 代码,也就是如果 proto 中包含 Service,那么是否也生成 grpc client 的辅助代码。

简单解读 build_helper

build_helper 位于 common/build_helper,是为了辅助自动将 proto 文件编译成 rs 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pub fn compile_proto(proto_root: &str, dependent_roots: Vec<&str>, generate_client_code: bool) {
   let mut additional_includes = vec![];
   for dependent_root in dependent_roots {
       // First compile dependent directories
       compile_dir(
           &dependent_root,
           vec![], /* additional_includes */
           false,  /* generate_client_code */
       );
       additional_includes.push(Path::new(dependent_root).to_path_buf());
   }
   // Now compile this directory
   compile_dir(&proto_root, additional_includes, generate_client_code);
}

// Compile all of the proto files in proto_root directory and use the additional
// includes when compiling.
pub fn compile_dir(
   proto_root: &str,
   additional_includes: Vec<PathBuf>,
   generate_client_code: bool,
) {
   for entry in WalkDir::new(proto_root) {
       let p = entry.unwrap();
       if p.file_type().is_dir() {
           continue;
       }

       let path = p.path();
       if let Some(ext) = path.extension() {
           if ext != "proto" {
               continue;
           }
           println!("cargo:rerun-if-changed={}", path.display());
           compile(&path, &additional_includes, generate_client_code);
       }
   }
}

fn compile(path: &Path, additional_includes: &[PathBuf], generate_client_code: bool) {
   ...
}

build.rs 直接调用的就是 compile_proto 这个函数,他非常简单就是先调用 compile_dir 来编译所有的依赖,然后再编译自身.

而 compile_dir 则是遍历指定的目录,利用 WalkDir 查找当前目录下所有的 proto 文件,然后逐个调用 compile 进行编译.

rust 中的字符串处理

1
2
3
4
5
6
7
8
9
10
11
fn compile(path: &Path, additional_includes: &[PathBuf], generate_client_code: bool) {
    let parent = path.parent().unwrap();
    let mut src_path = parent.to_owned().to_path_buf();
    src_path.push("src");

    let mut includes = Vec::from(additional_includes);
    //写成additional_includes.to_owned()也是可以的
    let mut includes = additional_includes.to_owned(); //最终都会调用slice的to_vec
    includes.push(parent.to_path_buf());
    ....
}

要跟操作系统打交道,⾸先需要介绍的是两个字符串类型:OsString 以及它所对应的字符串切⽚类型 OsStr。它们存在于 std::ffi 模块中。

Rust 标准的字符串类型是 String 和 str。它们的⼀个重要特点是保证了内部编码是统⼀的 utf-8。但是,当我们和具体的操作系统打交道时,统⼀的 utf-8 编码是不够⽤的,某些操作系统并没有规定⼀定是⽤的 utf-8 编码。所以,在和操作系统打交道的时候,String/str 类型并不是⼀个很好的选择。 ⽐如在 Windows 系统上,字符⼀般是⽤ 16 位数字来表⽰的。

为了应付这样的情况,Rust 在标准库中又设计了 OsString/OsStr 来处理这样的情况。这两种类型携带的⽅法跟 String/str ⾮常类似,⽤起来⼏乎没什么区别,它们之间也可以相互转换。

Rust 标准库中⽤ PathBuf 和 Path 两个类型来处理路径。它们之间的关系就类似 String 和 str 之间的关系:⼀个对内部数据有所有权,还有⼀个只是借⽤。实际上,读源码可知,PathBuf ⾥⾯存的是⼀个 OsString,Path ⾥⾯存的是⼀个 OsStr。这两个类型定义在 std::path 模块中。

通过这种方式可以方便的在字符串和 Path,PathBuf 之间进行任意转换。
在 compile_dir 的第 23 行中,我们提供给 WalkDir::new 一个 & str,rust 自动将其转换为了 Path。

FromProto 和 IntoProto

出于跨平台的考虑,proto 文件中的数据类型表达能力肯定不如 rust 丰富,所以不可避免需要在两者之间进行类型转换。因此 Libra 中提供了 proto_conv 接口专门用于实现两者之间的转换.

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// Helper to construct and parse [`proto::storage::GetAccountStateWithProofByStateRootRequest`]
///
/// It does so by implementing `IntoProto` and `FromProto`,
/// providing `into_proto` and `from_proto`.
#[derive(PartialEq, Eq, Clone, FromProto, IntoProto)]
#[ProtoType(crate::proto::storage::GetAccountStateWithProofByStateRootRequest)]
pub struct GetAccountStateWithProofByStateRootRequest {
    /// The access path to query with.
    pub address: AccountAddress,

    /// the state root hash the query is based on.
    pub state_root_hash: HashValue,
}
/// Helper to construct and parse [`proto::storage::GetAccountStateWithProofByStateRootResponse`]
///
/// It does so by implementing `IntoProto` and `FromProto`,
/// providing `into_proto` and `from_proto`.
#[derive(PartialEq, Eq, Clone)]
pub struct GetAccountStateWithProofByStateRootResponse {
    /// The account state blob requested.
    pub account_state_blob: Option<AccountStateBlob>,

    /// The state root hash the query is based on.
    pub sparse_merkle_proof: SparseMerkleProof,
}

针对 GetAccountStateWithProofByStateRootRequest 可以自动在 crate::proto::storage::GetAccountStateWithProofByStateRootRequest 和 GetAccountStateWithProofByStateRootRequest 之间进行转换,只需要 derive(FromProto,IntoProto) 即可。
而针对 GetAccountStateWithProofByStateRootResponse 则由于只能手工实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
impl FromProto for GetAccountStateWithProofByStateRootResponse {
    type ProtoType = crate::proto::storage::GetAccountStateWithProofByStateRootResponse;

    fn from_proto(mut object: Self::ProtoType) -> Result<Self> {
        let account_state_blob = if object.has_account_state_blob() {
            Some(AccountStateBlob::from_proto(
                object.take_account_state_blob(),
            )?)
        } else {
            None
        };
        Ok(Self {
            account_state_blob,
            sparse_merkle_proof: SparseMerkleProof::from_proto(object.take_sparse_merkle_proof())?,
        })
    }
}

impl IntoProto for GetAccountStateWithProofByStateRootResponse {
    type ProtoType = crate::proto::storage::GetAccountStateWithProofByStateRootResponse;

    fn into_proto(self) -> Self::ProtoType {
        let mut object = Self::ProtoType::new();

        if let Some(account_state_blob) = self.account_state_blob {
            object.set_account_state_blob(account_state_blob.into_proto());
        }
        object.set_sparse_merkle_proof(self.sparse_merkle_proof.into_proto());
        object
    }
}

本文作者为深入浅出共建者:白振轩

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享