想用Wasm开发dApp?你不得不读的入门教程

互联网 2019-10-23 15:51:42

在上期的技术视点中,我们简单介绍了如何在不依赖模板的情况下,完成一个简单的 Ontology Wasm 合约的开发,并介绍了 Ontology Wasm 工具库 API 文档的生成方式,方便开发者查询和调用已提供的功能。但我们发现在合约开发的过程中,通常需要进行以下操作:

  • 解析调用合约的参数;
  • 将自定义的结构体保存到链上;
  • 从链上读取已存在的数据并解析成原始的数据类型;
  • 跨合约调用的时候,传递目标合约需要的参数。
上面所列出来的情况,均涉及到参数的序列化和反序列化问题,本文将会详细介绍 Ontology Wasm 合约中参数序列化和反序列化的方法
图 | 网络

Encoder和Decoder接口

 
Encoder 接口定义了不同类型数据的序列化方法,具体实现逻辑请看sink.rs文件;Decoder 接口定义了不同类型数据的反序列化方法,具体实现逻辑请看source.rs文件。ontology-wasm-cdt-rust 工具库中已经为常用的数据类型实现 Encoder 和 Decoder 接口,有 u8、u16、u32、u64、u128、bool、Address、H256、Vec、&str、String 和 Tuple 多种类型。常用数据类型的序列化和反序列化示例如下:
let mut sink = Sink::new(16);
sink.write(1u128);
sink.write(true);
let addr = Address::repeat_byte(1);
sink.write(addr);
sink.write("test");
let vec_data = vec!["hello","world"];
sink.write(&vec_data);
let tuple_data = (1u8, "hello");
sink.write(tuple_data);
let mut source = Source::new(sink.bytes());
let res1:u128 = source.read().unwrap_or_default();
let res2:bool = source.read().unwrap_or_default();
let res3:Address = source.read().unwrap_or_default();
let res4:&str = source.read().unwrap_or_default();
let res5:Vec<&str> = source.read().unwrap_or_default();
let res6:(u8, &str) = source.read().unwrap_or_default();
assert_eq!(res1, 1u128);
assert_eq!(res2, true);
assert_eq!(res3, addr);
assert_eq!(res4, "test");
assert_eq!(res5, vec_data);
assert_eq!(res6, tuple_data);
sink.write()方法传入的参数支持所有已经实现 Encoder 接口的数据类型,序列化的时候需要声明其数据类型,比如1u128等。source.read()方法可以读取所有已经实现 Decoder 接口的数据类型,反序列化的时候要指明其结果的数据类型,如下面的代码:
let res1:u8 = source.read().unwrap_or_default();
在 res1后面要声明其数据类型是 u8。也可以写成:
let res1 = source.read_byte().unwrap_or_default();
在读取的时候,已经使用了read_byte,所以不需要在 res1后面声明数据类型了。

解析调用合约的参数

合约在获得调用参数时通过runtime::input()方法获得,但是该方法仅能拿到bytearray格式的参数,需要反序列化成对应的参数,第一个参数是合约方法名,后面的是方法参数,示例如下:

let input = runtime::input();
let mut source = Source::new(&input);
let method_name: &str = source.read().unwrap();
let mut sink = Sink::new();
match method_name {
    "transfer" => {
        let (from, to, amount) = source.read().unwrap();
        sink.write(ont::transfer(from, to, amount));
    }
    _ => panic!("unsupported action!"),
}
Rust 支持类型推导,大部分情况下可以省略类型声明,比如在ont::transfer()方法中已经声明了 from、to 和 amount 的数据类型,所以在前面解析 from、to 和 amount 的时候没有声明数据类型。

如果参数是 Vec<&str>类型,可以按照如下的方式进行反序列化:

let param:Vec<&str> = source.read().unwrap();
如果参数是 Vec<(&str,U128,Address)>类型,可以按照如下的方式进行反序列化:
let param:Vec<(&str,U128,Address)>= source.read().unwrap();

序列化自定义结构体

在合约开发中,我们经常需要对struct类型的数据进行序列化和反序列化,为了达到这个目的,我们仅需要在struct声明的地方加上#[derive(Encoder, Decoder)]即可,示例如下:

 #[derive(Encoder, Decoder)]
