Rust contract part 2 - Write contract with ckb-std

Rust CKB English

Edited at 2020-03-27

  • Update ckb-std and ckb-tool

This article introduces the ckb-std library; and shows how to rewrite our minimal contract with ckb-std, to enables syscalls and Vec, String.

The previous contract:

#![no_std]
#![no_main]
#![feature(asm)]
#![feature(lang_items)]

#[no_mangle]
pub fn _start() -> ! {
    exit(0)
}

/// Exit syscall
pub fn exit(_code: i8) -> ! {
    unsafe {
        // a0 is _code
        asm!("li a7, 93");
        asm!("ecall");
    }
    loop {}
}

#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

#[no_mangle]
pub fn abort() -> ! {
    panic!("abort!")
}

Makefile

We compile and run tests again and again in the previous article, for convenient, let’s write a Makefile first:

test: clean build patch
	cargo test -- --nocapture

build:
	cd contract && cargo build

clean:
	cd contract && cargo clean

C := contract/target/riscv64imac-unknown-none-elf/debug/contract
patch:
	ckb-binary-patcher -i $C -o $C

The make test is simple: rebuild the contract binary then run unit tests.

It is worth to notice the patch task, which calls ckb-binary-patcher to patch the contract binary; Its a solution for fixing VM’s buggy instructions, even we developed the CKB-VM diligently, there no bug-free software. Unfortunately, as the nature of blockchain, we can’t just fix the VM without a hard-fork. A better approach is to patch binary to get across the buggy instruction. You can see this issue for details.

Installing the ckb-binary-patcher:

cargo install --git https://github.com/xxuejie/ckb-binary-patcher.git

Then type make test to compile and test contract.

Hidden complexity under macro

Now let’s get back to our contract, the code is little complicated for a “hello world”. Let’s slim it, we begin with wrapping _start function:

#[no_mangle]
pub extern "C" fn _start() -> ! {
    exit(main())
}

pub fn main() -> i8 {
    // code...
    0
}

Now we can write code in the main function, that’s looking more comfortable, except the _start function is annoying; we can use a macro to hide the _start:

#[macro_export]
macro_rules! entry {
    ($main:path) => {
        #[no_mangle]
        pub extern "C" fn _start() -> ! {
            let f: fn() -> i8 = $main;
            ckb_std::syscalls::exit(f())
        }
    }
}

The entry macro defines the _start function which just calls main then exits the program with syscall exit; our contract code is below:

pub fn main() -> i8 {
    // code...
    0
}

entry!(main);

The Rust macro system is powerful, we can hidden other annoying functions and definitions under the macro; this is the basic idea of ckb-std; let’s refactor the contract with ckb-std:

#![no_std]
#![no_main]
#![feature(lang_items)]
#![feature(alloc_error_handler)]
#![feature(panic_info_message)]

use ckb_std::{entry, default_alloc};

#[no_mangle]
pub fn main() -> i8 {
    // code...
    0
}

entry!(main);
// define global allocator
default_alloc!();

This code looks good enough for a “hello world” program. The rustc requires the definition of features in the file, so we still need to keep them, but we hide other functions include a well-implemented panic handler and a global allocator in macros.

ckb std

Let’s try using Vec, String from alloc crate, and use the debug syscall to output under the test environment.

/// features...

use alloc::vec;
use ckb_std::{debug, entry, default_alloc};

#[no_mangle]
pub fn main() -> i8 {
    let v = vec![0u8; 42];
    debug!("{:?}", v.len());
    0
}

entry!(main);
default_alloc!();

// We can see the debug output under test environment.
// ->
// 0x423f33c96845ca512f4c9e9b19015481a4a8db1cf56dd1ddff8cecc17c38ac5d [contract debug] 42

References: