藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

CITA 的命令行工具(三)

cita-tool 在刚开始的时候,很大程度上借鉴了我另外一个库 InfluxdbClinet-rs 的实现,但在实现过程中,越来越领悟到 Rust trait 的作用,也就越来越偏向于使用 trait、trait object、generic,实现更高级的抽象。

从 influxdbclient 讲起

influxdbclient 这个库是我刚接触 Rust 三个月左右开始写的一个库,当时主要是在写 Python 和一点点 C。业余时间接触 Rust 三个月之后,总是要走出第一步,造一个轮子,刚好当时项目上需要使用 influxdb,并且 Rust 这边社区的库作者说他要去写 Go 了,并不会经常维护。于是,参照他的实现和官方的 Python 库,从头开始写起。

0.2 版本里面,这个库的实现里面有定义一些 trait,如下:

pub struct InfluxdbClient {
    host: String,
    db: String,
    username: String,
    passwd: String,
    read_timeout: Option<u64>,
    write_timeout: Option<u64>,
}

pub trait InfluxClient {
    /// Query whether the corresponding database exists, return bool
    fn ping(&self) -> bool;

    /// Query the version of the database and return the version number
    fn get_version(&self) -> Option<String>;

    /// Write a point to the database
    fn write_point(&self, point: Point, precision: Option<Precision>, rp: Option<&str>) -> Result<bool, error::Error>;
    ...
}

impl InfluxClient for InfluxdbClient {
    ...
}

在这个库升级到 0.3 版本的时候,我把所有自己定义的 trait 都删掉了,让实现和类型完全绑定在一起。当时的想法其实很简单,trait 的定义在库中完全是多此一举,导致了很多没有必要的引入,整个库非常简单,引入 Client,就能与 influxdb 进行交互,并不需要 trait。

在 Rust 里面做高级抽象

但是,当你真的想要在 Rust 里面做更好的抽象的时候,你会发现,trait 是必须要了解并掌握的一个概念。在 Rust 标准库里面,定义了大量 trait,用来统一抽象某些行为,比如 Read、Write 抽象 IO 的行为。

在 Rust 中,trait 承担了很多责任:

trait 并不是一种类型,但 trait object 是一种类型,它与泛型的区别在于编译器的处理,泛型属于静态分派,trait object 属于动态分派。这两种行为都是在 Rust 里面实现 “多态” 的重要手段。

就如同比较有代表性的 Python 里面实现多态:

from abc import ABCMeta, abstractmethod

class Animal:
    __metaclass__ = ABCMeta

    def __init__(self):
        pass
    
    @abstractmethod
    def name(self):
        pass
    
    @abstractmethod
    def call(self):
        pass
    
class Dog(Animal):
    def __init__(self):
        self.name = "Dog"
        self.call = "汪"
    
    def name(self):
        print(self.name)
        
    def call(self):
        print(self.call)
        
class Cat(Animal):
    def __init__(self):
        self.name = "Cat"
        self.call = "喵"
    
    def name(self):
        print(self.name)
        
    def call(self):
        print(self.call)

而这些在 Rust 里面相对应来实现:

trait Animal {
    fn name(&self);
    fn call(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn name(&self) {
        println!("Dog");
    }
    
    fn call(&self) {
        println!("汪");
    }
}

impl Animal for Cat {
    fn name(&self) {
        println!("Cat");
    }
    