struct ReceiveRecord {
    account: Address,
    amount: u64,
}

#[derive(Encoder, Decoder)] struct EnvlopeStruct { token_addr: Address, total_amount: u64, total_package_count: u64, remain_amount: u64, remain_package_count: u64, records: Vec<ReceiveRecord>, }

在使用该功能的时候,要注意struct的每个字段必须都已经实现Encoder和Decoder接口,加上该注解后,就可以按照如下的方式序列化和反序列化了:
let addr = Address::repeat_byte(1);
let rr = ReceiveRecord{
    account: addr,
    amount: 1u64,
};
let es = EnvlopeStruct{
    token_addr: addr,
    total_amount: 1u64,
    total_package_count: 1u64,
    remain_amount: 1u64,
    remain_package_count: 1u64,
    records: vec![rr],
};
let mut sink = Sink::new(16);
sink.write(&es);

let mut source = Source::new(sink.bytes()); let es2:EnvlopeStruct = source.read().unwrap();

assert_eq!(&es.token_addr,&es2.token_addr); assert_eq!(&es.total_amount,&es2.total_amount); assert_eq!(&es.total_package_count,&es2.total_package_count); assert_eq!(&es.remain_amount,&es2.remain_amount); assert_eq!(&es.remain_package_count,&es2.remain_package_count);

从链上读取指定类型的数据

 

合约中不同类型的数据在保存到链上之前,需要先序列化成bytearray类型的数据,从链上读取数据时,读到的也都是bytearray类型的数据,需要反序列化成指定的数据类型。database模块提供了更加简便的接口供开发者使用。
  • fn put<K: AsRef<[u8]>, T: Encoder>(key: K, val: T) 根据 key 保存 T 类型的数据,要求 T 类型实现Encoder接口示例:
let es = EnvlopeStruct{
    token_addr: addr,
    total_amount: 1u64,
    total_package_count: 1u64,
    remain_amount: 1u64,
    remain_package_count: 1u64,
    records: vec![rr],
};
database::put("key", es);
我们从database::put的源码可以看到,该方法在执行的时候,会先序列化 es 参数,然后将序列化结果保存到链上。
  • fn get<K: AsRef<[u8]>, T>(key: K) -> Option<T> where for<'a> T: Decoder<'a> + 'static,根据 key 获得指定类型 T 的数据,其中,T 类型要求实现了 Decoder 接口。示例:
let res:EnvlopeStruct = database::get("key").unwrap();
我们从database::get的源码可以看到,该方法在执行的时候,会先从链上拿到 bytearray 类型的数据,然后再反序列化得到 EnvlopeStruct 类型的数据。

跨合约参数传递

 

在跨合约调用的时候,参数是以 bytearray 的格式进行传递的,所以需要将不同类型的数据进行序列化,下面以跨合约调用 Wasm 合约为例。

let (contract_address,method,a, b): (&Address,&str,u128, u128) = source.read().unwrap();

let mut sink = Sink::new(16);
sink.write(method);
sink.write(a);
sink.write(b);
let resv = runtime::call_contract(contract_address, sink.bytes()).expect("get no return");
该段代码实现了一个合约调用另一个 Wasm 合约的功能,调用的合约先解析出来参数,然后根据解析的参数去调用另外一个合约时,需要将方法名和方法参数依次序列化,通过 sink.bytes()方法拿到序列化的结果,最后通过runtime模块中的 call_contract方法实现跨合约调用的功能。

结语

本文详细介绍了 Ontology Wasm 合约中参数序列化和反序列化的方法。主要包括在合约执行的过程中如何解析外部传进来的参数,在合约中自定义的结构体如何进行序列化和反序列化,链上读取的 bytearray 类型的数据如何反序列化成原始的数据类型,以及原始的数据类型如何序列化成 bytearray 类型的数据保存到链上,最后介绍了跨合约调用中如何进行参数的传递。

Wasm 技术具有十分庞大活跃的社区,且 Wasm 可以支持更加复杂的合约,并拥有丰富的第三库支持,生态十分完善,开发门槛比较低,在 Ontology 链上开发和部署 Wasm 合约对于想要入门的开发者来说十分友好。

来源:公众号:本体研究院

相关资讯Relevent