Skip to main content

Dynamic loading in Capsule


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


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 docker container to reproducible build contracts. It's also used by cross.
  • cross-rs - Capsule uses cross to build rust contracts. Install with
# Do this after you installed cargo
cargo install cross --git

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 --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 ""
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

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

docker run --rm -v `pwd`:/code ${BUILDER_DOCKER} bash -c "cd /code && make" shared-lib.c
$(CC) $(CFLAGS) $(LDFLAGS) -shared -o $@ $<
$(OBJCOPY) --only-keep-debug $@ [email protected]
$(OBJCOPY) --strip-debug --strip-all $@

Run make all-via-docker to compile the

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 file:

touch contracts/dynamic-loading-demo/

The (aka build scripts) execute on the building stage, see details , in we compute the data_hash of and put the result into a constant variable.

Add blake2b crate as build dependencies.

blake2b-rs = "0.1.5"

Write the constant CODE_HASH_SHARED_LIB to file

pub use blake2b_rs::{Blake2b, Blake2bBuilder};

use std::{
io::{BufWriter, Read, Write},

const BUF_SIZE: usize = 8 * 1024;
const CKB_HASH_PERSONALIZATION: &[u8] = b"ckb-default-hash";

fn main() {
let out_path = Path::new("src").join("");
let mut out_file = BufWriter::new(File::create(&out_path).expect("create"));

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 = buf).expect("read file");
if read_bytes > 0 {
} else {

let mut hash = [0u8; 32];
blake2b.finalize(&mut hash);

&mut out_file,
"pub const {}: [u8; 32] = {:?};\n",
format!("CODE_HASH_{}", name.to_uppercase().replace("-", "_")),
.expect("write to");

pub fn new_blake2b() -> Blake2b {

Run capsule build, the file src/ will be generated.

We define the module code_hashes in the 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.


We need to deploy the to a cell, then reference the cell in the testing transaction. Open tests/src/

use std::fs::File;
use std::io::Read;
// ...
// deploy shared library
let shared_lib_bin = {
let mut buf = Vec::new();
.read_to_end(&mut buf)
.expect("read code");
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()
// reference to shared library cell

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