Dynamic loading in Capsule
Introduction
Many contracts have a demand for cryptography primitives. In contracts written in Rust, we can easily integrate a cryptography library by adding it as a dependency. But it is not efficient; first, it increases the binary size of the contract; we need to spend more coins to deploy the contract. Second, each contract may include duplicated libraries; it is a waste of the on-chain space.
We introduce the dynamic loading mechanism to solve this problem:
- A shared library can be loaded in different programming languages
 - Using dynamic loading can significantly reduce the contract binary size.
 - Using shared libraries increases the utility of the on-chain space.
 
Starting from the v0.6 version, ckb-std introduces the dynamic loading module, which provides a high-level interface to dynamically loading libraries from on-chain cells.
In this tutorial, we build an example shared library in C, and try to dynamically load the shared library from a contract written in Rust.
If you run into an issue on this tutorial you can create a new issue or contact us on Nervos talk or Discord.
Setup the develop environment
Install Capsule
Prerequisites
The following must be installed and available to use Capsule.
- Cargo and Rust - Capsule uses cargo to generate Rust contracts and run tests. Install Rust
 - Docker - Capsule uses 
dockercontainer to reproducible build contracts. It's also used bycross. https://docs.docker.com/get-docker/ cross-rs- Capsule usescrossto build rust contracts. Install with
# Do this after you installed cargo
cargo install cross --git https://github.com/cross-rs/cross
Note: The current user must have permission to manage Docker instances. For more information, see Manage Docker as a non-root user.
Now you can proceed to install Capsule. It is recommended to download the binary here.
Or you can install Capsule from it's source:
cargo install capsule --git https://github.com/nervosnetwork/capsule.git --tag v0.1.3
Then check if it works with the following command:
capsule check
(click here to view response)
------------------------------
cargo      installed
docker     installed
cross-util installed
ckb-cli    installed v1.4.0 (required v1.2.0)
------------------------------
Create a project
capsule new dynamic-loading-demo
(click here to view response)
New project "dynamic-loading-demo"
Created file "capsule.toml"
Created file "deployment.toml"
Created file "README.md"
Created file "Cargo.toml"
Created file ".gitignore"
Created "/home/jjy/workspace/dynamic-loading-demo"
     Created binary (application) `dynamic-loading-demo` package
Created contract "dynamic-loading-demo"
Created tests
     Created library `tests` package
Done
Make a shared library
We create a directory to put our C code.
cd dynamic-loading-demo
mkdir shared-lib
We define two functions in our shared library. The visibility attribute tells the compiler to export the following symbol to the shared library.
// shared-lib/shared-lib.c
typedef unsigned long size_t;
__attribute__((visibility("default"))) int
plus_42(size_t num) {
  return 42 + num;
}
__attribute__((visibility("default"))) char *
foo() {
  return "foo";
}
We need the RISC-V gnu toolchain to compile the source. Fortunately, we can set up the compiling environment with Docker:
Create the share-lib/Makefile
TARGET := riscv64-unknown-linux-gnu
CC := $(TARGET)-gcc
LD := $(TARGET)-gcc
OBJCOPY := $(TARGET)-objcopy
CFLAGS := -fPIC -O3 -nostdinc -nostdlib -nostartfiles -fvisibility=hidden -I deps/ckb-c-stdlib -I deps/ckb-c-stdlib/libc -I deps -I deps/molecule -I c -I build -I deps/secp256k1/src -I deps/secp256k1 -Wall -Werror -Wno-nonnull -Wno-nonnull-compare -Wno-unused-function -g
LDFLAGS := -Wl,-static -fdata-sections -ffunction-sections -Wl,--gc-sections
# docker pull nervos/ckb-riscv-gnu-toolchain:gnu-bionic-20191012
BUILDER_DOCKER := nervos/ckb-riscv-gnu-toolchain@sha256:aae8a3f79705f67d505d1f1d5ddc694a4fd537ed1c7e9622420a470d59ba2ec3
all-via-docker:
    docker run --rm -v `pwd`:/code ${BUILDER_DOCKER} bash -c "cd /code && make shared-lib.so"
shared-lib.so: shared-lib.c
    $(CC) $(CFLAGS) $(LDFLAGS) -shared -o $@ $<
    $(OBJCOPY) --only-keep-debug $@ $@.debug
    $(OBJCOPY) --strip-debug --strip-all $@