    fn call(&self) {
        println!("喵");
    }
}

在 Rust 里面,就连线程安全的约束都是由两个 trait 去约束的,Rust 语言本身并不知晓 “线程” “并发” 是什么,而是抽象出一些高级的 trait,用来描述类型在并发环境下的特性:

pub fn spawn<F, T>(f: F) -> JoinHandle<T> where
    F: FnOnce() -> T, F: Send + 'static, T: Send + 'static
{
    Builder::new().spawn(f).unwrap()
}

cita-tool 中的 trait 演进

rpc 接口 trait

最早,整个项目只有一个 trait,用来定义 jsonrpc 接口,而且所有的类型都写死,扩展的可能性很低:

pub trait ClientExt {
    /// net_peerCount: Get network peer count
    fn get_net_peer_count(&mut self, url: &str) -> u32;
    /// cita_blockNumber: Get current height
    fn get_block_number(&mut self, url: &str) -> Option<u64>;
    /// cita_sendTransaction: Send a transaction return transaction hash
    fn send_transaction(
        &mut self,
        url: &str,
        code: &str,
        address: &str,
        current_height: u64,
    ) -> Result<String, String>;
    /// eth_getTransactionReceipt: Get transaction receipt
    fn get_transaction_receipt(&mut self, url: &str, hash: &str) -> JsonRpcResponse;
	...
}

之后为了统一处理整个接口,修改为使用关联类型加泛型的模式:

pub trait ClientExt<T, E>
where
    T: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
    E: Fail,
{
    /// Rpc response
    type RpcResult;

    /// net_peerCount: Get network peer count
    fn get_net_peer_count(&mut self, url: &str) -> Self::RpcResult;
    /// cita_blockNumber: Get current height
    fn get_block_number(&mut self, url: &str) -> Self::RpcResult;
    /// cita_sendTransaction: Send a transaction return transaction hash
    fn send_transaction(
        &mut self,
        url: &str,
        code: &str,
        address: &str,
        current_height: Option<u64>,
        quota: Option<u64>,
        value: Option<u64>,
        blake2b: bool,
    ) -> Self::RpcResult;
    /// eth_getTransactionReceipt: Get transaction receipt
	fn get_transaction_receipt(&mut self, url: &str, hash: &str) -> Self::RpcResult;
    ...
}

后来发现关联类型实际上并没有达到我想要的泛型扩展,同时因为参数太多,定义了一个类型 TransactionOptions,最后改成:

pub trait ClientExt<T, E>
where
    T: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
    E: Fail,
{
    /// peerCount: Get network peer count
    fn get_peer_count(&self) -> Result<T, E>;
    /// blockNumber: Get current height
    fn get_block_number(&self) -> Result<T, E>;
    /// sendTransaction: Send a transaction and return transaction hash
    fn send_raw_transaction(&mut self, transaction_option: TransactionOptions) -> Result<T, E>;
    /// getBlockByHash: Get block by hash
    fn get_block_by_hash(&self, hash: &str, transaction_info: bool) -> Result<T, E>;
    /// getBlockByNumber: Get block by number
    fn get_block_by_number(&self, height: &str, transaction_info: bool) -> Result<T, E>;
    /// getTransactionReceipt: Get transaction receipt
	fn get_transaction_receipt(&self, hash: &str) -> Result<T, E>;
    ...
}

这里关联类型因为不支持下面这种写法,被丢弃了:

pub trait ClientExt<T, E>
where
    T: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
    E: Fail,
{
    /// Rpc response
    type RpcResult = Result<T, E>;
    
    /// peerCount: Get network peer count
    fn get_peer_count(&self) -> Self::RpcResult;
    /// blockNumber: Get current height
    fn get_block_number(&self) -> Self::RpcResult;
    /// sendTransaction: Send a transaction and return transaction hash
    fn send_raw_transaction(&mut self, transaction_option: TransactionOptions) -> Self::RpcResult;
    /// getBlockByHash: Get block by hash
    fn get_block_by_hash(&self, hash: &str, transaction_info: bool) -> Self::RpcResult;
    /// getBlockByNumber: Get block by number
    fn get_block_by_number(&self, height: &str, transaction_info: bool) -> Self::RpcResult;
    /// getTransactionReceipt: Get transaction receipt
	fn get_transaction_receipt(&self, hash: &str) -> Self::RpcResult;
    ...
}

系统合约 trait

cli 在实现过程中,增加了对 CITA 系统合约的支持,用来方便用户更加轻松地调用 CITA 的系统合约,最初,所有合约的实现都是一个 trait 定义的:

/// High degree of encapsulation of system contract operation
pub trait ContractExt: ClientExt<JsonRpcResponse, ToolError> {
    /// Get node status
    fn node_status(&mut self, url: &str, address: &str) -> Self::RpcResult {
        let code = format!(
            "{function}{complete}{param}",
            function = "0x645b8b1b",
            complete = "0".repeat(24),
            param = remove_0x(address)
        );
        self.call(
            url,
            None,
            "00000000000000000000000000000000013241a2",
            Some(&code),
            "latest",
		)
    }
	...
}

因为合约地址散落在每个 trait 方法里面,这样维护起来非常难受,如果有笔误,几乎就是在大海捞针,并且没有很好得区分每个系统合约,杂乱无章。

接下来,我们尝试为每个系统合约定义一个 trait,用来表达该合约的调用行为,以 node manage 合约为例:

/// High degree of encapsulation of system contract operation
pub trait NodeManageExt: ClientExt<JsonRpcResponse, ToolError> {
    /// contract address
    const address;
    /// Get node status
    fn node_status(&mut self, url: &str, address: &str) -> Self::RpcResult {
        let code = format!(
            "{function}{complete}{param}",
            function = "0x645b8b1b",
            complete = "0".repeat(24),
            param = remove_0x(address)
        );
        self.call(
            url,
            None,
            Self::address,
            Some(&code),
            "latest",
		)
    }
	...
}

将 address 定义为每个 trait 的关联 const,这样可以把合约地址统一管理在 trait 的关联类型上,但是,这里又会出现一个问题,把多个合约 trait 同时对同一个类型进行实现的时候,会碰到命名空间冲突的问题,因为都是 const address,于是这个版本直接没在 git 仓库历史里面出现过就被 pass 了。

接着,为了解决命名空间污染的问题,决定把每个合约都做成一个 struct 类型,每个合约自己的类型由自己的 trait 实现,同时统一抽象一个对于系统合约来说底层的抽象行为 trait:

统一抽象 trait:

/// Call/SendTx to a contract method
pub trait ContractCall {
    /// Rpc response
    type RpcResult;

