How to Develop Wasm Contract with Rust on Ontology
Since the Ontology Wasm was launched on the Ontology TestNet, it has received wide attention from community developers as this technology reduces the cost of moving dApp contracts with complex business logic onto the blockchain, thus greatly enriching the dApp ecosystem.
Ontology Wasm currently supports development in both Rust and C++ languages. The Rust language has better support for Wasm, and the generated bytecode is simpler, which can further reduce the cost of contract calls. So how do you use Rust to develop a contract on Ontology?
Develop Wasm Contract with Rust
Create a Contract
Cargo is a good project creation and package management tool for developing Rust programs that helps developers better organize code and third-party library dependencies. To create a new Ontology Wasm contract, just execute the following command:
The project structure it generates is:
The Cargo.toml file is used to configure project basic information and dependent library information. The [lib] section in the file must be set to crate-type = [“cdylib”]; the lib.rs file is used for writing contract logic code. In addition, you need to add dependency settings in the [dependencies] section of the configuration file Cargo.toml:
With this dependency, developers can call interfaces that interact with the Ontology blockchain and tools such as parameter serialization.
Contract Entry Function
Each program has an entry function, such as the main function we commonly see, but the contract does not have a main function. When developing a Wasm contract with Rust, the invoke function is used by default as the entry function for contract execution. The function name in Rust will be confused when compiling the Rust source code into bytecode that the virtual machine can execute. To prevent the compiler from generating extra bytecode and to reduce the contract size, the invoke function adds the #[no_mangle] annotation.
How does the invoke function get the parameters for transaction execution? The ontio_std library provides the runtime::input() function to receive parameters for transaction execution. Developers can use ZeroCopySource to deserialize the received byte array. Amongst them, the first byte array that is read out is the name of the invoke method, and then the method parameters.
How is the contract execution result returned? The runtime::ret function provided by the ontio_std library returns the result of the method execution.
A complete invoke function is as follows:
Contract Data Serialization and Deserialization
In the contract development process, developers always encounter serialization and deserialization problems, that is, how to save a struct type of data to the database and how the byte array read from the database is deserialized to obtain struct type data.
The ontio_std library provides the Decoder and Encoder interfaces for serializing and deserializing data. The fields of the struct structure also implement the Decoder and Encoder interfaces so that the struct can be serialized and deserialized. Sink instances are required when serializing various data types. The Sink instance has a set type field buf, which stores byte type data, and all serialized data is stored in buf.
For fixed-length data (for example: byte, u16, u32, u64, etc.), the data is directly converted into a byte array and then stored in buf; for data of non-fixed length, the length needs to be serialized first, and then the D\data (such as unsigned integers of unknown size, including u16, u32 or u64, etc.).
Deserialization is just the opposite. For every serialization method, there is a corresponding deserialization method. Deserialization requires the use of Source instances. This instance has two fields, buf and pos. Buf is used to store the data to be deserialized, and pos is used to store the current read position. When reading a specified type of data, if you know its length, you can read it directly; for data of unknown length, read the length first, and then read the content.
Accessing and Updating Data on the Chain
Ontology-wasm-cdt-rust has encapsulated the operation method of the on-chain data, which is convenient for developers to implement operations such as adding, deleting, changing, and querying data on the chain as follows:
Ø database::get(key) is used to query data from the chain, and key requires the implementation of the AsRef interface.
Ø database::put(key, value) is used to store data on the chain. The key requires the implementation of the AsRef interface and the value requires the implementation of the Encoder interface.
Ø database::delete(key) is used to delete data from the chain, and the key requires the implementation of the AsRef interface.
When implementing contract methods, we need to access the data on the chain and need the corresponding virtual machine to execute the contract bytecode, so it is generally necessary to deploy the contract on the chain for testing. But such a test method is troublesome. To make it easier for developers to test contracts, the ontio_std library provides a mock test module. This module provides the simulation of the data on the chain, making it easy for developers to unit test the methods in the contract. Specific examples can be found here.
Developers can use console::debug(msg) to output debugging information during contract debugging. The msg information will be printed in the node log. There is a precondition that the log level needs to be set to debug mode when the Ontology local test node is started.
In addition, developers can also use runtime::notify(msg) to output relevant debugging information during contract debugging. This method will save the printed information to the chain and can be queried from the chain via the getSmartCodeEvent method.
As a leading public chain, Ontology is one of the first projects that support the Wasm contract and is doing its bit to develop Wasm technology. At the same time, we also welcome more enthusiasts of Wasm technology to join the Ontology developer community to build a technological ecosystem.