Run make all-via-docker to compile the shared-lib.so.
Dynamic loading
We use CKBDLContext::load to load a library. To use this function, we need to know the data_hash of the target shared library.
Create a build.rs file:
touch contracts/dynamic-loading-demo/build.rs
The build.rs (aka build scripts) execute on the building stage, see details , in build.rs we compute the data_hash of shared.so and put the result into a constant variable.
Add blake2b crate as build dependencies.
[build-dependencies]
blake2b-rs = "0.1.5"
Write the constant CODE_HASH_SHARED_LIB to file code_hashes.rs.
pub use blake2b_rs::{Blake2b, Blake2bBuilder};
use std::{
    fs::File,
    io::{BufWriter, Read, Write},
    path::Path,
};
const BUF_SIZE: usize = 8 * 1024;
const CKB_HASH_PERSONALIZATION: &[u8] = b"ckb-default-hash";
fn main() {
    let out_path = Path::new("src").join("code_hashes.rs");
    let mut out_file = BufWriter::new(File::create(&out_path).expect("create code_hashes.rs"));
    let name = "shared-lib";
    let path = format!("../../shared-lib/{}.so", name);
    let mut buf = [0u8; BUF_SIZE];
    // build hash
    let mut blake2b = new_blake2b();
    let mut fd = File::open(&path).expect("open file");
    loop {
        let read_bytes = fd.read(&mut buf).expect("read file");
        if read_bytes > 0 {
            blake2b.update(&buf[..read_bytes]);
        } else {
            break;
        }
    }
    let mut hash = [0u8; 32];
    blake2b.finalize(&mut hash);
    write!(
        &mut out_file,
        "pub const {}: [u8; 32] = {:?};\n",
        format!("CODE_HASH_{}", name.to_uppercase().replace("-", "_")),
        hash
    )
        .expect("write to code_hashes.rs");
}
pub fn new_blake2b() -> Blake2b {
    Blake2bBuilder::new(32)
        .personal(CKB_HASH_PERSONALIZATION)
        .build()
}
Run capsule build, the file src/code_hashes.rs will be generated.
We define the module code_hashes in the lib.rs. Then add the dynamic loading code to the main function.
mod code_hashes;
use code_hashes::CODE_HASH_SHARED_LIB;
use ckb_std::dynamic_loading::{CKBDLContext, Symbol};
//...
// Create a DL context with 64K buffer.
let mut context = CKBDLContext::<[u8; 64 * 1024]>::new();
// Load library
let lib = context.load(&CODE_HASH_SHARED_LIB).expect("load shared lib");
// get symbols
unsafe {
    type Plus42 = unsafe extern "C" fn(n: usize) -> usize;
    let plus_42: Symbol<Plus42> = lib.get(b"plus_42").expect("find plus_42");
    assert_eq!(plus_42(13), 13 + 42);
    type Foo = unsafe extern "C" fn() -> *const u8;
    let foo: Symbol<Foo> = lib.get(b"foo").expect("find foo");
    let ptr = foo();
    let mut buf = [0u8; 3];
    buf.as_mut_ptr().copy_from(ptr, buf.len());
    assert_eq!(&buf[..], b"foo");
}
Run capsule build to make sure the contract can be built without errors.
Testing
We need to deploy the shared-lib.so to a cell, then reference the cell in the testing transaction.
Open tests/src/tests.rs.
use std::fs::File;
use std::io::Read;
// ...
// deploy shared library
let shared_lib_bin = {
    let mut buf = Vec::new();
    File::open("../shared-lib/shared-lib.so")
        .unwrap()
        .read_to_end(&mut buf)
        .expect("read code");
    Bytes::from(buf)
};
let shared_lib_out_point = context.deploy_cell(shared_lib_bin);
let shared_lib_dep = CellDep::new_builder().out_point(shared_lib_out_point).build();
// ...
// build transaction
let tx = TransactionBuilder::default()
    .input(input)
    .outputs(outputs)
    .outputs_data(outputs_data.pack())
    .cell_dep(lock_script_dep)
    // reference to shared library cell
    .cell_dep(shared_lib_dep)
    .build();
Run capsule test.
(click here to view response)
running 1 test
consume cycles: 1808802
test tests::test_basic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
   Doc-tests tests
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Other resources
- Full code
 - Basic usage of capsule: Write a SUDT script by Capsule
 - Secp256k1 dynamic loading example: ckb-dynamic-loading-secp256k1