    /// Prepare contract call arguments
    fn prepare_call_args(
        &self,
        name: &str,
        values: &[&str],
        to_addr: Option<Address>,
    ) -> Result<(String, String), ToolError>;

    /// SendTx a contract method
    fn contract_send_tx(
        &mut self,
        url: &str,
        name: &str,
        values: &[&str],
        to_addr: Option<Address>,
        blake2b: bool,
    ) -> Self::RpcResult;

    /// Call a contract method
    fn contract_call(
        &self,
        url: &str,
        name: &str,
        values: &[&str],
        to_addr: Option<Address>,
    ) -> Self::RpcResult;
}

每个具体合约 trait:

/// Group System Contract
pub trait GroupExt: ContractCall {
    /// Create a ContractClient
    fn create(client: Option<Client>) -> Self;

    /// Call a group query function
    fn group_query(
        &self,
        url: &str,
        function_name: &str,
        values: &[&str],
        address: &str,
    ) -> Self::RpcResult;
    ...
}

重复的 impl 行为,就通过实现过程宏 derive 进行进一步压缩,有关过程宏实现可以看这里

接下来,为了提高扩展性,将有关系统合约的 trait 实现也写成了泛型模式,整个实现就显得更加灵活:

/// Call/SendTx to a contract method
pub trait ContractCall<R, E>
where
    R: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
    E: Fail,
{
    /// Prepare contract call arguments
    fn prepare_call_args(
        &self,
        name: &str,
        values: &[&str],
        to_addr: Option<Address>,
    ) -> Result<(String, String), E>;

    /// SendTx a contract method
    fn contract_send_tx(
        &mut self,
        name: &str,
        values: &[&str],
        quota: Option<u64>,
        to_addr: Option<Address>,
    ) -> Result<R, E>;

    /// Call a contract method
    fn contract_call(
        &self,
        name: &str,
        values: &[&str],
        to_addr: Option<Address>,
        height: Option<&str>,
    ) -> Result<R, E>;

    /// Call a contract method with a to_address
    fn contract_call_to_address(
        &self,
        function_name: &str,
        values: &[&str],
        address: &str,
        height: Option<&str>,
    ) -> Result<R, E> {
        let address = Address::from_str(remove_0x(address)).unwrap();
        self.contract_call(function_name, values, Some(address), height)
    }
}

小结

其实整个库的真正业务相关的逻辑,都在这些 trait 的折腾上面了。如果对我本身 client 的实现不爽,就可以根据这些泛型 trait 实现自己的一套代码,只需要引用这些 trait 就可以了。要说现在对这个库还有不满意的地方,就是 rpctypes 的定义了,因为确实没有对整个 input 和 output 做整理,所以这些 types 都是一些很简单的开放式定义,所有的 field 都是 pub 的,不限制任何使用。

回想整个过程,确实不易,说了这么多 sdk 的实现,下面一篇将讲解 cli 执行程序的实现。

评论区

加载更多

登录后评论