1. Cargo
cargo new <project-name>
to create a new projectcargo add <crate-name>
to add a crate
2. Rules for Modules
Consider the following project structure:
2.1. Define a module (By File)
By default we can can create a module by creating simply a blockchain.rs
file.
2.2. Define a module (By Folder)
If we want to create a module by directory blockchain/
, we need an entrypoint named mod.rs
in that directory.
2.3. Export
In blockchain/mod.rs
or blockchain/*.rs
, we mark the target object that we want to export by pub
.
2.4. Import module
In main.rs
if we want to import objects from blockchain/
, we write:
// main.rs // ask rust to grab the blockchain.rs or blockchain/mod.rs mod blockchain; // what to import from blockchain/mod.rs use blockchain::{Block, BlockChain, BlockSearch, BlockSearchResult};
Rust will search for the file blockchain.rs
, if nothing is matched then it looks for blockchain/mod.rs
.
Each file is itself a namespace/module.
2.5. Import submodule
If we want to make the submodule blockchain/transaction.rs
accessible by main.rs
, first we declare to export that submodule in mod.rs
:
// blockchain/mod.rs pub mod transaction // accessible by main.rs // mod trasnaction // accessible only by blockchain/mod.rs.
Then in main.rs
(if Transaction
is defined in blockchain/transaction.rs
):
mod blockchain; use blockchain::transaction::Transaction
2.6. Import submodule from submodule (use crate::)
Assume now:
and we have
// blockchain/new_module.rs pub struct NewModule { pub name: String, }
then to import this NewModule
into transaction.rs
we
-
mod new_module
to declare a module inmod.rs
and -
use crate::blockchain::new_module::NewModule;
intransaction.rs
.
3. Implement Methods to a Struct
As in golang
or kotlin
we can define additional method to an existing struct/class:
pub struct Transaction { sender_address: Vec<u8>, recipient_address: Vec<u8>, value: u64, } impl Transaction { pub fn new(sender: Vec<u8>, recipient: Vec<u8>, value: u64) -> Transaction { Transaction { sender_address: sender, recipient_address: recipient, value, } } pub fn print(&self) { println!("sender_address {:?}", self.sender_address); println!("recipient_address: {:?}", self.recipient_address); } }
4. The println!
Function
println!
Function-
When we want rust to print a custom struct, add
#[derive(Debug)]
to let rust generate and formatting function for us. These struct can be printed byprintln!("some statement {:?}", some_struct)
-
For primitive type we simple write
println!("some value {}", some_value)
-
To print a struct with customized logic, see this section
5. Trait (Interface)
A trait is simply an interface in other programming languages:
5.1. Custom Trait
pub trait Serialization<T> { fn serialization(&self) -> Vec<u8>; fn deserialization(bytes: Vec<u8>) -> T; }
To let a struct implement a trait, we write
impl Serialization<Transaction> for Transaction { fn serialization(&self) -> Vec<u8> { ... } }
5.2. Predefined Trait: std::fmt::Display
std::fmt::Display
Sometimes we would like to define the custom logging of our own structs via
println!("the object {}", the_object)
while the {:?}
that prints [#derive(Debug)]
-annotated struct has no desired custom logic, here is how we do it:
use std::fmt; impl fmt::Display for Transaction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "\n{}\nSender address: {:?}\nRecipent addresss: {:?}\nValue: {}\n{}", "-".repeat(40), self.sender_address, self.recipient_address, self.value, "-".repeat(40) ) } }
6. Convert into Bytes and Convert backwards
In some ocassion we need to serialize data efficiently via converting everythings into bytes.
6.1. usize
Nowadays our OS is typically in 64-bit, the usize
by default refers to u64
, which takes 8 bytes:
let forward: Vec<u8> = (100_000_000 as usize).to_be_bytes().to_vec(); // we get 100_000_000 again: let backard = usize::from_be_bytes(bytes[0..8].try_into().unwrap());
Here bytes[0..8]
is a [u8]
type variable, which is known as an unsized variable that cannot be used anywhere (rust needs to know the size at compile time).
Here is what's happening:
let slice = bytes[0..8] .try_into() // try to convert the unsized array into a sized one .unwrap() // extract value from Ok(&[u8])
6.2. String
let forward: Vec<u8> = "some_string".as_bytes().to_vec(); let backward: String = String::from_utf8(in_bytes).unwrap();
7. Operator Overloading by Implementing Traits
7.1. +
+
Assume that we want to overload the meaning of +
operator, then we simply write:
use std::ops::AddAssign; // vvvvv type on the RHS impl AddAssign<i32> for Block { // vvvvv type on the RHS fn add_assign(&mut self, rhs: i32) { self.nonce += rhs; } }
Our custom struct can now add an integer: block + 1
to mean block.nonce += 1
.
7.2. ==
==
use std::cmp::PartialEq; pub struct Block { nonce: i32, previous_hash: Vec<u8>, time_stamp: u128, transactions: Vec<Vec<u8>>, } impl PartialEq for Block { fn eq(&self, other: &Self) -> bool { let self_hash = self.hash(); let other_hash = other.hash(); self_hash == other_hash } fn ne(&self, other: &Self) -> bool { !(*self == *other) } }
7.3. [index]
[index]
Note that we have to define the Output
type:
use std::ops::{Index}; pub struct BlockChain { transaction_pool: Vec<Vec<u8>>, chain: Vec<Block>, } impl Index<usize> for BlockChain { type Output = Block; fn index(&self, index: usize) -> &Self::Output { let res = self.chain.get(index); match res { Some(block) => { return block; } None => { panic!("index out of range for the chain"); } } } }