Getting Started

Introduction to developing canisters in C++


Install icpp-pro

First install icpp-pro as explained in the Installation Guide

Create the greet project

Create the greet starter project with:

icpp init

This will create a complete C++ project, with:

greet-folder-structure

demo script

In the root folder of the greet project, you can find two demo scripts:

  • demo.ps1 , for Windows PowerShell
  • demo.sh, for Linux & Mac

You can run these scripts, as demonstrated in these short videos:

The sections below explain each step, and show how to run them from the command line.

Start local IC network

Start the local IC network with:

# In a Windows PowerShell (MiniConda is recommended):
wsl --% . ~/.local/share/dfx/env; dfx start --clean --background

# In a Linux/Mac terminal
dfx start --clean --background

Build the WebAssembly file

You compile & link your code with:

icpp build-wasm

icpp will compile the C++ files defined in the icpp.toml file and create a wasm file in the build folder. The name of the wasm file to be created is defined inside icpp.toml .

Deploy

Deploy the build/greet.wasm to a canister in the local network with:

# In a Windows PowerShell (MiniConda is recommended):
wsl --% . ~/.local/share/dfx/env; dfx deploy

# In a Linux/Mac terminal
dfx deploy

dfx deploys the files specified in dfx.json file.

Working with static libraries

The greet project has a src folder plus two static libraries:

greet/
|- libhello/
|  |- hello.cpp    # a C++ file of the static library 'libhello'
|  |- hello.h
|- libworld/
|  |- world.cpp    # a C++ file of the static library 'libworld'
|  |- world.h
|- src/
|  |- greet.cpp    # a C++ file of the smart contract
|  |- greet.h      # include file used by greet.cpp
|  |- greet.did    # candid file, containing the canister API spec

The [[build-library]] sections in the icpp.toml file specifies how the libraries must be build:

# file: icpp.toml
[[build-library]]
lib_name = "libhello"
cpp_paths = ["libhello/*.cpp"]
cpp_include_dirs = []
cpp_compile_flags = []
c_paths = []
c_include_dirs = []
c_compile_flags = []

[[build-library]]
lib_name = "libworld"
cpp_paths = ["libworld/*.cpp"]
cpp_include_dirs = []
cpp_compile_flags = []
c_paths = []
c_include_dirs = []
c_compile_flags = []

The following commands are available to work with these static libraries:

# Build everything, including the libraries
icpp build-wasm

# Once you have build all the libraries, and you only changed other code
# you can skip compiling the libraries
icpp build-wasm --to-compile mine-no-lib

# To build just a library. Omit LIBRARY_NAME to build all the libraries
icpp build-library [LIBRARY_NAME]

When you issue the icpp build-wasm command, icpp will first compile & build a static library for each library defined in the icpp.toml file. Each library will be build in it's own folder with name ./build-library/<lib_name/lib_name.a. In our example:

greet/
|- build-library
|  |- libhello
|  |  |- libhello.a
|  |- libworld
|  |  |- libworld.a

Note that there will also be a static library for the internal IC and CANDID code, named __ic_candid__.a. This is built automatically.

Then, icpp will create a wasm file in the build folder. The name of the wasm file to be created is defined inside icpp.toml. In our example:

greet/
|- build
|  |- greet.wasm

Test

Test with dfx

You can test the APIs from the command line, using dfx canister call.

For example, call the greet_2 API of your deployed greet canister with:

# In a Windows PowerShell (MiniConda is recommended):
wsl --% . ~/.local/share/dfx/env; dfx canister --network local call greet greet_2 '("C++ Developer")'

# In a Linux/Mac terminal
dfx canister --network local call greet greet_2 '("C++ Developer")'

# The response will be:
(
  "hello C++ Developer!\nYour principal is: .....-..etc..",
)

Test with Candid UI

You can also test the APIs from your browser, using Candid UI, just by clicking on the link printed by the dfx deploy command.

Test automation with pytest

icpp-pro comes with a smoke testing framework.

To run the automated API tests against your locally deployed canister, issue this command:

# from directory: `greet`

# Make sure the canister is deployed as described above.
# Then run the smoke tests using pytest
pytest --network local
=================== test session starts ===================
platform linux -- Python 3.10.8, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/<path>/icpp
plugins: anyio-3.6.2
collected 4 items                                         

test/test_apis.py ..                                [100%]

==================== 4 passed in 0.13s ====================

Notes:

  • pytest is installed automatically with icpp.

  • pytest will automatically detect the test folder:

greet/
|- test/
|  |- conftest.py   # pytest fixtures
|  |- test_apis.py  # the API tests
|  |- __init__.py   # empty file turning `test` into a python package
                    # -> required by pytest to resolve fixture imports

Debug with VS Code

icpp-pro comes with a native development framework.

build-native

To create a native executable with the build in Mock IC, use the command:

icpp build-native

Run mockic.exe

You can run the native executable directly, with the command:

build-native/mockic.exe

Debug interactively

First, Install & Configure VS Code for use with icpp.

Then set breakpoints in your code and start the debugger.

The MockIC that was included in the executable will run the tests that you specified in native/main.cpp and call your Smart Contract functions as if they're run in a canister.

This allows you to debug your Smart Contract interactively, not having to rely on debug_print statements alone. This speeds up your development process tremendously.

Next Steps

You can find additional Smart Contract examples in the icpp-demos repository.

IC Limitations

For the most part you can write any style of C++20, but the IC has two limitations to be aware of.

No file system

If your C++ code is making use of the file system, you will get an error message during compile time.

icpp uses the wasi-sdk compiler and sometimes, even when you are not using the file-system, the compiled Wasm module still includes functions that use the file-system and as a result it adds external system interfaces that the IC canister system API does not support. You will get an error during deployment to the canister.

To work around that issue, we have implemented traps to stub those functions. This allows you to compile & deploy your code without the system interfaces being added to the Wasm module, and if it so happens that you actually do use them, a trap with a clear message will occur at runtime.

1000 Globals

The IC has a hard limit allowing 1000 globals.

If your compiled Wasm module has more, then you will get this error message during deployment:

$ dfx deploy
...
Error: Failed while trying to deploy canisters.
Caused by: Failed while trying to deploy canisters.
  Failed while trying to install all canisters.
    Failed to install wasm module to canister 'greet'.
      Failed to install wasm in canister 'rrkah-fqaaa-aaaaa-aaaaq-cai'.
        Failed to install wasm.
          The Replica returned an error: code 5, message: 
          "Wasm module of canister rrkah-fqaaa-aaaaa-aaaaq-cai is not valid: 
          Wasm module defined 2000 globals which exceeds the maximum number allowed 1000."

You can check how many globals are in your compiled Wasm module with these commands:

icpp build-wasm
wasm2wat <path-to-wasm> | grep "(global (;" | wc